* FIx-up type hints in tests.logging. * Add missing type hints to test_transactions.tags/v1.77.0rc1
@@ -0,0 +1 @@ | |||||
Add missing type hints. |
@@ -40,10 +40,7 @@ exclude = (?x) | |||||
|tests/http/federation/test_matrix_federation_agent.py | |tests/http/federation/test_matrix_federation_agent.py | ||||
|tests/http/federation/test_srv_resolver.py | |tests/http/federation/test_srv_resolver.py | ||||
|tests/http/test_proxyagent.py | |tests/http/test_proxyagent.py | ||||
|tests/logging/__init__.py | |||||
|tests/logging/test_terse_json.py | |||||
|tests/module_api/test_api.py | |tests/module_api/test_api.py | ||||
|tests/rest/client/test_transactions.py | |||||
|tests/rest/media/v1/test_media_storage.py | |tests/rest/media/v1/test_media_storage.py | ||||
|tests/server.py | |tests/server.py | ||||
|tests/test_state.py | |tests/test_state.py | ||||
@@ -92,6 +89,9 @@ disallow_untyped_defs = True | |||||
[mypy-tests.handlers.*] | [mypy-tests.handlers.*] | ||||
disallow_untyped_defs = True | disallow_untyped_defs = True | ||||
[mypy-tests.logging.*] | |||||
disallow_untyped_defs = True | |||||
[mypy-tests.metrics.*] | [mypy-tests.metrics.*] | ||||
disallow_untyped_defs = True | disallow_untyped_defs = True | ||||
@@ -19,6 +19,7 @@ from typing import TYPE_CHECKING, Awaitable, Callable, Dict, Tuple | |||||
from typing_extensions import ParamSpec | from typing_extensions import ParamSpec | ||||
from twisted.internet.defer import Deferred | |||||
from twisted.python.failure import Failure | from twisted.python.failure import Failure | ||||
from twisted.web.server import Request | from twisted.web.server import Request | ||||
@@ -90,7 +91,7 @@ class HttpTransactionCache: | |||||
fn: Callable[P, Awaitable[Tuple[int, JsonDict]]], | fn: Callable[P, Awaitable[Tuple[int, JsonDict]]], | ||||
*args: P.args, | *args: P.args, | ||||
**kwargs: P.kwargs, | **kwargs: P.kwargs, | ||||
) -> Awaitable[Tuple[int, JsonDict]]: | |||||
) -> "Deferred[Tuple[int, JsonDict]]": | |||||
"""Fetches the response for this transaction, or executes the given function | """Fetches the response for this transaction, or executes the given function | ||||
to produce a response for this transaction. | to produce a response for this transaction. | ||||
@@ -13,9 +13,11 @@ | |||||
# limitations under the License. | # limitations under the License. | ||||
import logging | import logging | ||||
from tests.unittest import TestCase | |||||
class LoggerCleanupMixin: | |||||
def get_logger(self, handler): | |||||
class LoggerCleanupMixin(TestCase): | |||||
def get_logger(self, handler: logging.Handler) -> logging.Logger: | |||||
""" | """ | ||||
Attach a handler to a logger and add clean-ups to remove revert this. | Attach a handler to a logger and add clean-ups to remove revert this. | ||||
""" | """ | ||||
@@ -153,7 +153,7 @@ class LogContextScopeManagerTestCase(TestCase): | |||||
scopes = [] | scopes = [] | ||||
async def task(i: int): | |||||
async def task(i: int) -> None: | |||||
scope = start_active_span( | scope = start_active_span( | ||||
f"task{i}", | f"task{i}", | ||||
tracer=self._tracer, | tracer=self._tracer, | ||||
@@ -165,7 +165,7 @@ class LogContextScopeManagerTestCase(TestCase): | |||||
self.assertEqual(self._tracer.active_span, scope.span) | self.assertEqual(self._tracer.active_span, scope.span) | ||||
scope.close() | scope.close() | ||||
async def root(): | |||||
async def root() -> None: | |||||
with start_active_span("root span", tracer=self._tracer) as root_scope: | with start_active_span("root span", tracer=self._tracer) as root_scope: | ||||
self.assertEqual(self._tracer.active_span, root_scope.span) | self.assertEqual(self._tracer.active_span, root_scope.span) | ||||
scopes.append(root_scope) | scopes.append(root_scope) | ||||
@@ -11,7 +11,10 @@ | |||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
# See the License for the specific language governing permissions and | # See the License for the specific language governing permissions and | ||||
# limitations under the License. | # limitations under the License. | ||||
from twisted.test.proto_helpers import AccumulatingProtocol | |||||
from typing import Tuple | |||||
from twisted.internet.protocol import Protocol | |||||
from twisted.test.proto_helpers import AccumulatingProtocol, MemoryReactorClock | |||||
from synapse.logging import RemoteHandler | from synapse.logging import RemoteHandler | ||||
@@ -20,7 +23,9 @@ from tests.server import FakeTransport, get_clock | |||||
from tests.unittest import TestCase | from tests.unittest import TestCase | ||||
def connect_logging_client(reactor, client_id): | |||||
def connect_logging_client( | |||||
reactor: MemoryReactorClock, client_id: int | |||||
) -> Tuple[Protocol, AccumulatingProtocol]: | |||||
# This is essentially tests.server.connect_client, but disabling autoflush on | # This is essentially tests.server.connect_client, but disabling autoflush on | ||||
# the client transport. This is necessary to avoid an infinite loop due to | # the client transport. This is necessary to avoid an infinite loop due to | ||||
# sending of data via the logging transport causing additional logs to be | # sending of data via the logging transport causing additional logs to be | ||||
@@ -35,10 +40,10 @@ def connect_logging_client(reactor, client_id): | |||||
class RemoteHandlerTestCase(LoggerCleanupMixin, TestCase): | class RemoteHandlerTestCase(LoggerCleanupMixin, TestCase): | ||||
def setUp(self): | |||||
def setUp(self) -> None: | |||||
self.reactor, _ = get_clock() | self.reactor, _ = get_clock() | ||||
def test_log_output(self): | |||||
def test_log_output(self) -> None: | |||||
""" | """ | ||||
The remote handler delivers logs over TCP. | The remote handler delivers logs over TCP. | ||||
""" | """ | ||||
@@ -51,6 +56,7 @@ class RemoteHandlerTestCase(LoggerCleanupMixin, TestCase): | |||||
client, server = connect_logging_client(self.reactor, 0) | client, server = connect_logging_client(self.reactor, 0) | ||||
# Trigger data being sent | # Trigger data being sent | ||||
assert isinstance(client.transport, FakeTransport) | |||||
client.transport.flush() | client.transport.flush() | ||||
# One log message, with a single trailing newline | # One log message, with a single trailing newline | ||||
@@ -61,7 +67,7 @@ class RemoteHandlerTestCase(LoggerCleanupMixin, TestCase): | |||||
# Ensure the data passed through properly. | # Ensure the data passed through properly. | ||||
self.assertEqual(logs[0], "Hello there, wally!") | self.assertEqual(logs[0], "Hello there, wally!") | ||||
def test_log_backpressure_debug(self): | |||||
def test_log_backpressure_debug(self) -> None: | |||||
""" | """ | ||||
When backpressure is hit, DEBUG logs will be shed. | When backpressure is hit, DEBUG logs will be shed. | ||||
""" | """ | ||||
@@ -83,6 +89,7 @@ class RemoteHandlerTestCase(LoggerCleanupMixin, TestCase): | |||||
# Allow the reconnection | # Allow the reconnection | ||||
client, server = connect_logging_client(self.reactor, 0) | client, server = connect_logging_client(self.reactor, 0) | ||||
assert isinstance(client.transport, FakeTransport) | |||||
client.transport.flush() | client.transport.flush() | ||||
# Only the 7 infos made it through, the debugs were elided | # Only the 7 infos made it through, the debugs were elided | ||||
@@ -90,7 +97,7 @@ class RemoteHandlerTestCase(LoggerCleanupMixin, TestCase): | |||||
self.assertEqual(len(logs), 7) | self.assertEqual(len(logs), 7) | ||||
self.assertNotIn(b"debug", server.data) | self.assertNotIn(b"debug", server.data) | ||||
def test_log_backpressure_info(self): | |||||
def test_log_backpressure_info(self) -> None: | |||||
""" | """ | ||||
When backpressure is hit, DEBUG and INFO logs will be shed. | When backpressure is hit, DEBUG and INFO logs will be shed. | ||||
""" | """ | ||||
@@ -116,6 +123,7 @@ class RemoteHandlerTestCase(LoggerCleanupMixin, TestCase): | |||||
# Allow the reconnection | # Allow the reconnection | ||||
client, server = connect_logging_client(self.reactor, 0) | client, server = connect_logging_client(self.reactor, 0) | ||||
assert isinstance(client.transport, FakeTransport) | |||||
client.transport.flush() | client.transport.flush() | ||||
# The 10 warnings made it through, the debugs and infos were elided | # The 10 warnings made it through, the debugs and infos were elided | ||||
@@ -124,7 +132,7 @@ class RemoteHandlerTestCase(LoggerCleanupMixin, TestCase): | |||||
self.assertNotIn(b"debug", server.data) | self.assertNotIn(b"debug", server.data) | ||||
self.assertNotIn(b"info", server.data) | self.assertNotIn(b"info", server.data) | ||||
def test_log_backpressure_cut_middle(self): | |||||
def test_log_backpressure_cut_middle(self) -> None: | |||||
""" | """ | ||||
When backpressure is hit, and no more DEBUG and INFOs cannot be culled, | When backpressure is hit, and no more DEBUG and INFOs cannot be culled, | ||||
it will cut the middle messages out. | it will cut the middle messages out. | ||||
@@ -140,6 +148,7 @@ class RemoteHandlerTestCase(LoggerCleanupMixin, TestCase): | |||||
# Allow the reconnection | # Allow the reconnection | ||||
client, server = connect_logging_client(self.reactor, 0) | client, server = connect_logging_client(self.reactor, 0) | ||||
assert isinstance(client.transport, FakeTransport) | |||||
client.transport.flush() | client.transport.flush() | ||||
# The first five and last five warnings made it through, the debugs and | # The first five and last five warnings made it through, the debugs and | ||||
@@ -151,7 +160,7 @@ class RemoteHandlerTestCase(LoggerCleanupMixin, TestCase): | |||||
logs, | logs, | ||||
) | ) | ||||
def test_cancel_connection(self): | |||||
def test_cancel_connection(self) -> None: | |||||
""" | """ | ||||
Gracefully handle the connection being cancelled. | Gracefully handle the connection being cancelled. | ||||
""" | """ | ||||
@@ -14,24 +14,28 @@ | |||||
import json | import json | ||||
import logging | import logging | ||||
from io import BytesIO, StringIO | from io import BytesIO, StringIO | ||||
from typing import cast | |||||
from unittest.mock import Mock, patch | from unittest.mock import Mock, patch | ||||
from twisted.web.http import HTTPChannel | |||||
from twisted.web.server import Request | from twisted.web.server import Request | ||||
from synapse.http.site import SynapseRequest | from synapse.http.site import SynapseRequest | ||||
from synapse.logging._terse_json import JsonFormatter, TerseJsonFormatter | from synapse.logging._terse_json import JsonFormatter, TerseJsonFormatter | ||||
from synapse.logging.context import LoggingContext, LoggingContextFilter | from synapse.logging.context import LoggingContext, LoggingContextFilter | ||||
from synapse.types import JsonDict | |||||
from tests.logging import LoggerCleanupMixin | from tests.logging import LoggerCleanupMixin | ||||
from tests.server import FakeChannel | |||||
from tests.server import FakeChannel, get_clock | |||||
from tests.unittest import TestCase | from tests.unittest import TestCase | ||||
class TerseJsonTestCase(LoggerCleanupMixin, TestCase): | class TerseJsonTestCase(LoggerCleanupMixin, TestCase): | ||||
def setUp(self): | |||||
def setUp(self) -> None: | |||||
self.output = StringIO() | self.output = StringIO() | ||||
self.reactor, _ = get_clock() | |||||
def get_log_line(self): | |||||
def get_log_line(self) -> JsonDict: | |||||
# One log message, with a single trailing newline. | # One log message, with a single trailing newline. | ||||
data = self.output.getvalue() | data = self.output.getvalue() | ||||
logs = data.splitlines() | logs = data.splitlines() | ||||
@@ -39,7 +43,7 @@ class TerseJsonTestCase(LoggerCleanupMixin, TestCase): | |||||
self.assertEqual(data.count("\n"), 1) | self.assertEqual(data.count("\n"), 1) | ||||
return json.loads(logs[0]) | return json.loads(logs[0]) | ||||
def test_terse_json_output(self): | |||||
def test_terse_json_output(self) -> None: | |||||
""" | """ | ||||
The Terse JSON formatter converts log messages to JSON. | The Terse JSON formatter converts log messages to JSON. | ||||
""" | """ | ||||
@@ -61,7 +65,7 @@ class TerseJsonTestCase(LoggerCleanupMixin, TestCase): | |||||
self.assertCountEqual(log.keys(), expected_log_keys) | self.assertCountEqual(log.keys(), expected_log_keys) | ||||
self.assertEqual(log["log"], "Hello there, wally!") | self.assertEqual(log["log"], "Hello there, wally!") | ||||
def test_extra_data(self): | |||||
def test_extra_data(self) -> None: | |||||
""" | """ | ||||
Additional information can be included in the structured logging. | Additional information can be included in the structured logging. | ||||
""" | """ | ||||
@@ -93,7 +97,7 @@ class TerseJsonTestCase(LoggerCleanupMixin, TestCase): | |||||
self.assertEqual(log["int"], 3) | self.assertEqual(log["int"], 3) | ||||
self.assertIs(log["bool"], True) | self.assertIs(log["bool"], True) | ||||
def test_json_output(self): | |||||
def test_json_output(self) -> None: | |||||
""" | """ | ||||
The Terse JSON formatter converts log messages to JSON. | The Terse JSON formatter converts log messages to JSON. | ||||
""" | """ | ||||
@@ -114,7 +118,7 @@ class TerseJsonTestCase(LoggerCleanupMixin, TestCase): | |||||
self.assertCountEqual(log.keys(), expected_log_keys) | self.assertCountEqual(log.keys(), expected_log_keys) | ||||
self.assertEqual(log["log"], "Hello there, wally!") | self.assertEqual(log["log"], "Hello there, wally!") | ||||
def test_with_context(self): | |||||
def test_with_context(self) -> None: | |||||
""" | """ | ||||
The logging context should be added to the JSON response. | The logging context should be added to the JSON response. | ||||
""" | """ | ||||
@@ -139,7 +143,7 @@ class TerseJsonTestCase(LoggerCleanupMixin, TestCase): | |||||
self.assertEqual(log["log"], "Hello there, wally!") | self.assertEqual(log["log"], "Hello there, wally!") | ||||
self.assertEqual(log["request"], "name") | self.assertEqual(log["request"], "name") | ||||
def test_with_request_context(self): | |||||
def test_with_request_context(self) -> None: | |||||
""" | """ | ||||
Information from the logging context request should be added to the JSON response. | Information from the logging context request should be added to the JSON response. | ||||
""" | """ | ||||
@@ -154,11 +158,13 @@ class TerseJsonTestCase(LoggerCleanupMixin, TestCase): | |||||
site.server_version_string = "Server v1" | site.server_version_string = "Server v1" | ||||
site.reactor = Mock() | site.reactor = Mock() | ||||
site.experimental_cors_msc3886 = False | site.experimental_cors_msc3886 = False | ||||
request = SynapseRequest(FakeChannel(site, None), site) | |||||
request = SynapseRequest( | |||||
cast(HTTPChannel, FakeChannel(site, self.reactor)), site | |||||
) | |||||
# Call requestReceived to finish instantiating the object. | # Call requestReceived to finish instantiating the object. | ||||
request.content = BytesIO() | request.content = BytesIO() | ||||
# Partially skip some of the internal processing of SynapseRequest. | |||||
request._started_processing = Mock() | |||||
# Partially skip some internal processing of SynapseRequest. | |||||
request._started_processing = Mock() # type: ignore[assignment] | |||||
request.request_metrics = Mock(spec=["name"]) | request.request_metrics = Mock(spec=["name"]) | ||||
with patch.object(Request, "render"): | with patch.object(Request, "render"): | ||||
request.requestReceived(b"POST", b"/_matrix/client/versions", b"1.1") | request.requestReceived(b"POST", b"/_matrix/client/versions", b"1.1") | ||||
@@ -200,7 +206,7 @@ class TerseJsonTestCase(LoggerCleanupMixin, TestCase): | |||||
self.assertEqual(log["protocol"], "1.1") | self.assertEqual(log["protocol"], "1.1") | ||||
self.assertEqual(log["user_agent"], "") | self.assertEqual(log["user_agent"], "") | ||||
def test_with_exception(self): | |||||
def test_with_exception(self) -> None: | |||||
""" | """ | ||||
The logging exception type & value should be added to the JSON response. | The logging exception type & value should be added to the JSON response. | ||||
""" | """ | ||||
@@ -13,18 +13,22 @@ | |||||
# limitations under the License. | # limitations under the License. | ||||
from http import HTTPStatus | from http import HTTPStatus | ||||
from typing import Any, Generator, Tuple, cast | |||||
from unittest.mock import Mock, call | from unittest.mock import Mock, call | ||||
from twisted.internet import defer, reactor | |||||
from twisted.internet import defer, reactor as _reactor | |||||
from synapse.logging.context import SENTINEL_CONTEXT, LoggingContext, current_context | from synapse.logging.context import SENTINEL_CONTEXT, LoggingContext, current_context | ||||
from synapse.rest.client.transactions import CLEANUP_PERIOD_MS, HttpTransactionCache | from synapse.rest.client.transactions import CLEANUP_PERIOD_MS, HttpTransactionCache | ||||
from synapse.types import ISynapseReactor, JsonDict | |||||
from synapse.util import Clock | from synapse.util import Clock | ||||
from tests import unittest | from tests import unittest | ||||
from tests.test_utils import make_awaitable | from tests.test_utils import make_awaitable | ||||
from tests.utils import MockClock | from tests.utils import MockClock | ||||
reactor = cast(ISynapseReactor, _reactor) | |||||
class HttpTransactionCacheTestCase(unittest.TestCase): | class HttpTransactionCacheTestCase(unittest.TestCase): | ||||
def setUp(self) -> None: | def setUp(self) -> None: | ||||
@@ -34,11 +38,13 @@ class HttpTransactionCacheTestCase(unittest.TestCase): | |||||
self.hs.get_auth = Mock() | self.hs.get_auth = Mock() | ||||
self.cache = HttpTransactionCache(self.hs) | self.cache = HttpTransactionCache(self.hs) | ||||
self.mock_http_response = (HTTPStatus.OK, "GOOD JOB!") | |||||
self.mock_http_response = (HTTPStatus.OK, {"result": "GOOD JOB!"}) | |||||
self.mock_key = "foo" | self.mock_key = "foo" | ||||
@defer.inlineCallbacks | @defer.inlineCallbacks | ||||
def test_executes_given_function(self): | |||||
def test_executes_given_function( | |||||
self, | |||||
) -> Generator["defer.Deferred[Any]", object, None]: | |||||
cb = Mock(return_value=make_awaitable(self.mock_http_response)) | cb = Mock(return_value=make_awaitable(self.mock_http_response)) | ||||
res = yield self.cache.fetch_or_execute( | res = yield self.cache.fetch_or_execute( | ||||
self.mock_key, cb, "some_arg", keyword="arg" | self.mock_key, cb, "some_arg", keyword="arg" | ||||
@@ -47,7 +53,9 @@ class HttpTransactionCacheTestCase(unittest.TestCase): | |||||
self.assertEqual(res, self.mock_http_response) | self.assertEqual(res, self.mock_http_response) | ||||
@defer.inlineCallbacks | @defer.inlineCallbacks | ||||
def test_deduplicates_based_on_key(self): | |||||
def test_deduplicates_based_on_key( | |||||
self, | |||||
) -> Generator["defer.Deferred[Any]", object, None]: | |||||
cb = Mock(return_value=make_awaitable(self.mock_http_response)) | cb = Mock(return_value=make_awaitable(self.mock_http_response)) | ||||
for i in range(3): # invoke multiple times | for i in range(3): # invoke multiple times | ||||
res = yield self.cache.fetch_or_execute( | res = yield self.cache.fetch_or_execute( | ||||
@@ -58,18 +66,20 @@ class HttpTransactionCacheTestCase(unittest.TestCase): | |||||
cb.assert_called_once_with("some_arg", keyword="arg", changing_args=0) | cb.assert_called_once_with("some_arg", keyword="arg", changing_args=0) | ||||
@defer.inlineCallbacks | @defer.inlineCallbacks | ||||
def test_logcontexts_with_async_result(self): | |||||
def test_logcontexts_with_async_result( | |||||
self, | |||||
) -> Generator["defer.Deferred[Any]", object, None]: | |||||
@defer.inlineCallbacks | @defer.inlineCallbacks | ||||
def cb(): | |||||
def cb() -> Generator["defer.Deferred[object]", object, Tuple[int, JsonDict]]: | |||||
yield Clock(reactor).sleep(0) | yield Clock(reactor).sleep(0) | ||||
return "yay" | |||||
return 1, {} | |||||
@defer.inlineCallbacks | @defer.inlineCallbacks | ||||
def test(): | |||||
def test() -> Generator["defer.Deferred[Any]", object, None]: | |||||
with LoggingContext("c") as c1: | with LoggingContext("c") as c1: | ||||
res = yield self.cache.fetch_or_execute(self.mock_key, cb) | res = yield self.cache.fetch_or_execute(self.mock_key, cb) | ||||
self.assertIs(current_context(), c1) | self.assertIs(current_context(), c1) | ||||
self.assertEqual(res, "yay") | |||||
self.assertEqual(res, (1, {})) | |||||
# run the test twice in parallel | # run the test twice in parallel | ||||
d = defer.gatherResults([test(), test()]) | d = defer.gatherResults([test(), test()]) | ||||
@@ -78,13 +88,15 @@ class HttpTransactionCacheTestCase(unittest.TestCase): | |||||
self.assertIs(current_context(), SENTINEL_CONTEXT) | self.assertIs(current_context(), SENTINEL_CONTEXT) | ||||
@defer.inlineCallbacks | @defer.inlineCallbacks | ||||
def test_does_not_cache_exceptions(self): | |||||
def test_does_not_cache_exceptions( | |||||
self, | |||||
) -> Generator["defer.Deferred[Any]", object, None]: | |||||
"""Checks that, if the callback throws an exception, it is called again | """Checks that, if the callback throws an exception, it is called again | ||||
for the next request. | for the next request. | ||||
""" | """ | ||||
called = [False] | called = [False] | ||||
def cb(): | |||||
def cb() -> "defer.Deferred[Tuple[int, JsonDict]]": | |||||
if called[0]: | if called[0]: | ||||
# return a valid result the second time | # return a valid result the second time | ||||
return defer.succeed(self.mock_http_response) | return defer.succeed(self.mock_http_response) | ||||
@@ -104,13 +116,15 @@ class HttpTransactionCacheTestCase(unittest.TestCase): | |||||
self.assertIs(current_context(), test_context) | self.assertIs(current_context(), test_context) | ||||
@defer.inlineCallbacks | @defer.inlineCallbacks | ||||
def test_does_not_cache_failures(self): | |||||
def test_does_not_cache_failures( | |||||
self, | |||||
) -> Generator["defer.Deferred[Any]", object, None]: | |||||
"""Checks that, if the callback returns a failure, it is called again | """Checks that, if the callback returns a failure, it is called again | ||||
for the next request. | for the next request. | ||||
""" | """ | ||||
called = [False] | called = [False] | ||||
def cb(): | |||||
def cb() -> "defer.Deferred[Tuple[int, JsonDict]]": | |||||
if called[0]: | if called[0]: | ||||
# return a valid result the second time | # return a valid result the second time | ||||
return defer.succeed(self.mock_http_response) | return defer.succeed(self.mock_http_response) | ||||
@@ -130,7 +144,7 @@ class HttpTransactionCacheTestCase(unittest.TestCase): | |||||
self.assertIs(current_context(), test_context) | self.assertIs(current_context(), test_context) | ||||
@defer.inlineCallbacks | @defer.inlineCallbacks | ||||
def test_cleans_up(self): | |||||
def test_cleans_up(self) -> Generator["defer.Deferred[Any]", object, None]: | |||||
cb = Mock(return_value=make_awaitable(self.mock_http_response)) | cb = Mock(return_value=make_awaitable(self.mock_http_response)) | ||||
yield self.cache.fetch_or_execute(self.mock_key, cb, "an arg") | yield self.cache.fetch_or_execute(self.mock_key, cb, "an arg") | ||||
# should NOT have cleaned up yet | # should NOT have cleaned up yet | ||||