提交 37457551 编写于 作者: B Ben Darnell

test: Count writes to stderr as failures

Python 3 logs warnings in destructors if objects responsible for file
descriptors are not explicitly closed. These warnings were previously
being ignored by our test suite unless a human was looking at the log
output, but several recent PRs have introduced these issues. This
change ensures we catch them (and fixes the most recent one in
process_test. The leak has always been there, but the previous commit
caused it to be logged).
上级 81dd461d
...@@ -198,7 +198,9 @@ class Subprocess(object): ...@@ -198,7 +198,9 @@ class Subprocess(object):
* ``stdin``, ``stdout``, and ``stderr`` may have the value * ``stdin``, ``stdout``, and ``stderr`` may have the value
``tornado.process.Subprocess.STREAM``, which will make the corresponding ``tornado.process.Subprocess.STREAM``, which will make the corresponding
attribute of the resulting Subprocess a `.PipeIOStream`. attribute of the resulting Subprocess a `.PipeIOStream`. If this option
is used, the caller is responsible for closing the streams when done
with them.
The ``Subprocess.STREAM`` option and the ``set_exit_callback`` and The ``Subprocess.STREAM`` option and the ``set_exit_callback`` and
``wait_for_exit`` methods do not work on Windows. There is ``wait_for_exit`` methods do not work on Windows. There is
......
...@@ -157,6 +157,8 @@ class SubprocessTest(AsyncTestCase): ...@@ -157,6 +157,8 @@ class SubprocessTest(AsyncTestCase):
stdin=Subprocess.STREAM, stdin=Subprocess.STREAM,
stdout=Subprocess.STREAM, stderr=subprocess.STDOUT) stdout=Subprocess.STREAM, stderr=subprocess.STDOUT)
self.addCleanup(lambda: (subproc.proc.terminate(), subproc.proc.wait())) self.addCleanup(lambda: (subproc.proc.terminate(), subproc.proc.wait()))
self.addCleanup(subproc.stdout.close)
self.addCleanup(subproc.stdin.close)
subproc.stdout.read_until(b'>>> ', self.stop) subproc.stdout.read_until(b'>>> ', self.stop)
self.wait() self.wait()
subproc.stdin.write(b"print('hello')\n") subproc.stdin.write(b"print('hello')\n")
...@@ -195,6 +197,8 @@ class SubprocessTest(AsyncTestCase): ...@@ -195,6 +197,8 @@ class SubprocessTest(AsyncTestCase):
subproc.stderr.read_until(b'\n', self.stop) subproc.stderr.read_until(b'\n', self.stop)
data = self.wait() data = self.wait()
self.assertEqual(data, b'hello\n') self.assertEqual(data, b'hello\n')
# More mysterious EBADF: This fails if done with self.addCleanup instead of here.
subproc.stderr.close()
def test_sigchild(self): def test_sigchild(self):
# Twisted's SIGCHLD handler and Subprocess's conflict with each other. # Twisted's SIGCHLD handler and Subprocess's conflict with each other.
...@@ -224,6 +228,7 @@ class SubprocessTest(AsyncTestCase): ...@@ -224,6 +228,7 @@ class SubprocessTest(AsyncTestCase):
subproc = Subprocess([sys.executable, '-c', subproc = Subprocess([sys.executable, '-c',
'import time; time.sleep(30)'], 'import time; time.sleep(30)'],
stdout=Subprocess.STREAM) stdout=Subprocess.STREAM)
self.addCleanup(subproc.stdout.close)
subproc.set_exit_callback(self.stop) subproc.set_exit_callback(self.stop)
os.kill(subproc.pid, signal.SIGTERM) os.kill(subproc.pid, signal.SIGTERM)
try: try:
......
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import gc import gc
import io
import locale # system locale module, not tornado.locale import locale # system locale module, not tornado.locale
import logging import logging
import operator import operator
...@@ -64,7 +65,11 @@ def all(): ...@@ -64,7 +65,11 @@ def all():
return unittest.defaultTestLoader.loadTestsFromNames(TEST_MODULES) return unittest.defaultTestLoader.loadTestsFromNames(TEST_MODULES)
class TornadoTextTestRunner(unittest.TextTestRunner): def test_runner_factory(stderr):
class TornadoTextTestRunner(unittest.TextTestRunner):
def __init__(self, *args, **kwargs):
super(TornadoTextTestRunner, self).__init__(*args, stream=stderr, **kwargs)
def run(self, test): def run(self, test):
result = super(TornadoTextTestRunner, self).run(test) result = super(TornadoTextTestRunner, self).run(test)
if result.skipped: if result.skipped:
...@@ -74,6 +79,7 @@ class TornadoTextTestRunner(unittest.TextTestRunner): ...@@ -74,6 +79,7 @@ class TornadoTextTestRunner(unittest.TextTestRunner):
", ".join(sorted(skip_reasons)))) ", ".join(sorted(skip_reasons))))
self.stream.write("\n") self.stream.write("\n")
return result return result
return TornadoTextTestRunner
class LogCounter(logging.Filter): class LogCounter(logging.Filter):
...@@ -93,6 +99,19 @@ class LogCounter(logging.Filter): ...@@ -93,6 +99,19 @@ class LogCounter(logging.Filter):
return True return True
class CountingStderr(io.IOBase):
def __init__(self, real):
self.real = real
self.byte_count = 0
def write(self, data):
self.byte_count += len(data)
return self.real.write(data)
def flush(self):
return self.real.flush()
def main(): def main():
# The -W command-line option does not work in a virtualenv with # The -W command-line option does not work in a virtualenv with
# python 3 (as of virtualenv 1.7), so configure warnings # python 3 (as of virtualenv 1.7), so configure warnings
...@@ -163,6 +182,12 @@ def main(): ...@@ -163,6 +182,12 @@ def main():
add_parse_callback( add_parse_callback(
lambda: logging.getLogger().handlers[0].addFilter(log_counter)) lambda: logging.getLogger().handlers[0].addFilter(log_counter))
# Certain errors (especially "unclosed resource" errors raised in
# destructors) go directly to stderr instead of logging. Count
# anything written by anything but the test runner as an error.
orig_stderr = sys.stderr
sys.stderr = CountingStderr(orig_stderr)
import tornado.testing import tornado.testing
kwargs = {} kwargs = {}
if sys.version_info >= (3, 2): if sys.version_info >= (3, 2):
...@@ -172,19 +197,19 @@ def main(): ...@@ -172,19 +197,19 @@ def main():
# suppresses this behavior, although this looks like an implementation # suppresses this behavior, although this looks like an implementation
# detail. http://bugs.python.org/issue15626 # detail. http://bugs.python.org/issue15626
kwargs['warnings'] = False kwargs['warnings'] = False
kwargs['testRunner'] = TornadoTextTestRunner kwargs['testRunner'] = test_runner_factory(orig_stderr)
try: try:
tornado.testing.main(**kwargs) tornado.testing.main(**kwargs)
finally: finally:
# The tests should run clean; consider it a failure if they # The tests should run clean; consider it a failure if they
# logged anything at info level or above (except for the one # logged anything at info level or above.
# allowed info message "PASS") if (log_counter.info_count > 0 or
if (log_counter.info_count > 1 or
log_counter.warning_count > 0 or log_counter.warning_count > 0 or
log_counter.error_count > 0): log_counter.error_count > 0 or
logging.error("logged %d infos, %d warnings, and %d errors", sys.stderr.byte_count > 0):
logging.error("logged %d infos, %d warnings, %d errors, and %d bytes to stderr",
log_counter.info_count, log_counter.warning_count, log_counter.info_count, log_counter.warning_count,
log_counter.error_count) log_counter.error_count, sys.stderr.byte_count)
sys.exit(1) sys.exit(1)
......
...@@ -7,6 +7,7 @@ from tornado.testing import AsyncHTTPTestCase, AsyncTestCase, bind_unused_port, ...@@ -7,6 +7,7 @@ from tornado.testing import AsyncHTTPTestCase, AsyncTestCase, bind_unused_port,
from tornado.web import Application from tornado.web import Application
import contextlib import contextlib
import os import os
import platform
import traceback import traceback
import warnings import warnings
...@@ -120,6 +121,8 @@ class AsyncTestCaseWrapperTest(unittest.TestCase): ...@@ -120,6 +121,8 @@ class AsyncTestCaseWrapperTest(unittest.TestCase):
self.assertIn("should be decorated", result.errors[0][1]) self.assertIn("should be decorated", result.errors[0][1])
@skipBefore35 @skipBefore35
@unittest.skipIf(platform.python_implementation() == 'PyPy',
'pypy destructor warnings cannot be silenced')
def test_undecorated_coroutine(self): def test_undecorated_coroutine(self):
namespace = exec_test(globals(), locals(), """ namespace = exec_test(globals(), locals(), """
class Test(AsyncTestCase): class Test(AsyncTestCase):
......
...@@ -29,7 +29,7 @@ except ImportError: ...@@ -29,7 +29,7 @@ except ImportError:
netutil = None # type: ignore netutil = None # type: ignore
SimpleAsyncHTTPClient = None # type: ignore SimpleAsyncHTTPClient = None # type: ignore
Subprocess = None # type: ignore Subprocess = None # type: ignore
from tornado.log import gen_log, app_log from tornado.log import app_log
from tornado.stack_context import ExceptionStackContext from tornado.stack_context import ExceptionStackContext
from tornado.util import raise_exc_info, basestring_type, PY3 from tornado.util import raise_exc_info, basestring_type, PY3
import functools import functools
...@@ -638,6 +638,12 @@ def main(**kwargs): ...@@ -638,6 +638,12 @@ def main(**kwargs):
to show many test details as they are run. to show many test details as they are run.
See http://docs.python.org/library/unittest.html#unittest.main See http://docs.python.org/library/unittest.html#unittest.main
for full argument list. for full argument list.
.. versionchanged:: 5.0
This function produces no output of its own; only that produced
by the `unittest` module (Previously it would add a PASS or FAIL
log message).
""" """
from tornado.options import define, options, parse_command_line from tornado.options import define, options, parse_command_line
...@@ -673,7 +679,6 @@ def main(**kwargs): ...@@ -673,7 +679,6 @@ def main(**kwargs):
if __name__ == '__main__' and len(argv) == 1: if __name__ == '__main__' and len(argv) == 1:
print("No tests specified", file=sys.stderr) print("No tests specified", file=sys.stderr)
sys.exit(1) sys.exit(1)
try:
# In order to be able to run tests by their fully-qualified name # In order to be able to run tests by their fully-qualified name
# on the command line without importing all tests here, # on the command line without importing all tests here,
# module must be set to None. Python 3.2's unittest.main ignores # module must be set to None. Python 3.2's unittest.main ignores
...@@ -684,12 +689,6 @@ def main(**kwargs): ...@@ -684,12 +689,6 @@ def main(**kwargs):
unittest.main(module=None, argv=argv, **kwargs) unittest.main(module=None, argv=argv, **kwargs)
else: else:
unittest.main(defaultTest="all", argv=argv, **kwargs) unittest.main(defaultTest="all", argv=argv, **kwargs)
except SystemExit as e:
if e.code == 0:
gen_log.info('PASS')
else:
gen_log.error('FAIL')
raise
if __name__ == '__main__': if __name__ == '__main__':
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册