xref: /aosp_15_r20/prebuilts/build-tools/common/py3-stdlib/unittest/async_case.py (revision cda5da8d549138a6648c5ee6d7a49cf8f4a657be)
1*cda5da8dSAndroid Build Coastguard Workerimport asyncio
2*cda5da8dSAndroid Build Coastguard Workerimport contextvars
3*cda5da8dSAndroid Build Coastguard Workerimport inspect
4*cda5da8dSAndroid Build Coastguard Workerimport warnings
5*cda5da8dSAndroid Build Coastguard Worker
6*cda5da8dSAndroid Build Coastguard Workerfrom .case import TestCase
7*cda5da8dSAndroid Build Coastguard Worker
8*cda5da8dSAndroid Build Coastguard Worker
9*cda5da8dSAndroid Build Coastguard Workerclass IsolatedAsyncioTestCase(TestCase):
10*cda5da8dSAndroid Build Coastguard Worker    # Names intentionally have a long prefix
11*cda5da8dSAndroid Build Coastguard Worker    # to reduce a chance of clashing with user-defined attributes
12*cda5da8dSAndroid Build Coastguard Worker    # from inherited test case
13*cda5da8dSAndroid Build Coastguard Worker    #
14*cda5da8dSAndroid Build Coastguard Worker    # The class doesn't call loop.run_until_complete(self.setUp()) and family
15*cda5da8dSAndroid Build Coastguard Worker    # but uses a different approach:
16*cda5da8dSAndroid Build Coastguard Worker    # 1. create a long-running task that reads self.setUp()
17*cda5da8dSAndroid Build Coastguard Worker    #    awaitable from queue along with a future
18*cda5da8dSAndroid Build Coastguard Worker    # 2. await the awaitable object passing in and set the result
19*cda5da8dSAndroid Build Coastguard Worker    #    into the future object
20*cda5da8dSAndroid Build Coastguard Worker    # 3. Outer code puts the awaitable and the future object into a queue
21*cda5da8dSAndroid Build Coastguard Worker    #    with waiting for the future
22*cda5da8dSAndroid Build Coastguard Worker    # The trick is necessary because every run_until_complete() call
23*cda5da8dSAndroid Build Coastguard Worker    # creates a new task with embedded ContextVar context.
24*cda5da8dSAndroid Build Coastguard Worker    # To share contextvars between setUp(), test and tearDown() we need to execute
25*cda5da8dSAndroid Build Coastguard Worker    # them inside the same task.
26*cda5da8dSAndroid Build Coastguard Worker
27*cda5da8dSAndroid Build Coastguard Worker    # Note: the test case modifies event loop policy if the policy was not instantiated
28*cda5da8dSAndroid Build Coastguard Worker    # yet.
29*cda5da8dSAndroid Build Coastguard Worker    # asyncio.get_event_loop_policy() creates a default policy on demand but never
30*cda5da8dSAndroid Build Coastguard Worker    # returns None
31*cda5da8dSAndroid Build Coastguard Worker    # I believe this is not an issue in user level tests but python itself for testing
32*cda5da8dSAndroid Build Coastguard Worker    # should reset a policy in every test module
33*cda5da8dSAndroid Build Coastguard Worker    # by calling asyncio.set_event_loop_policy(None) in tearDownModule()
34*cda5da8dSAndroid Build Coastguard Worker
35*cda5da8dSAndroid Build Coastguard Worker    def __init__(self, methodName='runTest'):
36*cda5da8dSAndroid Build Coastguard Worker        super().__init__(methodName)
37*cda5da8dSAndroid Build Coastguard Worker        self._asyncioRunner = None
38*cda5da8dSAndroid Build Coastguard Worker        self._asyncioTestContext = contextvars.copy_context()
39*cda5da8dSAndroid Build Coastguard Worker
40*cda5da8dSAndroid Build Coastguard Worker    async def asyncSetUp(self):
41*cda5da8dSAndroid Build Coastguard Worker        pass
42*cda5da8dSAndroid Build Coastguard Worker
43*cda5da8dSAndroid Build Coastguard Worker    async def asyncTearDown(self):
44*cda5da8dSAndroid Build Coastguard Worker        pass
45*cda5da8dSAndroid Build Coastguard Worker
46*cda5da8dSAndroid Build Coastguard Worker    def addAsyncCleanup(self, func, /, *args, **kwargs):
47*cda5da8dSAndroid Build Coastguard Worker        # A trivial trampoline to addCleanup()
48*cda5da8dSAndroid Build Coastguard Worker        # the function exists because it has a different semantics
49*cda5da8dSAndroid Build Coastguard Worker        # and signature:
50*cda5da8dSAndroid Build Coastguard Worker        # addCleanup() accepts regular functions
51*cda5da8dSAndroid Build Coastguard Worker        # but addAsyncCleanup() accepts coroutines
52*cda5da8dSAndroid Build Coastguard Worker        #
53*cda5da8dSAndroid Build Coastguard Worker        # We intentionally don't add inspect.iscoroutinefunction() check
54*cda5da8dSAndroid Build Coastguard Worker        # for func argument because there is no way
55*cda5da8dSAndroid Build Coastguard Worker        # to check for async function reliably:
56*cda5da8dSAndroid Build Coastguard Worker        # 1. It can be "async def func()" itself
57*cda5da8dSAndroid Build Coastguard Worker        # 2. Class can implement "async def __call__()" method
58*cda5da8dSAndroid Build Coastguard Worker        # 3. Regular "def func()" that returns awaitable object
59*cda5da8dSAndroid Build Coastguard Worker        self.addCleanup(*(func, *args), **kwargs)
60*cda5da8dSAndroid Build Coastguard Worker
61*cda5da8dSAndroid Build Coastguard Worker    async def enterAsyncContext(self, cm):
62*cda5da8dSAndroid Build Coastguard Worker        """Enters the supplied asynchronous context manager.
63*cda5da8dSAndroid Build Coastguard Worker
64*cda5da8dSAndroid Build Coastguard Worker        If successful, also adds its __aexit__ method as a cleanup
65*cda5da8dSAndroid Build Coastguard Worker        function and returns the result of the __aenter__ method.
66*cda5da8dSAndroid Build Coastguard Worker        """
67*cda5da8dSAndroid Build Coastguard Worker        # We look up the special methods on the type to match the with
68*cda5da8dSAndroid Build Coastguard Worker        # statement.
69*cda5da8dSAndroid Build Coastguard Worker        cls = type(cm)
70*cda5da8dSAndroid Build Coastguard Worker        try:
71*cda5da8dSAndroid Build Coastguard Worker            enter = cls.__aenter__
72*cda5da8dSAndroid Build Coastguard Worker            exit = cls.__aexit__
73*cda5da8dSAndroid Build Coastguard Worker        except AttributeError:
74*cda5da8dSAndroid Build Coastguard Worker            raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does "
75*cda5da8dSAndroid Build Coastguard Worker                            f"not support the asynchronous context manager protocol"
76*cda5da8dSAndroid Build Coastguard Worker                           ) from None
77*cda5da8dSAndroid Build Coastguard Worker        result = await enter(cm)
78*cda5da8dSAndroid Build Coastguard Worker        self.addAsyncCleanup(exit, cm, None, None, None)
79*cda5da8dSAndroid Build Coastguard Worker        return result
80*cda5da8dSAndroid Build Coastguard Worker
81*cda5da8dSAndroid Build Coastguard Worker    def _callSetUp(self):
82*cda5da8dSAndroid Build Coastguard Worker        # Force loop to be initialized and set as the current loop
83*cda5da8dSAndroid Build Coastguard Worker        # so that setUp functions can use get_event_loop() and get the
84*cda5da8dSAndroid Build Coastguard Worker        # correct loop instance.
85*cda5da8dSAndroid Build Coastguard Worker        self._asyncioRunner.get_loop()
86*cda5da8dSAndroid Build Coastguard Worker        self._asyncioTestContext.run(self.setUp)
87*cda5da8dSAndroid Build Coastguard Worker        self._callAsync(self.asyncSetUp)
88*cda5da8dSAndroid Build Coastguard Worker
89*cda5da8dSAndroid Build Coastguard Worker    def _callTestMethod(self, method):
90*cda5da8dSAndroid Build Coastguard Worker        if self._callMaybeAsync(method) is not None:
91*cda5da8dSAndroid Build Coastguard Worker            warnings.warn(f'It is deprecated to return a value that is not None from a '
92*cda5da8dSAndroid Build Coastguard Worker                          f'test case ({method})', DeprecationWarning, stacklevel=4)
93*cda5da8dSAndroid Build Coastguard Worker
94*cda5da8dSAndroid Build Coastguard Worker    def _callTearDown(self):
95*cda5da8dSAndroid Build Coastguard Worker        self._callAsync(self.asyncTearDown)
96*cda5da8dSAndroid Build Coastguard Worker        self._asyncioTestContext.run(self.tearDown)
97*cda5da8dSAndroid Build Coastguard Worker
98*cda5da8dSAndroid Build Coastguard Worker    def _callCleanup(self, function, *args, **kwargs):
99*cda5da8dSAndroid Build Coastguard Worker        self._callMaybeAsync(function, *args, **kwargs)
100*cda5da8dSAndroid Build Coastguard Worker
101*cda5da8dSAndroid Build Coastguard Worker    def _callAsync(self, func, /, *args, **kwargs):
102*cda5da8dSAndroid Build Coastguard Worker        assert self._asyncioRunner is not None, 'asyncio runner is not initialized'
103*cda5da8dSAndroid Build Coastguard Worker        assert inspect.iscoroutinefunction(func), f'{func!r} is not an async function'
104*cda5da8dSAndroid Build Coastguard Worker        return self._asyncioRunner.run(
105*cda5da8dSAndroid Build Coastguard Worker            func(*args, **kwargs),
106*cda5da8dSAndroid Build Coastguard Worker            context=self._asyncioTestContext
107*cda5da8dSAndroid Build Coastguard Worker        )
108*cda5da8dSAndroid Build Coastguard Worker
109*cda5da8dSAndroid Build Coastguard Worker    def _callMaybeAsync(self, func, /, *args, **kwargs):
110*cda5da8dSAndroid Build Coastguard Worker        assert self._asyncioRunner is not None, 'asyncio runner is not initialized'
111*cda5da8dSAndroid Build Coastguard Worker        if inspect.iscoroutinefunction(func):
112*cda5da8dSAndroid Build Coastguard Worker            return self._asyncioRunner.run(
113*cda5da8dSAndroid Build Coastguard Worker                func(*args, **kwargs),
114*cda5da8dSAndroid Build Coastguard Worker                context=self._asyncioTestContext,
115*cda5da8dSAndroid Build Coastguard Worker            )
116*cda5da8dSAndroid Build Coastguard Worker        else:
117*cda5da8dSAndroid Build Coastguard Worker            return self._asyncioTestContext.run(func, *args, **kwargs)
118*cda5da8dSAndroid Build Coastguard Worker
119*cda5da8dSAndroid Build Coastguard Worker    def _setupAsyncioRunner(self):
120*cda5da8dSAndroid Build Coastguard Worker        assert self._asyncioRunner is None, 'asyncio runner is already initialized'
121*cda5da8dSAndroid Build Coastguard Worker        runner = asyncio.Runner(debug=True)
122*cda5da8dSAndroid Build Coastguard Worker        self._asyncioRunner = runner
123*cda5da8dSAndroid Build Coastguard Worker
124*cda5da8dSAndroid Build Coastguard Worker    def _tearDownAsyncioRunner(self):
125*cda5da8dSAndroid Build Coastguard Worker        runner = self._asyncioRunner
126*cda5da8dSAndroid Build Coastguard Worker        runner.close()
127*cda5da8dSAndroid Build Coastguard Worker
128*cda5da8dSAndroid Build Coastguard Worker    def run(self, result=None):
129*cda5da8dSAndroid Build Coastguard Worker        self._setupAsyncioRunner()
130*cda5da8dSAndroid Build Coastguard Worker        try:
131*cda5da8dSAndroid Build Coastguard Worker            return super().run(result)
132*cda5da8dSAndroid Build Coastguard Worker        finally:
133*cda5da8dSAndroid Build Coastguard Worker            self._tearDownAsyncioRunner()
134*cda5da8dSAndroid Build Coastguard Worker
135*cda5da8dSAndroid Build Coastguard Worker    def debug(self):
136*cda5da8dSAndroid Build Coastguard Worker        self._setupAsyncioRunner()
137*cda5da8dSAndroid Build Coastguard Worker        super().debug()
138*cda5da8dSAndroid Build Coastguard Worker        self._tearDownAsyncioRunner()
139*cda5da8dSAndroid Build Coastguard Worker
140*cda5da8dSAndroid Build Coastguard Worker    def __del__(self):
141*cda5da8dSAndroid Build Coastguard Worker        if self._asyncioRunner is not None:
142*cda5da8dSAndroid Build Coastguard Worker            self._tearDownAsyncioRunner()
143