<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[265769] trunk/Tools</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/265769">265769</a></dd>
<dt>Author</dt> <dd>jbedard@apple.com</dd>
<dt>Date</dt> <dd>2020-08-17 13:20:12 -0700 (Mon, 17 Aug 2020)</dd>
</dl>

<h3>Log Message</h3>
<pre>[webkitcorepy] Add OutputCapture to webkitcorepy
https://bugs.webkit.org/show_bug.cgi?id=215380
<rdar://problem/66846384>

Reviewed by Dewei Zhu.

Although webkitpy has an OutputCapture class, that class does not separate logging from
stdout and stderr, which makes the API less useful. This version of the OutputCapture class
is inspired by the one in webkitpy, but with a cleaner interface.

* Scripts/libraries/webkitcorepy/README.md: Document usage of OutputCapture.
* Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py: Export LoggerCapture, OutputCapture and
OutputDuplicate as API, bump version.
* Scripts/libraries/webkitcorepy/webkitcorepy/output_capture.py: Added.
(LoggerCapture): Capture logs at some level for the duration of the interface.
(OutputCapture): Capture stdout, stderr and all logging channels for the duration of the interface.
(OutputCapture.ReplaceSysStream): Replace sys.stdout or sys.stderr for a block.
(OutputDuplicate): Context which can duplicate all output for a block of code. This is useful to both print and
capture logs.
(OutputDuplicate.Stream): File-like object that routes output to multiple file-like objects.
* Scripts/libraries/webkitcorepy/webkitcorepy/tests/output_capture_unittest.py: Added.
(LoggerCaptureTest):
(OutputCaptureTest):
(OutputOutputDuplicateTest):</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkToolsChangeLog">trunk/Tools/ChangeLog</a></li>
<li><a href="#trunkToolsScriptslibrarieswebkitcorepyREADMEmd">trunk/Tools/Scripts/libraries/webkitcorepy/README.md</a></li>
<li><a href="#trunkToolsScriptslibrarieswebkitcorepywebkitcorepy__init__py">trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunkToolsScriptslibrarieswebkitcorepywebkitcorepyoutput_capturepy">trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/output_capture.py</a></li>
<li><a href="#trunkToolsScriptslibrarieswebkitcorepywebkitcorepytestsoutput_capture_unittestpy">trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/tests/output_capture_unittest.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkToolsChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Tools/ChangeLog (265768 => 265769)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/ChangeLog    2020-08-17 19:54:18 UTC (rev 265768)
+++ trunk/Tools/ChangeLog       2020-08-17 20:20:12 UTC (rev 265769)
</span><span class="lines">@@ -1,3 +1,30 @@
</span><ins>+2020-08-17  Jonathan Bedard  <jbedard@apple.com>
+
+        [webkitcorepy] Add OutputCapture to webkitcorepy
+        https://bugs.webkit.org/show_bug.cgi?id=215380
+        <rdar://problem/66846384>
+
+        Reviewed by Dewei Zhu.
+
+        Although webkitpy has an OutputCapture class, that class does not separate logging from
+        stdout and stderr, which makes the API less useful. This version of the OutputCapture class
+        is inspired by the one in webkitpy, but with a cleaner interface.
+
+        * Scripts/libraries/webkitcorepy/README.md: Document usage of OutputCapture.
+        * Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py: Export LoggerCapture, OutputCapture and
+        OutputDuplicate as API, bump version.
+        * Scripts/libraries/webkitcorepy/webkitcorepy/output_capture.py: Added.
+        (LoggerCapture): Capture logs at some level for the duration of the interface.
+        (OutputCapture): Capture stdout, stderr and all logging channels for the duration of the interface.
+        (OutputCapture.ReplaceSysStream): Replace sys.stdout or sys.stderr for a block.
+        (OutputDuplicate): Context which can duplicate all output for a block of code. This is useful to both print and
+        capture logs.
+        (OutputDuplicate.Stream): File-like object that routes output to multiple file-like objects.
+        * Scripts/libraries/webkitcorepy/webkitcorepy/tests/output_capture_unittest.py: Added.
+        (LoggerCaptureTest):
+        (OutputCaptureTest):
+        (OutputOutputDuplicateTest):
+
</ins><span class="cx"> 2020-08-17  Fujii Hironori  <Hironori.Fujii@sony.com>
</span><span class="cx"> 
</span><span class="cx">         [TestWebKitAPI] Some WTF_HashMap tests are failing if TestWTF is executed directly
</span></span></pre></div>
<a id="trunkToolsScriptslibrarieswebkitcorepyREADMEmd"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/webkitcorepy/README.md (265768 => 265769)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/webkitcorepy/README.md     2020-08-17 19:54:18 UTC (rev 265768)
+++ trunk/Tools/Scripts/libraries/webkitcorepy/README.md        2020-08-17 20:20:12 UTC (rev 265769)
</span><span class="lines">@@ -43,3 +43,17 @@
</span><span class="cx">     stamp = time.time()
</span><span class="cx">     time.sleep(5)
</span><span class="cx"> ```
</span><ins>+Capturing stdout, stderr and logging output for testing
+```
+capturer = OutputCapture()
+with capturer:
+    print('data\n')
+assert capturer.stdout.getvalue() == 'data\n'
+```
+Capturing stdout, stderr and logging output for testing
+```
+capturer = OutputCapture()
+with capturer:
+    print('data\n')
+assert capturer.stdout.getvalue() == 'data\n'
+```
</ins></span></pre></div>
<a id="trunkToolsScriptslibrarieswebkitcorepywebkitcorepy__init__py"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py (265768 => 265769)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py      2020-08-17 19:54:18 UTC (rev 265768)
+++ trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py 2020-08-17 20:20:12 UTC (rev 265769)
</span><span class="lines">@@ -31,8 +31,9 @@
</span><span class="cx"> 
</span><span class="cx"> from webkitcorepy.version import Version
</span><span class="cx"> from webkitcorepy.string_utils import BytesIO, StringIO, UnicodeIO, unicode
</span><ins>+from webkitcorepy.output_capture import LoggerCapture, OutputCapture, OutputDuplicate
</ins><span class="cx"> 
</span><del>-version = Version(0, 2, 4)
</del><ins>+version = Version(0, 2, 5)
</ins><span class="cx"> 
</span><span class="cx"> from webkitcorepy.autoinstall import Package, AutoInstall
</span><span class="cx"> if sys.version_info > (3, 0):
</span></span></pre></div>
<a id="trunkToolsScriptslibrarieswebkitcorepywebkitcorepyoutput_capturepy"></a>
<div class="addfile"><h4>Added: trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/output_capture.py (0 => 265769)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/output_capture.py                                (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/output_capture.py   2020-08-17 20:20:12 UTC (rev 265769)
</span><span class="lines">@@ -0,0 +1,202 @@
</span><ins>+# Copyright (C) 2020 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1.  Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import contextlib
+import io
+import logging
+import sys
+
+from webkitcorepy import log, mocks
+from webkitcorepy.string_utils import StringIO
+
+
+class LoggerCapture(object):
+    def __init__(self, logger=None, level=logging.WARNING):
+        self.logger = logger or logging.getLogger()
+        self.level = level
+
+        self._original_log_level_stack = []
+        self._original_handlers_stack = []
+        self._handler_stack = []
+        self.log = StringIO()
+
+    def __enter__(self):
+        self._original_log_level_stack.append(self.logger.level)
+
+        self._original_handlers_stack.append([handler for handler in self.logger.handlers])
+        for handler in self._original_handlers_stack[-1]:
+            self.logger.removeHandler(handler)
+
+        context_handler = logging.StreamHandler(self.log)
+        self._handler_stack.append(context_handler)
+        context_handler.setLevel(self.level)
+
+        self.logger.addHandler(context_handler)
+        self.logger.setLevel(self.level)
+
+        return self
+
+    def __exit__(self, *args, **kwargs):
+        if self._handler_stack:
+            to_remove = self._handler_stack.pop()
+            to_remove.flush()
+            self.logger.removeHandler(to_remove)
+
+        if self._original_log_level_stack:
+            self.logger.setLevel(self._original_log_level_stack.pop())
+
+        if self._original_handlers_stack is not None:
+            for handler in self._original_handlers_stack.pop():
+                self.logger.addHandler(handler)
+
+
+class OutputCapture(mocks.ContextStack):
+    top = None
+
+    class ReplaceSysStream(object):
+        def __init__(self, name, stream):
+            self.name = name
+            if self.name not in {'stdout', 'stderr'}:
+                raise ValueError("'{}' is not a valid system stream name".format(self.name))
+            self.stream = stream
+            self.originals = []
+
+        def __enter__(self):
+            self.originals.append(getattr(sys, self.name))
+            setattr(sys, self.name, self.stream)
+            return self
+
+        def __exit__(self, *args, **kwargs):
+            setattr(sys, self.name, self.originals.pop())
+
+    def __init__(self, loggers=None, level=logging.WARNING):
+        super(OutputCapture, self).__init__(cls=OutputCapture)
+
+        self.stdout = StringIO()
+        self.stderr = StringIO()
+
+        if not loggers:
+            loggers = [logging.getLogger(), log]
+
+        self.patches.append(self.ReplaceSysStream('stdout', self.stdout))
+        self.patches.append(self.ReplaceSysStream('stderr', self.stderr))
+
+        for logger in loggers:
+            if logger.name in ['stdout', 'stderr', 'patches', 'top']:
+                raise ValueError('{} collides with an OutputCapture member'.format(logger.name))
+            lc = LoggerCapture(logger, level)
+            setattr(self, logger.name, lc)
+            self.patches.append(lc)
+
+
+class OutputDuplicate(mocks.ContextStack):
+    top = None
+
+    class Stream(io.IOBase):
+        def __init__(self, *targets):
+            if not targets:
+                raise ValueError('No target streams provided')
+            self.targets = targets
+
+        def flush(self):
+            [target.flush() for target in self.targets]
+
+        def writelines(self, lines):
+            [target.writelines(lines) for target in self.targets]
+
+        def write(self, b):
+            [target.write(b) for target in self.targets]
+            return len(b)
+
+        @property
+        def closed(self):
+            return all([target.closed for target in self.targets])
+
+        def close(self):
+            pass
+
+        def fileno(self):
+            # Use the first valid file number
+            for target in self.targets:
+                if target.fileno():
+                    return target.fileno()
+            return None
+
+        def isatty(self):
+            return any([target.isatty() for target in self.targets])
+
+        def readable(self):
+            return False
+
+        def readline(self, size=-1):
+            raise NotImplementedError()
+
+        def readlines(self, hint=-1):
+            raise NotImplementedError()
+
+        def seek(self, offset, whence=io.SEEK_SET):
+            raise NotImplementedError()
+
+        def seekable(self):
+            return False
+
+        def tell(self):
+            raise NotImplementedError()
+
+        def truncate(self, size=None):
+            raise NotImplementedError()
+
+        def writable(self):
+            return True
+
+    def __init__(self, loggers=None):
+        super(OutputDuplicate, self).__init__(cls=OutputDuplicate)
+
+        self.output = StringIO()
+        self.loggers = [logging.getLogger()] or loggers
+
+        self._handler_stack = []
+        self._streams_stack = []
+
+    def __enter__(self):
+        self._streams_stack.append((
+            OutputCapture.ReplaceSysStream('stdout', self.Stream(sys.stdout, self.output)).__enter__(),
+            OutputCapture.ReplaceSysStream('stderr', self.Stream(sys.stderr, self.output)).__enter__(),
+        ))
+
+        context_handler = logging.StreamHandler(self.output)
+        self._handler_stack.append(context_handler)
+        context_handler.setLevel(min([logger.level for logger in self.loggers]))
+        [logger.addHandler(context_handler) for logger in self.loggers]
+
+        return super(OutputDuplicate, self).__enter__()
+
+    def __exit__(self, *args, **kwargs):
+        super(OutputDuplicate, self).__exit__(*args, **kwargs)
+
+        context_handler = self._handler_stack.pop()
+        context_handler.flush()
+        [logger.removeHandler(context_handler) for logger in self.loggers]
+
+        stdout_stream, stderr_stream = self._streams_stack.pop()
+        stdout_stream.__exit__(*args, **kwargs)
+        stderr_stream.__exit__(*args, **kwargs)
</ins></span></pre></div>
<a id="trunkToolsScriptslibrarieswebkitcorepywebkitcorepytestsoutput_capture_unittestpy"></a>
<div class="addfile"><h4>Added: trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/tests/output_capture_unittest.py (0 => 265769)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/tests/output_capture_unittest.py                         (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/tests/output_capture_unittest.py    2020-08-17 20:20:12 UTC (rev 265769)
</span><span class="lines">@@ -0,0 +1,106 @@
</span><ins>+# Copyright (C) 2020 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1.  Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import logging
+import sys
+import unittest
+
+from webkitcorepy import log, LoggerCapture, OutputCapture, OutputDuplicate
+
+
+class LoggerCaptureTest(unittest.TestCase):
+    def test_basic(self):
+        with LoggerCapture(log) as capturer, LoggerCapture():
+            log.info('Hidden')
+            log.warn('Printed')
+
+        self.assertEqual(capturer.log.getvalue(), 'Printed\n')
+
+    def test_level(self):
+        with LoggerCapture(log, level=logging.INFO) as capturer, LoggerCapture():
+            log.debug('Hidden')
+            log.info('Printed 1')
+            log.warn('Printed 2')
+
+        self.assertEqual(capturer.log.getvalue(), 'Printed 1\nPrinted 2\n')
+
+    def test_multiple_entry(self):
+        with LoggerCapture(log, level=logging.INFO) as capturer, LoggerCapture():
+            log.info('Level 1')
+            with capturer:
+                log.info('Level 2')
+
+        self.assertEqual(capturer.log.getvalue(), 'Level 1\nLevel 2\n')
+
+
+class OutputCaptureTest(unittest.TestCase):
+    def test_basic(self):
+        with OutputCapture() as capturer:
+            log.info('Hidden')
+            log.warn('Printed')
+            sys.stdout.write('stdout\n')
+            sys.stderr.write('stderr\n')
+
+        self.assertEqual(capturer.webkitcorepy.log.getvalue(), 'Printed\n')
+        self.assertEqual(capturer.stdout.getvalue(), 'stdout\n')
+        self.assertEqual(capturer.stderr.getvalue(), 'stderr\n')
+
+    def test_multiple_entry(self):
+        with OutputCapture() as captured:
+            sys.stdout.write('Line 1\n')
+            log.warn('Log 1')
+            with captured:
+                sys.stdout.write('Line 2\n')
+                log.warn('Log 2')
+
+        self.assertEqual(captured.webkitcorepy.log.getvalue(), 'Log 1\nLog 2\n')
+        self.assertEqual(captured.stdout.getvalue(), 'Line 1\nLine 2\n')
+
+
+class OutputDuplicateTest(unittest.TestCase):
+    def test_basic(self):
+        with OutputCapture() as capturer:
+            with OutputDuplicate() as duplicator:
+                log.info('Hidden')
+                log.warn('Printed')
+                sys.stdout.write('stdout\n')
+                sys.stderr.write('stderr\n')
+
+            self.assertEqual(duplicator.output.getvalue(), 'Printed\nstdout\nstderr\n')
+
+        self.assertEqual(capturer.webkitcorepy.log.getvalue(), 'Printed\n')
+        self.assertEqual(capturer.stdout.getvalue(), 'stdout\n')
+        self.assertEqual(capturer.stderr.getvalue(), 'stderr\n')
+
+    def test_multiple_entry(self):
+        with OutputCapture(level=logging.INFO) as captuered:
+            with OutputDuplicate() as duplicator:
+                log.info('Log 1')
+                sys.stdout.write('Level 1\n')
+                with duplicator:
+                    log.info('Log 2')
+                    sys.stdout.write('Level 2\n')
+
+            self.assertEqual(duplicator.output.getvalue(), 'Log 1\nLevel 1\nLog 2\nLog 2\nLevel 2\nLevel 2\n')
+
+        self.assertEqual(captuered.webkitcorepy.log.getvalue(), 'Log 1\nLog 2\n')
+        self.assertEqual(captuered.stdout.getvalue(), 'Level 1\nLevel 2\n')
</ins></span></pre>
</div>
</div>

</body>
</html>