Diff
Modified: trunk/Tools/ChangeLog (265768 => 265769)
--- trunk/Tools/ChangeLog 2020-08-17 19:54:18 UTC (rev 265768)
+++ trunk/Tools/ChangeLog 2020-08-17 20:20:12 UTC (rev 265769)
@@ -1,3 +1,30 @@
+2020-08-17 Jonathan Bedard <[email protected]>
+
+ [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):
+
2020-08-17 Fujii Hironori <[email protected]>
[TestWebKitAPI] Some WTF_HashMap tests are failing if TestWTF is executed directly
Modified: trunk/Tools/Scripts/libraries/webkitcorepy/README.md (265768 => 265769)
--- 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)
@@ -43,3 +43,17 @@
stamp = time.time()
time.sleep(5)
```
+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'
+```
Modified: trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py (265768 => 265769)
--- 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)
@@ -31,8 +31,9 @@
from webkitcorepy.version import Version
from webkitcorepy.string_utils import BytesIO, StringIO, UnicodeIO, unicode
+from webkitcorepy.output_capture import LoggerCapture, OutputCapture, OutputDuplicate
-version = Version(0, 2, 4)
+version = Version(0, 2, 5)
from webkitcorepy.autoinstall import Package, AutoInstall
if sys.version_info > (3, 0):
Added: trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/output_capture.py (0 => 265769)
--- 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)
@@ -0,0 +1,202 @@
+# 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)
Added: trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/tests/output_capture_unittest.py (0 => 265769)
--- 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)
@@ -0,0 +1,106 @@
+# 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')