Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
testing.py867 linesDownload Raw Back to tornado
1"""Support classes for automated testing.
2
3* `AsyncTestCase` and `AsyncHTTPTestCase`:  Subclasses of unittest.TestCase
4  with additional support for testing asynchronous (`.IOLoop`-based) code.
5
6* `ExpectLog`: Make test logs less spammy.
7
8* `main()`: A simple test runner (wrapper around unittest.main()) with support
9  for the tornado.autoreload module to rerun the tests when code changes.
10"""
11
12import asyncio
13from collections.abc import Generator
14import functools
15import inspect
16import logging
17import os
18import re
19import signal
20import socket
21import sys
22import unittest
23import warnings
24
25from tornado import gen
26from tornado.httpclient import AsyncHTTPClient, HTTPResponse
27from tornado.httpserver import HTTPServer
28from tornado.ioloop import IOLoop, TimeoutError
29from tornado import netutil
30from tornado.platform.asyncio import AsyncIOMainLoop
31from tornado.process import Subprocess
32from tornado.log import app_log
33from tornado.util import raise_exc_info, basestring_type
34from tornado.web import Application
35
36import typing
37from typing import Tuple, Any, Callable, Type, Dict, Union, Optional, Coroutine
38from types import TracebackType
39
40if typing.TYPE_CHECKING:
41    _ExcInfoTuple = Tuple[
42        Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]
43    ]
44
45
46_NON_OWNED_IOLOOPS = AsyncIOMainLoop
47
48
49def bind_unused_port(
50    reuse_port: bool = False, address: str = "127.0.0.1"
51) -> Tuple[socket.socket, int]:
52    """Binds a server socket to an available port on localhost.
53
54    Returns a tuple (socket, port).
55
56    .. versionchanged:: 4.4
57       Always binds to ``127.0.0.1`` without resolving the name
58       ``localhost``.
59
60    .. versionchanged:: 6.2
61       Added optional ``address`` argument to
62       override the default "127.0.0.1".
63    """
64    sock = netutil.bind_sockets(
65        0, address, family=socket.AF_INET, reuse_port=reuse_port
66    )[0]
67    port = sock.getsockname()[1]
68    return sock, port
69
70
71def get_async_test_timeout() -> float:
72    """Get the global timeout setting for async tests.
73
74    Returns a float, the timeout in seconds.
75
76    .. versionadded:: 3.1
77    """
78    env = os.environ.get("ASYNC_TEST_TIMEOUT")
79    if env is not None:
80        try:
81            return float(env)
82        except ValueError:
83            pass
84    return 5
85
86
87class AsyncTestCase(unittest.TestCase):
88    """`~unittest.TestCase` subclass for testing `.IOLoop`-based
89    asynchronous code.
90
91    The unittest framework is synchronous, so the test must be
92    complete by the time the test method returns. This means that
93    asynchronous code cannot be used in quite the same way as usual
94    and must be adapted to fit. To write your tests with coroutines,
95    decorate your test methods with `tornado.testing.gen_test` instead
96    of `tornado.gen.coroutine`.
97
98    This class also provides the (deprecated) `stop()` and `wait()`
99    methods for a more manual style of testing. The test method itself
100    must call ``self.wait()``, and asynchronous callbacks should call
101    ``self.stop()`` to signal completion.
102
103    By default, a new `.IOLoop` is constructed for each test and is available
104    as ``self.io_loop``.  If the code being tested requires a
105    reused global `.IOLoop`, subclasses should override `get_new_ioloop` to return it,
106    although this is deprecated as of Tornado 6.3.
107
108    The `.IOLoop`'s ``start`` and ``stop`` methods should not be
109    called directly.  Instead, use `self.stop <stop>` and `self.wait
110    <wait>`.  Arguments passed to ``self.stop`` are returned from
111    ``self.wait``.  It is possible to have multiple ``wait``/``stop``
112    cycles in the same test.
113
114    Example::
115
116        # This test uses coroutine style.
117        class MyTestCase(AsyncTestCase):
118            @tornado.testing.gen_test
119            def test_http_fetch(self):
120                client = AsyncHTTPClient()
121                response = yield client.fetch("http://www.tornadoweb.org")
122                # Test contents of response
123                self.assertIn("FriendFeed", response.body)
124
125        # This test uses argument passing between self.stop and self.wait.
126        class MyTestCase2(AsyncTestCase):
127            def test_http_fetch(self):
128                client = AsyncHTTPClient()
129                client.fetch("http://www.tornadoweb.org/", self.stop)
130                response = self.wait()
131                # Test contents of response
132                self.assertIn("FriendFeed", response.body)
133    """
134
135    def __init__(self, methodName: str = "runTest") -> None:
136        super().__init__(methodName)
137        self.__stopped = False
138        self.__running = False
139        self.__failure = None  # type: Optional[_ExcInfoTuple]
140        self.__stop_args = None  # type: Any
141        self.__timeout = None  # type: Optional[object]
142
143        # Not used in this class itself, but used by @gen_test
144        self._test_generator = None  # type: Optional[Union[Generator, Coroutine]]
145
146    def setUp(self) -> None:
147        py_ver = sys.version_info
148        if ((3, 10, 0) <= py_ver < (3, 10, 9)) or ((3, 11, 0) <= py_ver <= (3, 11, 1)):
149            # Early releases in the Python 3.10 and 3.1 series had deprecation
150            # warnings that were later reverted; we must suppress them here.
151            setup_with_context_manager(self, warnings.catch_warnings())
152            warnings.filterwarnings(
153                "ignore",
154                message="There is no current event loop",
155                category=DeprecationWarning,
156                module=r"tornado\..*",
157            )
158        super().setUp()
159        if type(self).get_new_ioloop is not AsyncTestCase.get_new_ioloop:
160            warnings.warn("get_new_ioloop is deprecated", DeprecationWarning)
161        self.io_loop = self.get_new_ioloop()
162        asyncio.set_event_loop(self.io_loop.asyncio_loop)  # type: ignore[attr-defined]
163
164    def tearDown(self) -> None:
165        # Native coroutines tend to produce warnings if they're not
166        # allowed to run to completion. It's difficult to ensure that
167        # this always happens in tests, so cancel any tasks that are
168        # still pending by the time we get here.
169        asyncio_loop = self.io_loop.asyncio_loop  # type: ignore
170        tasks = asyncio.all_tasks(asyncio_loop)
171        # Tasks that are done may still appear here and may contain
172        # non-cancellation exceptions, so filter them out.
173        tasks = [t for t in tasks if not t.done()]  # type: ignore
174        for t in tasks:
175            t.cancel()
176        # Allow the tasks to run and finalize themselves (which means
177        # raising a CancelledError inside the coroutine). This may
178        # just transform the "task was destroyed but it is pending"
179        # warning into a "uncaught CancelledError" warning, but
180        # catching CancelledErrors in coroutines that may leak is
181        # simpler than ensuring that no coroutines leak.
182        if tasks:
183            done, pending = self.io_loop.run_sync(lambda: asyncio.wait(tasks))
184            assert not pending
185            # If any task failed with anything but a CancelledError, raise it.
186            for f in done:
187                try:
188                    f.result()
189                except asyncio.CancelledError:
190                    pass
191
192        # Clean up Subprocess, so it can be used again with a new ioloop.
193        Subprocess.uninitialize()
194        asyncio.set_event_loop(None)
195        if not isinstance(self.io_loop, _NON_OWNED_IOLOOPS):
196            # Try to clean up any file descriptors left open in the ioloop.
197            # This avoids leaks, especially when tests are run repeatedly
198            # in the same process with autoreload (because curl does not
199            # set FD_CLOEXEC on its file descriptors)
200            self.io_loop.close(all_fds=True)
201        super().tearDown()
202        # In case an exception escaped or the StackContext caught an exception
203        # when there wasn't a wait() to re-raise it, do so here.
204        # This is our last chance to raise an exception in a way that the
205        # unittest machinery understands.
206        self.__rethrow()
207
208    def get_new_ioloop(self) -> IOLoop:
209        """Returns the `.IOLoop` to use for this test.
210
211        By default, a new `.IOLoop` is created for each test.
212        Subclasses may override this method to return
213        `.IOLoop.current()` if it is not appropriate to use a new
214        `.IOLoop` in each tests (for example, if there are global
215        singletons using the default `.IOLoop`) or if a per-test event
216        loop is being provided by another system (such as
217        ``pytest-asyncio``).
218
219        .. deprecated:: 6.3
220           This method will be removed in Tornado 7.0.
221        """
222        return IOLoop(make_current=False)
223
224    def _handle_exception(
225        self, typ: Type[Exception], value: Exception, tb: TracebackType
226    ) -> bool:
227        if self.__failure is None:
228            self.__failure = (typ, value, tb)
229        else:
230            app_log.error(
231                "multiple unhandled exceptions in test", exc_info=(typ, value, tb)
232            )
233        self.stop()
234        return True
235
236    def __rethrow(self) -> None:
237        if self.__failure is not None:
238            failure = self.__failure
239            self.__failure = None
240            raise_exc_info(failure)
241
242    def run(
243        self, result: Optional[unittest.TestResult] = None
244    ) -> Optional[unittest.TestResult]:
245        ret = super().run(result)
246        # As a last resort, if an exception escaped super.run() and wasn't
247        # re-raised in tearDown, raise it here.  This will cause the
248        # unittest run to fail messily, but that's better than silently
249        # ignoring an error.
250        self.__rethrow()
251        return ret
252
253    def _callTestMethod(self, method: Callable) -> None:
254        """Run the given test method, raising an error if it returns non-None.
255
256        Failure to decorate asynchronous test methods with ``@gen_test`` can lead to tests
257        incorrectly passing.
258
259        Remove this override when Python 3.10 support is dropped. This check (in the form of a
260        DeprecationWarning) became a part of the standard library in 3.11.
261
262        Note that ``_callTestMethod`` is not documented as a public interface. However, it is
263        present in all supported versions of Python (3.8+), and if it goes away in the future that's
264        OK because we can just remove this override as noted above.
265        """
266        # Calling super()._callTestMethod would hide the return value, even in python 3.8-3.10
267        # where the check isn't being done for us.
268        result = method()
269        if isinstance(result, Generator) or inspect.iscoroutine(result):
270            raise TypeError(
271                "Generator and coroutine test methods should be"
272                " decorated with tornado.testing.gen_test"
273            )
274        elif result is not None:
275            raise ValueError("Return value from test method ignored: %r" % result)
276
277    def stop(self, _arg: Any = None, **kwargs: Any) -> None:
278        """Stops the `.IOLoop`, causing one pending (or future) call to `wait()`
279        to return.
280
281        Keyword arguments or a single positional argument passed to `stop()` are
282        saved and will be returned by `wait()`.
283
284        .. deprecated:: 5.1
285
286           `stop` and `wait` are deprecated; use ``@gen_test`` instead.
287        """
288        assert _arg is None or not kwargs
289        self.__stop_args = kwargs or _arg
290        if self.__running:
291            self.io_loop.stop()
292            self.__running = False
293        self.__stopped = True
294
295    def wait(
296        self,
297        condition: Optional[Callable[..., bool]] = None,
298        timeout: Optional[float] = None,
299    ) -> Any:
300        """Runs the `.IOLoop` until stop is called or timeout has passed.
301
302        In the event of a timeout, an exception will be thrown. The
303        default timeout is 5 seconds; it may be overridden with a
304        ``timeout`` keyword argument or globally with the
305        ``ASYNC_TEST_TIMEOUT`` environment variable.
306
307        If ``condition`` is not ``None``, the `.IOLoop` will be restarted
308        after `stop()` until ``condition()`` returns ``True``.
309
310        .. versionchanged:: 3.1
311           Added the ``ASYNC_TEST_TIMEOUT`` environment variable.
312
313        .. deprecated:: 5.1
314
315           `stop` and `wait` are deprecated; use ``@gen_test`` instead.
316        """
317        if timeout is None:
318            timeout = get_async_test_timeout()
319
320        if not self.__stopped:
321            if timeout:
322
323                def timeout_func() -> None:
324                    try:
325                        raise self.failureException(
326                            "Async operation timed out after %s seconds" % timeout
327                        )
328                    except Exception:
329                        self.__failure = sys.exc_info()
330                    self.stop()
331
332                self.__timeout = self.io_loop.add_timeout(
333                    self.io_loop.time() + timeout, timeout_func
334                )
335            while True:
336                self.__running = True
337                self.io_loop.start()
338                if self.__failure is not None or condition is None or condition():
339                    break
340            if self.__timeout is not None:
341                self.io_loop.remove_timeout(self.__timeout)
342                self.__timeout = None
343        assert self.__stopped
344        self.__stopped = False
345        self.__rethrow()
346        result = self.__stop_args
347        self.__stop_args = None
348        return result
349
350
351class AsyncHTTPTestCase(AsyncTestCase):
352    """A test case that starts up an HTTP server.
353
354    Subclasses must override `get_app()`, which returns the
355    `tornado.web.Application` (or other `.HTTPServer` callback) to be tested.
356    Tests will typically use the provided ``self.http_client`` to fetch
357    URLs from this server.
358
359    Example, assuming the "Hello, world" example from the user guide is in
360    ``hello.py``::
361
362        import hello
363
364        class TestHelloApp(AsyncHTTPTestCase):
365            def get_app(self):
366                return hello.make_app()
367
368            def test_homepage(self):
369                response = self.fetch('/')
370                self.assertEqual(response.code, 200)
371                self.assertEqual(response.body, 'Hello, world')
372
373    That call to ``self.fetch()`` is equivalent to ::
374
375        self.http_client.fetch(self.get_url('/'), self.stop)
376        response = self.wait()
377
378    which illustrates how AsyncTestCase can turn an asynchronous operation,
379    like ``http_client.fetch()``, into a synchronous operation. If you need
380    to do other asynchronous operations in tests, you'll probably need to use
381    ``stop()`` and ``wait()`` yourself.
382    """
383
384    def setUp(self) -> None:
385        super().setUp()
386        sock, port = bind_unused_port()
387        self.__port = port
388
389        self.http_client = self.get_http_client()
390        self._app = self.get_app()
391        self.http_server = self.get_http_server()
392        self.http_server.add_sockets([sock])
393
394    def get_http_client(self) -> AsyncHTTPClient:
395        return AsyncHTTPClient()
396
397    def get_http_server(self) -> HTTPServer:
398        return HTTPServer(self._app, **self.get_httpserver_options())
399
400    def get_app(self) -> Application:
401        """Should be overridden by subclasses to return a
402        `tornado.web.Application` or other `.HTTPServer` callback.
403        """
404        raise NotImplementedError()
405
406    def fetch(
407        self, path: str, raise_error: bool = False, **kwargs: Any
408    ) -> HTTPResponse:
409        """Convenience method to synchronously fetch a URL.
410
411        The given path will be appended to the local server's host and
412        port.  Any additional keyword arguments will be passed directly to
413        `.AsyncHTTPClient.fetch` (and so could be used to pass
414        ``method="POST"``, ``body="..."``, etc).
415
416        If the path begins with http:// or https://, it will be treated as a
417        full URL and will be fetched as-is.
418
419        If ``raise_error`` is ``True``, a `tornado.httpclient.HTTPError` will
420        be raised if the response code is not 200. This is the same behavior
421        as the ``raise_error`` argument to `.AsyncHTTPClient.fetch`, but
422        the default is ``False`` here (it's ``True`` in `.AsyncHTTPClient`)
423        because tests often need to deal with non-200 response codes.
424
425        .. versionchanged:: 5.0
426           Added support for absolute URLs.
427
428        .. versionchanged:: 5.1
429
430           Added the ``raise_error`` argument.
431
432        .. deprecated:: 5.1
433
434           This method currently turns any exception into an
435           `.HTTPResponse` with status code 599. In Tornado 6.0,
436           errors other than `tornado.httpclient.HTTPError` will be
437           passed through, and ``raise_error=False`` will only
438           suppress errors that would be raised due to non-200
439           response codes.
440
441        """
442        if path.lower().startswith(("http://", "https://")):
443            url = path
444        else:
445            url = self.get_url(path)
446        return self.io_loop.run_sync(
447            lambda: self.http_client.fetch(url, raise_error=raise_error, **kwargs),
448            timeout=get_async_test_timeout(),
449        )
450
451    def get_httpserver_options(self) -> Dict[str, Any]:
452        """May be overridden by subclasses to return additional
453        keyword arguments for the server.
454        """
455        return {}
456
457    def get_http_port(self) -> int:
458        """Returns the port used by the server.
459
460        A new port is chosen for each test.
461        """
462        return self.__port
463
464    def get_protocol(self) -> str:
465        return "http"
466
467    def get_url(self, path: str) -> str:
468        """Returns an absolute url for the given path on the test server."""
469        return f"{self.get_protocol()}://127.0.0.1:{self.get_http_port()}{path}"
470
471    def tearDown(self) -> None:
472        self.http_server.stop()
473        self.io_loop.run_sync(
474            self.http_server.close_all_connections, timeout=get_async_test_timeout()
475        )
476        self.http_client.close()
477        del self.http_server
478        del self._app
479        super().tearDown()
480
481
482class AsyncHTTPSTestCase(AsyncHTTPTestCase):
483    """A test case that starts an HTTPS server.
484
485    Interface is generally the same as `AsyncHTTPTestCase`.
486    """
487
488    def get_http_client(self) -> AsyncHTTPClient:
489        return AsyncHTTPClient(force_instance=True, defaults=dict(validate_cert=False))
490
491    def get_httpserver_options(self) -> Dict[str, Any]:
492        return dict(ssl_options=self.get_ssl_options())
493
494    def get_ssl_options(self) -> Dict[str, Any]:
495        """May be overridden by subclasses to select SSL options.
496
497        By default includes a self-signed testing certificate.
498        """
499        return AsyncHTTPSTestCase.default_ssl_options()
500
501    @staticmethod
502    def default_ssl_options() -> Dict[str, Any]:
503        # Testing keys were generated with:
504        # openssl req -new -keyout tornado/test/test.key \
505        #     -out tornado/test/test.crt \
506        #     -nodes -days 3650 -x509 \
507        #     -subj "/CN=foo.example.com" -addext "subjectAltName = DNS:foo.example.com"
508        module_dir = os.path.dirname(__file__)
509        return dict(
510            certfile=os.path.join(module_dir, "test", "test.crt"),
511            keyfile=os.path.join(module_dir, "test", "test.key"),
512        )
513
514    def get_protocol(self) -> str:
515        return "https"
516
517
518@typing.overload
519def gen_test(
520    *, timeout: Optional[float] = None
521) -> Callable[[Callable[..., Union[Generator, "Coroutine"]]], Callable[..., None]]:
522    pass
523
524
525@typing.overload  # noqa: F811
526def gen_test(func: Callable[..., Union[Generator, "Coroutine"]]) -> Callable[..., None]:
527    pass
528
529
530def gen_test(  # noqa: F811
531    func: Optional[Callable[..., Union[Generator, "Coroutine"]]] = None,
532    timeout: Optional[float] = None,
533) -> Union[
534    Callable[..., None],
535    Callable[[Callable[..., Union[Generator, "Coroutine"]]], Callable[..., None]],
536]:
537    """Testing equivalent of ``@gen.coroutine``, to be applied to test methods.
538
539    ``@gen.coroutine`` cannot be used on tests because the `.IOLoop` is not
540    already running.  ``@gen_test`` should be applied to test methods
541    on subclasses of `AsyncTestCase`.
542
543    Example::
544
545        class MyTest(AsyncHTTPTestCase):
546            @gen_test
547            def test_something(self):
548                response = yield self.http_client.fetch(self.get_url('/'))
549
550    By default, ``@gen_test`` times out after 5 seconds. The timeout may be
551    overridden globally with the ``ASYNC_TEST_TIMEOUT`` environment variable,
552    or for each test with the ``timeout`` keyword argument::
553
554        class MyTest(AsyncHTTPTestCase):
555            @gen_test(timeout=10)
556            def test_something_slow(self):
557                response = yield self.http_client.fetch(self.get_url('/'))
558
559    Note that ``@gen_test`` is incompatible with `AsyncTestCase.stop`,
560    `AsyncTestCase.wait`, and `AsyncHTTPTestCase.fetch`. Use ``yield
561    self.http_client.fetch(self.get_url())`` as shown above instead.
562
563    .. versionadded:: 3.1
564       The ``timeout`` argument and ``ASYNC_TEST_TIMEOUT`` environment
565       variable.
566
567    .. versionchanged:: 4.0
568       The wrapper now passes along ``*args, **kwargs`` so it can be used
569       on functions with arguments.
570
571    """
572    if timeout is None:
573        timeout = get_async_test_timeout()
574
575    def wrap(f: Callable[..., Union[Generator, "Coroutine"]]) -> Callable[..., None]:
576        # Stack up several decorators to allow us to access the generator
577        # object itself.  In the innermost wrapper, we capture the generator
578        # and save it in an attribute of self.  Next, we run the wrapped
579        # function through @gen.coroutine.  Finally, the coroutine is
580        # wrapped again to make it synchronous with run_sync.
581        #
582        # This is a good case study arguing for either some sort of
583        # extensibility in the gen decorators or cancellation support.
584        @functools.wraps(f)
585        def pre_coroutine(self, *args, **kwargs):
586            # type: (AsyncTestCase, *Any, **Any) -> Union[Generator, Coroutine]
587            # Type comments used to avoid pypy3 bug.
588            result = f(self, *args, **kwargs)
589            if isinstance(result, Generator) or inspect.iscoroutine(result):
590                self._test_generator = result
591            else:
592                self._test_generator = None
593            return result
594
595        if inspect.iscoroutinefunction(f):
596            coro = pre_coroutine
597        else:
598            coro = gen.coroutine(pre_coroutine)  # type: ignore[assignment]
599
600        @functools.wraps(coro)
601        def post_coroutine(self, *args, **kwargs):
602            # type: (AsyncTestCase, *Any, **Any) -> None
603            try:
604                return self.io_loop.run_sync(
605                    functools.partial(coro, self, *args, **kwargs), timeout=timeout
606                )
607            except TimeoutError as e:
608                # run_sync raises an error with an unhelpful traceback.
609                # If the underlying generator is still running, we can throw the
610                # exception back into it so the stack trace is replaced by the
611                # point where the test is stopped. The only reason the generator
612                # would not be running would be if it were cancelled, which means
613                # a native coroutine, so we can rely on the cr_running attribute.
614                if self._test_generator is not None and getattr(
615                    self._test_generator, "cr_running", True
616                ):
617                    self._test_generator.throw(e)
618                    # In case the test contains an overly broad except
619                    # clause, we may get back here.
620                # Coroutine was stopped or didn't raise a useful stack trace,
621                # so re-raise the original exception which is better than nothing.
622                raise
623
624        return post_coroutine
625
626    if func is not None:
627        # Used like:
628        #     @gen_test
629        #     def f(self):
630        #         pass
631        return wrap(func)
632    else:
633        # Used like @gen_test(timeout=10)
634        return wrap
635
636
637# Without this attribute, nosetests will try to run gen_test as a test
638# anywhere it is imported.
639gen_test.__test__ = False  # type: ignore
640
641
642class ExpectLog(logging.Filter):
643    """Context manager to capture and suppress expected log output.
644
645    Useful to make tests of error conditions less noisy, while still
646    leaving unexpected log entries visible.  *Not thread safe.*
647
648    The attribute ``logged_stack`` is set to ``True`` if any exception
649    stack trace was logged.
650
651    Usage::
652
653        with ExpectLog('tornado.application', "Uncaught exception"):
654            error_response = self.fetch("/some_page")
655
656    .. versionchanged:: 4.3
657       Added the ``logged_stack`` attribute.
658    """
659
660    def __init__(
661        self,
662        logger: Union[logging.Logger, basestring_type],
663        regex: str,
664        required: bool = True,
665        level: Optional[int] = None,
666    ) -> None:
667        """Constructs an ExpectLog context manager.
668
669        :param logger: Logger object (or name of logger) to watch.  Pass an
670            empty string to watch the root logger.
671        :param regex: Regular expression to match.  Any log entries on the
672            specified logger that match this regex will be suppressed.
673        :param required: If true, an exception will be raised if the end of the
674            ``with`` statement is reached without matching any log entries.
675        :param level: A constant from the ``logging`` module indicating the
676            expected log level. If this parameter is provided, only log messages
677            at this level will be considered to match. Additionally, the
678            supplied ``logger`` will have its level adjusted if necessary (for
679            the duration of the ``ExpectLog`` to enable the expected message.
680
681        .. versionchanged:: 6.1
682           Added the ``level`` parameter.
683
684        .. deprecated:: 6.3
685           In Tornado 7.0, only ``WARNING`` and higher logging levels will be
686           matched by default. To match ``INFO`` and lower levels, the ``level``
687           argument must be used. This is changing to minimize differences
688           between ``tornado.testing.main`` (which enables ``INFO`` logs by
689           default) and most other test runners (including those in IDEs)
690           which have ``INFO`` logs disabled by default.
691        """
692        if isinstance(logger, basestring_type):
693            logger = logging.getLogger(logger)
694        self.logger = logger
695        self.regex = re.compile(regex)
696        self.required = required
697        # matched and deprecated_level_matched are a counter for the respective event.
698        self.matched = 0
699        self.deprecated_level_matched = 0
700        self.logged_stack = False
701        self.level = level
702        self.orig_level = None  # type: Optional[int]
703
704    def filter(self, record: logging.LogRecord) -> bool:
705        if record.exc_info:
706            self.logged_stack = True
707        message = record.getMessage()
708        if self.regex.match(message):
709            if self.level is None and record.levelno < logging.WARNING:
710                # We're inside the logging machinery here so generating a DeprecationWarning
711                # here won't be reported cleanly (if warnings-as-errors is enabled, the error
712                # just gets swallowed by the logging module), and even if it were it would
713                # have the wrong stack trace. Just remember this fact and report it in
714                # __exit__ instead.
715                self.deprecated_level_matched += 1
716            if self.level is not None and record.levelno != self.level:
717                app_log.warning(
718                    "Got expected log message %r at unexpected level (%s vs %s)"
719                    % (message, logging.getLevelName(self.level), record.levelname)
720                )
721                return True
722            self.matched += 1
723            return False
724        return True
725
726    def __enter__(self) -> "ExpectLog":
727        if self.level is not None and self.level < self.logger.getEffectiveLevel():
728            self.orig_level = self.logger.level
729            self.logger.setLevel(self.level)
730        self.logger.addFilter(self)
731        return self
732
733    def __exit__(
734        self,
735        typ: "Optional[Type[BaseException]]",
736        value: Optional[BaseException],
737        tb: Optional[TracebackType],
738    ) -> None:
739        if self.orig_level is not None:
740            self.logger.setLevel(self.orig_level)
741        self.logger.removeFilter(self)
742        if not typ and self.required and not self.matched:
743            raise Exception("did not get expected log message")
744        if (
745            not typ
746            and self.required
747            and (self.deprecated_level_matched >= self.matched)
748        ):
749            warnings.warn(
750                "ExpectLog matched at INFO or below without level argument",
751                DeprecationWarning,
752            )
753
754
755# From https://nedbatchelder.com/blog/201508/using_context_managers_in_test_setup.html
756def setup_with_context_manager(testcase: unittest.TestCase, cm: Any) -> Any:
757    """Use a context manager to setUp a test case.
758
759    Example::
760
761        def setUp(self):
762            setup_with_context_manager(self, warnings.catch_warnings())
763            warnings.filterwarnings("ignore", category=DeprecationWarning)
764            # The catch_warnings context manager will be deactivated
765            # automatically in tearDown.
766
767    """
768    val = cm.__enter__()
769    testcase.addCleanup(cm.__exit__, None, None, None)
770    return val
771
772
773def main(**kwargs: Any) -> None:
774    """A simple test runner.
775
776    This test runner is essentially equivalent to `unittest.main` from
777    the standard library, but adds support for Tornado-style option
778    parsing and log formatting. It is *not* necessary to use this
779    `main` function to run tests using `AsyncTestCase`; these tests
780    are self-contained and can run with any test runner.
781
782    The easiest way to run a test is via the command line::
783
784        python -m tornado.testing tornado.test.web_test
785
786    See the standard library ``unittest`` module for ways in which
787    tests can be specified.
788
789    Projects with many tests may wish to define a test script like
790    ``tornado/test/runtests.py``.  This script should define a method
791    ``all()`` which returns a test suite and then call
792    `tornado.testing.main()`.  Note that even when a test script is
793    used, the ``all()`` test suite may be overridden by naming a
794    single test on the command line::
795
796        # Runs all tests
797        python -m tornado.test.runtests
798        # Runs one test
799        python -m tornado.test.runtests tornado.test.web_test
800
801    Additional keyword arguments passed through to ``unittest.main()``.
802    For example, use ``tornado.testing.main(verbosity=2)``
803    to show many test details as they are run.
804    See http://docs.python.org/library/unittest.html#unittest.main
805    for full argument list.
806
807    .. versionchanged:: 5.0
808
809       This function produces no output of its own; only that produced
810       by the `unittest` module (previously it would add a PASS or FAIL
811       log message).
812    """
813    from tornado.options import define, options, parse_command_line
814
815    define(
816        "exception_on_interrupt",
817        type=bool,
818        default=True,
819        help=(
820            "If true (default), ctrl-c raises a KeyboardInterrupt "
821            "exception.  This prints a stack trace but cannot interrupt "
822            "certain operations.  If false, the process is more reliably "
823            "killed, but does not print a stack trace."
824        ),
825    )
826
827    # support the same options as unittest's command-line interface
828    define("verbose", type=bool)
829    define("quiet", type=bool)
830    define("failfast", type=bool)
831    define("catch", type=bool)
832    define("buffer", type=bool)
833
834    argv = [sys.argv[0]] + parse_command_line(sys.argv)
835
836    if not options.exception_on_interrupt:
837        signal.signal(signal.SIGINT, signal.SIG_DFL)
838
839    if options.verbose is not None:
840        kwargs["verbosity"] = 2
841    if options.quiet is not None:
842        kwargs["verbosity"] = 0
843    if options.failfast is not None:
844        kwargs["failfast"] = True
845    if options.catch is not None:
846        kwargs["catchbreak"] = True
847    if options.buffer is not None:
848        kwargs["buffer"] = True
849
850    if __name__ == "__main__" and len(argv) == 1:
851        print("No tests specified", file=sys.stderr)
852        sys.exit(1)
853    # In order to be able to run tests by their fully-qualified name
854    # on the command line without importing all tests here,
855    # module must be set to None.  Python 3.2's unittest.main ignores
856    # defaultTest if no module is given (it tries to do its own
857    # test discovery, which is incompatible with auto2to3), so don't
858    # set module if we're not asking for a specific test.
859    if len(argv) > 1:
860        unittest.main(module=None, argv=argv, **kwargs)  # type: ignore
861    else:
862        unittest.main(defaultTest="all", argv=argv, **kwargs)
863
864
865if __name__ == "__main__":
866    main()
867 
codekingpro/portable-devtools · Team Ai