From 74e9ced14d8d19b5e16315f3096c837c8051b2fb Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 3 Oct 2026 16:00:24 +0200 Subject: [PATCH 1/9] Run all asyncio tests under uvloop. --- tests/asyncio/test_client.py | 25 +++++++++++++++++++++++++ tests/asyncio/test_connection.py | 9 +++++++++ tests/asyncio/test_messages.py | 9 +++++++++ tests/asyncio/test_router.py | 9 +++++++++ tests/asyncio/test_server.py | 25 +++++++++++++++++++++++++ tests/asyncio/utils.py | 16 ++++++++++++++++ tests/requirements.txt | 1 + tox.ini | 1 + 8 files changed, 95 insertions(+) create mode 100644 tests/asyncio/utils.py diff --git a/tests/asyncio/test_client.py b/tests/asyncio/test_client.py index 409d53647..03e5fc6ac 100644 --- a/tests/asyncio/test_client.py +++ b/tests/asyncio/test_client.py @@ -28,6 +28,7 @@ from ..proxy import ProxyMixin from ..utils import CLIENT_CONTEXT, MS, SERVER_CONTEXT, temp_unix_socket_path from .server import args, get_host_port, get_uri, handler +from .utils import UVLoopTestCase def short_backoff(): @@ -1129,3 +1130,27 @@ async def test_reentrancy(self): str(raised.exception), "connect() isn't reentrant", ) + + +class UVLoopClientTests(ClientTests, UVLoopTestCase): + pass + + +class UVLoopSecureClientTests(SecureClientTests, UVLoopTestCase): + pass + + +class UVLoopSocksProxyClientTests(SocksProxyClientTests, UVLoopTestCase): + pass + + +class UVLoopHTTPProxyClientTests(HTTPProxyClientTests, UVLoopTestCase): + pass + + +class UVLoopUnixClientTests(UnixClientTests, UVLoopTestCase): + pass + + +class UVLoopClientUsageErrorsTests(ClientUsageErrorsTests, UVLoopTestCase): + pass diff --git a/tests/asyncio/test_connection.py b/tests/asyncio/test_connection.py index 5655f6f0c..de5e877fd 100644 --- a/tests/asyncio/test_connection.py +++ b/tests/asyncio/test_connection.py @@ -22,6 +22,7 @@ from ..protocol import RecordingProtocol from ..utils import MS, LoggingTestCase, alist from .connection import InterceptingConnection +from .utils import UVLoopTestCase # Connection implements symmetrical behavior between clients and servers. @@ -1512,3 +1513,11 @@ async def test_broadcast_type_error(self): class ServerConnectionTests(ClientConnectionTests): LOCAL = SERVER REMOTE = CLIENT + + +class UVLoopClientConnectionTests(ClientConnectionTests, UVLoopTestCase): + pass + + +class UVLoopServerConnectionTests(ServerConnectionTests, UVLoopTestCase): + pass diff --git a/tests/asyncio/test_messages.py b/tests/asyncio/test_messages.py index ca0099f3b..605860664 100644 --- a/tests/asyncio/test_messages.py +++ b/tests/asyncio/test_messages.py @@ -9,6 +9,7 @@ from websockets.frames import BINARY, CONT, TEXT, Frame from ..utils import alist +from .utils import UVLoopTestCase class SimpleQueueTests(unittest.IsolatedAsyncioTestCase): @@ -561,3 +562,11 @@ async def test_set_invalid_low_water_mark(self): """low must be higher than high.""" with self.assertRaises(ValueError): Assembler(low=10, high=5) + + +class UVLoopSimpleQueueTests(SimpleQueueTests, UVLoopTestCase): + pass + + +class UVLoopAssemblerTests(AssemblerTests, UVLoopTestCase): + pass diff --git a/tests/asyncio/test_router.py b/tests/asyncio/test_router.py index c9651e9cc..5c0ea05f2 100644 --- a/tests/asyncio/test_router.py +++ b/tests/asyncio/test_router.py @@ -10,6 +10,7 @@ from ..utils import CLIENT_CONTEXT, SERVER_CONTEXT, alist, temp_unix_socket_path from .server import EvalShellMixin, get_uri, handler +from .utils import UVLoopTestCase try: @@ -192,3 +193,11 @@ async def test_router_supports_unix_sockets(self): await client.send("hello") messages = await alist(client) self.assertEqual(messages, ["hello", "hello", "hello"]) + + +class UVLoopRouterTests(RouterTests, UVLoopTestCase): + pass + + +class UVLoopUnixRouterTests(UnixRouterTests, UVLoopTestCase): + pass diff --git a/tests/asyncio/test_server.py b/tests/asyncio/test_server.py index 6487cabde..b6b05ad16 100644 --- a/tests/asyncio/test_server.py +++ b/tests/asyncio/test_server.py @@ -32,6 +32,7 @@ get_uri, handler, ) +from .utils import UVLoopTestCase class ServerTests(EvalShellMixin, LoggingTestCase, unittest.IsolatedAsyncioTestCase): @@ -991,3 +992,27 @@ async def test_bad_list_of_credentials(self): str(raised.exception), "invalid credentials argument: [42]", ) + + +class UVLoopServerTests(ServerTests, UVLoopTestCase): + pass + + +class UVLoopSecureServerTests(SecureServerTests, UVLoopTestCase): + pass + + +class UVLoopUnixServerTests(UnixServerTests, UVLoopTestCase): + pass + + +class UVLoopSecureUnixServerTests(SecureUnixServerTests, UVLoopTestCase): + pass + + +class UVLoopServerUsageErrorsTests(ServerUsageErrorsTests, UVLoopTestCase): + pass + + +class UVLoopBasicAuthTests(BasicAuthTests, UVLoopTestCase): + pass diff --git a/tests/asyncio/utils.py b/tests/asyncio/utils.py new file mode 100644 index 000000000..4aac92bf1 --- /dev/null +++ b/tests/asyncio/utils.py @@ -0,0 +1,16 @@ +import sys +import time +import unittest + + +try: + import uvloop +except ImportError: + pass + + +@unittest.skipUnless("uvloop" in sys.modules, "uvloop not installed") +class UVLoopTestCase(unittest.IsolatedAsyncioTestCase): + @staticmethod + def loop_factory(): + return uvloop.Loop() diff --git a/tests/requirements.txt b/tests/requirements.txt index 77de5350b..57b4854b8 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,3 +1,4 @@ mitmproxy python-socks[asyncio] trio +uvloop diff --git a/tox.ini b/tox.ini index c6204eee7..d86cf707c 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,7 @@ deps = py311,py312,py313,py314,py315,coverage,maxi_cov: mitmproxy py311,py312,py313,py314,py315,coverage,maxi_cov: python-socks[asyncio] trio + uvloop werkzeug [testenv:coverage] From 172ba7e26fe97919893ecee847d75d7e44788b51 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 3 Oct 2026 16:12:47 +0200 Subject: [PATCH 2/9] Simplify magic detection of secure connections in tests. This version doesn't depend on internals, and is compatible with uvloop. --- tests/asyncio/server.py | 4 +++- tests/sync/server.py | 5 +++-- tests/trio/server.py | 8 +++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/asyncio/server.py b/tests/asyncio/server.py index b3ffdfc8c..b260fc03a 100644 --- a/tests/asyncio/server.py +++ b/tests/asyncio/server.py @@ -1,4 +1,5 @@ import asyncio +import inspect import socket import urllib.parse @@ -12,7 +13,8 @@ def get_host_port(server): def get_uri(server, secure=None): if secure is None: - secure = server.server._ssl_context is not None # hack + closure_vars = inspect.getclosurevars(server.create_server) + secure = closure_vars.nonlocals["kwargs"].get("ssl") is not None protocol = "wss" if secure else "ws" host, port = get_host_port(server) return f"{protocol}://{host}:{port}" diff --git a/tests/sync/server.py b/tests/sync/server.py index 194829fea..eb43f2619 100644 --- a/tests/sync/server.py +++ b/tests/sync/server.py @@ -1,5 +1,5 @@ import contextlib -import ssl +import inspect import threading import time import urllib.parse @@ -14,7 +14,8 @@ def get_host_port(server): def get_uri(server, secure=None): if secure is None: - secure = isinstance(server.socket, ssl.SSLSocket) # hack + closure_vars = inspect.getclosurevars(server.handler) + secure = closure_vars.nonlocals["ssl"] is not None protocol = "wss" if secure else "ws" host, port = get_host_port(server) return f"{protocol}://{host}:{port}" diff --git a/tests/trio/server.py b/tests/trio/server.py index 7b69e491b..23345962e 100644 --- a/tests/trio/server.py +++ b/tests/trio/server.py @@ -1,7 +1,7 @@ import contextlib import functools +import inspect import socket -import ssl import urllib.parse import trio @@ -19,10 +19,8 @@ def get_host_port(server): def get_uri(server, secure=None): if secure is None: - secure = any( - isinstance(cell.cell_contents, ssl.SSLContext) - for cell in server.handler.__closure__ - ) # l33t hack + closure_vars = inspect.getclosurevars(server.handler) + secure = closure_vars.nonlocals["ssl"] is not None protocol = "wss" if secure else "ws" host, port = get_host_port(server) return f"{protocol}://{host}:{port}" From 262fabcbc281dbfad9edcf7f50ee30c1b7f5d832 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 3 Oct 2026 19:06:28 +0200 Subject: [PATCH 3/9] Remove argument not accepted by uvloop. --- tests/asyncio/test_client.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/asyncio/test_client.py b/tests/asyncio/test_client.py index 03e5fc6ac..633c87153 100644 --- a/tests/asyncio/test_client.py +++ b/tests/asyncio/test_client.py @@ -894,12 +894,17 @@ async def test_secure_https_proxy(self): async def test_https_proxy_server_hostname(self): """Client sets server_hostname to the value of proxy_server_hostname.""" async with serve(*args) as server: + # Pass an argument not prefixed with ssl or proxy_ for coverage, + # except on uvloop, which doesn't support any such argument. + if isinstance(asyncio.get_event_loop(), asyncio.BaseEventLoop): + kwargs = {"happy_eyeballs_delay": 0.1} + else: # pragma: no cover + kwargs = {} async with connect( get_uri(server), proxy_ssl=self.proxy_context, proxy_server_hostname="overridden", - # Pass an argument not prefixed with proxy_ for coverage. - happy_eyeballs_delay=0.1, + **kwargs, ) as client: ssl_object = client.transport.get_extra_info("ssl_object") self.assertEqual(ssl_object.server_hostname, "overridden") From 6a1a13e7be420869e842bc5cad7e3f0dea53a5c1 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 3 Oct 2026 19:10:07 +0200 Subject: [PATCH 4/9] Work around limitations of the uvloop clock. See https://github.com/MagicStack/uvloop/issues/359 --- tests/asyncio/test_client.py | 4 +++- tests/asyncio/test_connection.py | 3 ++- tests/asyncio/test_server.py | 4 +++- tests/asyncio/utils.py | 24 ++++++++++++++++++++++-- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/tests/asyncio/test_client.py b/tests/asyncio/test_client.py index 633c87153..a8c85846d 100644 --- a/tests/asyncio/test_client.py +++ b/tests/asyncio/test_client.py @@ -28,7 +28,7 @@ from ..proxy import ProxyMixin from ..utils import CLIENT_CONTEXT, MS, SERVER_CONTEXT, temp_unix_socket_path from .server import args, get_host_port, get_uri, handler -from .utils import UVLoopTestCase +from .utils import UVLoopTestCase, requires_accurate_clock def short_backoff(): @@ -141,6 +141,7 @@ async def test_legacy_user_agent_header(self): ) as client: self.assertEqual(client.request.headers["User-Agent"], "Smith") + @requires_accurate_clock async def test_keepalive_is_enabled(self): """Client enables keepalive and measures latency by default.""" async with serve(*args) as server: @@ -149,6 +150,7 @@ async def test_keepalive_is_enabled(self): await asyncio.sleep(2 * MS) self.assertGreater(client.latency, 0) + @requires_accurate_clock async def test_disable_keepalive(self): """Client disables keepalive.""" async with serve(*args) as server: diff --git a/tests/asyncio/test_connection.py b/tests/asyncio/test_connection.py index de5e877fd..fc3588ea9 100644 --- a/tests/asyncio/test_connection.py +++ b/tests/asyncio/test_connection.py @@ -22,7 +22,7 @@ from ..protocol import RecordingProtocol from ..utils import MS, LoggingTestCase, alist from .connection import InterceptingConnection -from .utils import UVLoopTestCase +from .utils import UVLoopTestCase, requires_accurate_clock # Connection implements symmetrical behavior between clients and servers. @@ -1058,6 +1058,7 @@ async def test_pong_unsupported_type(self): # Test keepalive. + @requires_accurate_clock @patch("random.getrandbits") async def test_keepalive(self, getrandbits): """keepalive sends pings at ping_interval and measures latency.""" diff --git a/tests/asyncio/test_server.py b/tests/asyncio/test_server.py index b6b05ad16..2febb7487 100644 --- a/tests/asyncio/test_server.py +++ b/tests/asyncio/test_server.py @@ -32,7 +32,7 @@ get_uri, handler, ) -from .utils import UVLoopTestCase +from .utils import UVLoopTestCase, requires_accurate_clock class ServerTests(EvalShellMixin, LoggingTestCase, unittest.IsolatedAsyncioTestCase): @@ -349,6 +349,7 @@ async def test_remove_server_header(self): client, "'Server' in ws.response.headers", "False" ) + @requires_accurate_clock async def test_keepalive_is_enabled(self): """Server enables keepalive and measures latency.""" async with serve(*args, ping_interval=MS) as server: @@ -361,6 +362,7 @@ async def test_keepalive_is_enabled(self): latency = eval(await client.recv()) self.assertGreater(latency, 0) + @requires_accurate_clock async def test_disable_keepalive(self): """Server disables keepalive.""" async with serve(*args, ping_interval=None) as server: diff --git a/tests/asyncio/utils.py b/tests/asyncio/utils.py index 4aac92bf1..d68494f10 100644 --- a/tests/asyncio/utils.py +++ b/tests/asyncio/utils.py @@ -7,10 +7,30 @@ import uvloop except ImportError: pass +else: + + class AccurateClockLoop(uvloop.Loop): + """ + Workaround for https://github.com/MagicStack/uvloop/issues/359. + + uvloop.Loop.time() has 1ms resolution, making latency measurements + on localhost come out as 0 in tests. Use a real clock instead. + + """ + + def time(self): + return time.perf_counter() + + +def requires_accurate_clock(test): + setattr(test, "requires_accurate_clock", True) + return test @unittest.skipUnless("uvloop" in sys.modules, "uvloop not installed") class UVLoopTestCase(unittest.IsolatedAsyncioTestCase): - @staticmethod - def loop_factory(): + def loop_factory(self): + test_method = getattr(type(self), self._testMethodName) + if getattr(test_method, "requires_accurate_clock", False): + return AccurateClockLoop() return uvloop.Loop() From 095000f364d204f1d9bf8ba4dabf0d5ada246782 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 3 Oct 2026 21:09:53 +0200 Subject: [PATCH 5/9] Patch in a way that's also compatible with uvloop. --- tests/asyncio/test_connection.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/asyncio/test_connection.py b/tests/asyncio/test_connection.py index fc3588ea9..db9f3a7df 100644 --- a/tests/asyncio/test_connection.py +++ b/tests/asyncio/test_connection.py @@ -1228,19 +1228,19 @@ async def test_logger(self): """Connection has a logger attribute.""" self.assertIsInstance(self.connection.logger, logging.LoggerAdapter) - @patch("asyncio.Transport.get_extra_info") - async def test_local_address(self, get_extra_info): + async def test_local_address(self): """Connection has a local_address attribute.""" - get_extra_info.return_value = ("sock", 1234) - self.assertEqual(self.connection.local_address, ("sock", 1234)) - get_extra_info.assert_called_with("sockname") + with patch.object(self.connection, "transport") as transport: + transport.get_extra_info.return_value = ("sock", 1234) + self.assertEqual(self.connection.local_address, ("sock", 1234)) + transport.get_extra_info.assert_called_with("sockname") - @patch("asyncio.Transport.get_extra_info") - async def test_remote_address(self, get_extra_info): + async def test_remote_address(self): """Connection has a remote_address attribute.""" - get_extra_info.return_value = ("peer", 1234) - self.assertEqual(self.connection.remote_address, ("peer", 1234)) - get_extra_info.assert_called_with("peername") + with patch.object(self.connection, "transport") as transport: + transport.get_extra_info.return_value = ("peer", 1234) + self.assertEqual(self.connection.remote_address, ("peer", 1234)) + transport.get_extra_info.assert_called_with("peername") async def test_state(self): """Connection has a state attribute.""" From b8efd07ee263c627a0be5fd1def1d33a5c963136 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 3 Oct 2026 21:10:27 +0200 Subject: [PATCH 6/9] Make assertion compatible with uvloop. asyncio returns a tuple, uvloop returns a list. --- tests/asyncio/test_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/asyncio/test_server.py b/tests/asyncio/test_server.py index 2febb7487..87e66f64e 100644 --- a/tests/asyncio/test_server.py +++ b/tests/asyncio/test_server.py @@ -690,7 +690,7 @@ async def test_sockets(self): async with serve(*args) as server: sock = server.sockets[0] self.assertIsInstance(sock.fileno(), int) - self.assertEqual(server.sockets, ()) + self.assertEqual(list(server.sockets), []) async def test_context_manager_closes_server(self): """Server closes when exiting the asynchronous context manager.""" From 124b4b77cf05de3c2dd4c00401b4410c20c41d2b Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 4 Oct 2026 13:54:58 +0200 Subject: [PATCH 7/9] Tweak test timings for compatibility with uvloop. Wait at least 2ms for some sequences of events. --- tests/asyncio/test_connection.py | 12 ++++++------ tests/sync/test_connection.py | 12 ++++++------ tests/trio/test_connection.py | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/asyncio/test_connection.py b/tests/asyncio/test_connection.py index db9f3a7df..6869a994e 100644 --- a/tests/asyncio/test_connection.py +++ b/tests/asyncio/test_connection.py @@ -1069,8 +1069,8 @@ async def test_keepalive(self, getrandbits): self.assertEqual(self.connection.latency, 0) # 3 ms: keepalive() sends a ping frame. # 3.x ms: a pong frame is received. - await asyncio.sleep(4 * MS) - # 4 ms: check that the ping frame was sent. + await asyncio.sleep(5 * MS) + # 5 ms: check that the ping frame was sent. await self.assertFrameSent(Frame(PING, b"rand")) self.assertGreater(self.connection.latency, 0) self.assertLess(self.connection.latency, MS) @@ -1091,9 +1091,9 @@ async def test_keepalive_times_out(self, getrandbits): self.connection.start_keepalive() # 4 ms: keepalive() sends a ping frame. # 4.x ms: a pong frame is dropped. - await asyncio.sleep(5 * MS) + await asyncio.sleep(6 * MS) # 6 ms: no pong frame is received; the connection is closed. - await asyncio.sleep(3 * MS) + await asyncio.sleep(2 * MS) # 8 ms: check that the connection is closed. self.assertEqual(self.connection.state, CLOSED) @@ -1107,9 +1107,9 @@ async def test_keepalive_ignores_timeout(self, getrandbits): self.connection.start_keepalive() # 4 ms: keepalive() sends a ping frame. # 4.x ms: a pong frame is dropped. - await asyncio.sleep(5 * MS) + await asyncio.sleep(6 * MS) # 6 ms: no pong frame is received; the connection remains open. - await asyncio.sleep(3 * MS) + await asyncio.sleep(2 * MS) # 8 ms: check that the connection is still open. self.assertEqual(self.connection.state, OPEN) diff --git a/tests/sync/test_connection.py b/tests/sync/test_connection.py index 55b44b3eb..8e13c1821 100644 --- a/tests/sync/test_connection.py +++ b/tests/sync/test_connection.py @@ -801,8 +801,8 @@ def test_keepalive(self, getrandbits): self.assertEqual(self.connection.latency, 0) # 3 ms: keepalive() sends a ping frame. # 3.x ms: a pong frame is received. - time.sleep(4 * MS) - # 4 ms: check that the ping frame was sent. + time.sleep(5 * MS) + # 5 ms: check that the ping frame was sent. self.assertFrameSent(Frame(PING, b"rand")) self.assertGreater(self.connection.latency, 0) self.assertLess(self.connection.latency, MS) @@ -823,10 +823,10 @@ def test_keepalive_times_out(self, getrandbits): self.connection.start_keepalive() # 4 ms: keepalive() sends a ping frame. # 4.x ms: a pong frame is dropped. - time.sleep(4 * MS) + time.sleep(5 * MS) # Exiting the context manager sleeps for 1 ms. # 6 ms: no pong frame is received; the connection is closed. - time.sleep(3 * MS) + time.sleep(2 * MS) # 8 ms: check that the connection is closed. self.assertEqual(self.connection.state, CLOSED) @@ -840,10 +840,10 @@ def test_keepalive_ignores_timeout(self, getrandbits): self.connection.start_keepalive() # 4 ms: keepalive() sends a ping frame. # 4.x ms: a pong frame is dropped. - time.sleep(4 * MS) + time.sleep(5 * MS) # Exiting the context manager sleeps for 1 ms. # 6 ms: no pong frame is received; the connection remains open. - time.sleep(3 * MS) + time.sleep(2 * MS) # 8 ms: check that the connection is still open. self.assertEqual(self.connection.state, OPEN) diff --git a/tests/trio/test_connection.py b/tests/trio/test_connection.py index 5b2c5f99e..f52619e10 100644 --- a/tests/trio/test_connection.py +++ b/tests/trio/test_connection.py @@ -996,8 +996,8 @@ async def test_keepalive(self, getrandbits): self.assertEqual(self.connection.latency, 0) # 3 ms: keepalive() sends a ping frame. # 3.x ms: a pong frame is received. - await trio.sleep(4 * MS) - # 4 ms: check that the ping frame was sent. + await trio.sleep(5 * MS) + # 5 ms: check that the ping frame was sent. await self.assertFrameSent(Frame(PING, b"rand")) self.assertGreater(self.connection.latency, 0) self.assertLess(self.connection.latency, MS) @@ -1018,9 +1018,9 @@ async def test_keepalive_times_out(self, getrandbits): self.connection.start_keepalive() # 4 ms: keepalive() sends a ping frame. # 4.x ms: a pong frame is dropped. - await trio.sleep(5 * MS) + await trio.sleep(6 * MS) # 6 ms: no pong frame is received; the connection is closed. - await trio.sleep(3 * MS) + await trio.sleep(2 * MS) # 8 ms: check that the connection is closed. self.assertEqual(self.connection.state, CLOSED) @@ -1034,9 +1034,9 @@ async def test_keepalive_ignores_timeout(self, getrandbits): self.connection.start_keepalive() # 4 ms: keepalive() sends a ping frame. # 4.x ms: a pong frame is dropped. - await trio.sleep(5 * MS) + await trio.sleep(6 * MS) # 6 ms: no pong frame is received; the connection remains open. - await trio.sleep(3 * MS) + await trio.sleep(2 * MS) # 8 ms: check that the connection is still open. self.assertEqual(self.connection.state, OPEN) From fd12a350dfc037fa68f864b0444245a87a292cec Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 4 Oct 2026 13:14:33 +0200 Subject: [PATCH 8/9] Factor out assertions checking elapsed time. And make them robust to uvloop's low resolution clock. See https://github.com/MagicStack/uvloop/issues/359 --- tests/asyncio/test_connection.py | 50 ++++++++++++-------------------- tests/sync/test_connection.py | 34 +++++++++------------- tests/trio/test_connection.py | 50 ++++++++++++-------------------- tests/utils.py | 14 +++++++++ 4 files changed, 65 insertions(+), 83 deletions(-) diff --git a/tests/asyncio/test_connection.py b/tests/asyncio/test_connection.py index 6869a994e..ea4cbd201 100644 --- a/tests/asyncio/test_connection.py +++ b/tests/asyncio/test_connection.py @@ -20,7 +20,7 @@ from websockets.protocol import CLIENT, CLOSED, OPEN, SERVER, Protocol from ..protocol import RecordingProtocol -from ..utils import MS, LoggingTestCase, alist +from ..utils import MS, LoggingTestCase, alist, assertDurationAtLeast from .connection import InterceptingConnection from .utils import UVLoopTestCase, requires_accurate_clock @@ -729,14 +729,12 @@ async def test_close_explicit_code_reason(self): async def test_close_waits_for_close_frame(self): """close waits for a close frame then EOF before returning.""" - t0 = self.loop.time() - async with self.delay_frames_rcvd(MS), self.delay_eof_rcvd(MS): - await self.connection.close() - t1 = self.loop.time() + with assertDurationAtLeast(MS, self.loop.time): + async with self.delay_frames_rcvd(MS), self.delay_eof_rcvd(MS): + await self.connection.close() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.NORMAL_CLOSURE) - self.assertGreater(t1 - t0, MS) with self.assertRaises(ConnectionClosedOK) as raised: await self.connection.recv() @@ -750,14 +748,12 @@ async def test_close_waits_for_connection_closed(self): if self.LOCAL is SERVER: self.skipTest("only relevant on the client-side") - t0 = self.loop.time() - async with self.delay_eof_rcvd(MS): - await self.connection.close() - t1 = self.loop.time() + with assertDurationAtLeast(MS, self.loop.time): + async with self.delay_eof_rcvd(MS): + await self.connection.close() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.NORMAL_CLOSURE) - self.assertGreater(t1 - t0, MS) with self.assertRaises(ConnectionClosedOK) as raised: await self.connection.recv() @@ -770,14 +766,12 @@ async def test_close_no_timeout_waits_for_close_frame(self): """close without timeout waits for a close frame then EOF before returning.""" self.connection.close_timeout = None - t0 = self.loop.time() - async with self.delay_frames_rcvd(MS), self.delay_eof_rcvd(MS): - await self.connection.close() - t1 = self.loop.time() + with assertDurationAtLeast(MS, self.loop.time): + async with self.delay_frames_rcvd(MS), self.delay_eof_rcvd(MS): + await self.connection.close() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.NORMAL_CLOSURE) - self.assertGreater(t1 - t0, MS) with self.assertRaises(ConnectionClosedOK) as raised: await self.connection.recv() @@ -793,14 +787,12 @@ async def test_close_no_timeout_waits_for_connection_closed(self): self.connection.close_timeout = None - t0 = self.loop.time() - async with self.delay_eof_rcvd(MS): - await self.connection.close() - t1 = self.loop.time() + with assertDurationAtLeast(MS, self.loop.time): + async with self.delay_eof_rcvd(MS): + await self.connection.close() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.NORMAL_CLOSURE) - self.assertGreater(t1 - t0, MS) with self.assertRaises(ConnectionClosedOK) as raised: await self.connection.recv() @@ -811,14 +803,12 @@ async def test_close_no_timeout_waits_for_connection_closed(self): async def test_close_timeout_waiting_for_close_frame(self): """close times out if no close frame is received.""" - t0 = self.loop.time() - async with self.drop_eof_rcvd(), self.drop_frames_rcvd(): - await self.connection.close() - t1 = self.loop.time() + with assertDurationAtLeast(2 * MS, self.loop.time): + async with self.drop_eof_rcvd(), self.drop_frames_rcvd(): + await self.connection.close() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.ABNORMAL_CLOSURE) - self.assertGreater(t1 - t0, 2 * MS) with self.assertRaises(ConnectionClosedError) as raised: await self.connection.recv() @@ -832,14 +822,12 @@ async def test_close_timeout_waiting_for_connection_closed(self): if self.LOCAL is SERVER: self.skipTest("only relevant on the client-side") - t0 = self.loop.time() - async with self.drop_eof_rcvd(): - await self.connection.close() - t1 = self.loop.time() + with assertDurationAtLeast(2 * MS, self.loop.time): + async with self.drop_eof_rcvd(): + await self.connection.close() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.NORMAL_CLOSURE) - self.assertGreater(t1 - t0, 2 * MS) with self.assertRaises(ConnectionClosedOK) as raised: await self.connection.recv() diff --git a/tests/sync/test_connection.py b/tests/sync/test_connection.py index 8e13c1821..43e813a31 100644 --- a/tests/sync/test_connection.py +++ b/tests/sync/test_connection.py @@ -21,7 +21,7 @@ from websockets.sync.connection import broadcast from ..protocol import RecordingProtocol -from ..utils import MS, LoggingTestCase +from ..utils import MS, LoggingTestCase, assertDurationAtLeast from .connection import InterceptingConnection from .utils import ThreadTestCase @@ -486,14 +486,12 @@ def test_close_explicit_code_reason(self): def test_close_waits_for_close_frame(self): """close waits for a close frame then EOF before returning.""" - t0 = time.time() - with self.delay_frames_rcvd(MS): - self.connection.close() - t1 = time.time() + with assertDurationAtLeast(MS, time.time): + with self.delay_frames_rcvd(MS): + self.connection.close() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.NORMAL_CLOSURE) - self.assertGreater(t1 - t0, MS) with self.assertRaises(ConnectionClosedOK) as raised: self.connection.recv() @@ -507,14 +505,12 @@ def test_close_waits_for_connection_closed(self): if self.LOCAL is SERVER: self.skipTest("only relevant on the client-side") - t0 = time.time() - with self.delay_eof_rcvd(MS): - self.connection.close() - t1 = time.time() + with assertDurationAtLeast(MS, time.time): + with self.delay_eof_rcvd(MS): + self.connection.close() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.NORMAL_CLOSURE) - self.assertGreater(t1 - t0, MS) with self.assertRaises(ConnectionClosedOK) as raised: self.connection.recv() @@ -525,14 +521,12 @@ def test_close_waits_for_connection_closed(self): def test_close_timeout_waiting_for_close_frame(self): """close times out if no close frame is received.""" - t0 = time.time() - with self.drop_frames_rcvd(), self.drop_eof_rcvd(): - self.connection.close() - t1 = time.time() + with assertDurationAtLeast(2 * MS, time.time): + with self.drop_frames_rcvd(), self.drop_eof_rcvd(): + self.connection.close() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.ABNORMAL_CLOSURE) - self.assertGreater(t1 - t0, 2 * MS) with self.assertRaises(ConnectionClosedError) as raised: self.connection.recv() @@ -546,14 +540,12 @@ def test_close_timeout_waiting_for_connection_closed(self): if self.LOCAL is SERVER: self.skipTest("only relevant on the client-side") - t0 = time.time() - with self.drop_eof_rcvd(): - self.connection.close() - t1 = time.time() + with assertDurationAtLeast(2 * MS, time.time): + with self.drop_eof_rcvd(): + self.connection.close() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.NORMAL_CLOSURE) - self.assertGreater(t1 - t0, 2 * MS) with self.assertRaises(ConnectionClosedOK) as raised: self.connection.recv() diff --git a/tests/trio/test_connection.py b/tests/trio/test_connection.py index f52619e10..76e190adf 100644 --- a/tests/trio/test_connection.py +++ b/tests/trio/test_connection.py @@ -20,7 +20,7 @@ from websockets.trio.connection import broadcast from ..protocol import RecordingProtocol -from ..utils import MS, LoggingTestCase, alist +from ..utils import MS, LoggingTestCase, alist, assertDurationAtLeast from .connection import InterceptingConnection from .utils import IsolatedTrioTestCase @@ -660,14 +660,12 @@ async def test_aclose_explicit_code_reason(self): async def test_aclose_waits_for_close_frame(self): """aclose waits for a close frame then EOF before returning.""" - t0 = trio.current_time() - async with self.delay_frames_rcvd(MS): - await self.connection.aclose() - t1 = trio.current_time() + with assertDurationAtLeast(MS, trio.current_time): + async with self.delay_frames_rcvd(MS): + await self.connection.aclose() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.NORMAL_CLOSURE) - self.assertGreater(t1 - t0, MS) with self.assertRaises(ConnectionClosedOK) as raised: await self.connection.recv() @@ -681,14 +679,12 @@ async def test_aclose_waits_for_connection_closed(self): if self.LOCAL is SERVER: self.skipTest("only relevant on the client-side") - t0 = trio.current_time() - async with self.delay_eof_rcvd(MS): - await self.connection.aclose() - t1 = trio.current_time() + with assertDurationAtLeast(MS, trio.current_time): + async with self.delay_eof_rcvd(MS): + await self.connection.aclose() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.NORMAL_CLOSURE) - self.assertGreater(t1 - t0, MS) with self.assertRaises(ConnectionClosedOK) as raised: await self.connection.recv() @@ -701,14 +697,12 @@ async def test_aclose_no_timeout_waits_for_close_frame(self): """aclose without timeout waits for a close frame then EOF before returning.""" self.connection.close_timeout = None - t0 = trio.current_time() - async with self.delay_frames_rcvd(MS): - await self.connection.aclose() - t1 = trio.current_time() + with assertDurationAtLeast(MS, trio.current_time): + async with self.delay_frames_rcvd(MS): + await self.connection.aclose() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.NORMAL_CLOSURE) - self.assertGreater(t1 - t0, MS) with self.assertRaises(ConnectionClosedOK) as raised: await self.connection.recv() @@ -724,14 +718,12 @@ async def test_aclose_no_timeout_waits_for_connection_closed(self): self.connection.close_timeout = None - t0 = trio.current_time() - async with self.delay_eof_rcvd(MS): - await self.connection.aclose() - t1 = trio.current_time() + with assertDurationAtLeast(MS, trio.current_time): + async with self.delay_eof_rcvd(MS): + await self.connection.aclose() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.NORMAL_CLOSURE) - self.assertGreater(t1 - t0, MS) with self.assertRaises(ConnectionClosedOK) as raised: await self.connection.recv() @@ -742,14 +734,12 @@ async def test_aclose_no_timeout_waits_for_connection_closed(self): async def test_close_timeout_waiting_for_close_frame(self): """aclose times out if no close frame is received.""" - t0 = trio.current_time() - async with self.drop_eof_rcvd(), self.drop_frames_rcvd(): - await self.connection.aclose() - t1 = trio.current_time() + with assertDurationAtLeast(2 * MS, trio.current_time): + async with self.drop_eof_rcvd(), self.drop_frames_rcvd(): + await self.connection.aclose() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.ABNORMAL_CLOSURE) - self.assertGreater(t1 - t0, 2 * MS) with self.assertRaises(ConnectionClosedError) as raised: await self.connection.recv() @@ -763,14 +753,12 @@ async def test_close_timeout_waiting_for_connection_closed(self): if self.LOCAL is SERVER: self.skipTest("only relevant on the client-side") - t0 = trio.current_time() - async with self.drop_eof_rcvd(): - await self.connection.aclose() - t1 = trio.current_time() + with assertDurationAtLeast(2 * MS, trio.current_time): + async with self.drop_eof_rcvd(): + await self.connection.aclose() self.assertEqual(self.connection.state, CLOSED) self.assertEqual(self.connection.close_code, CloseCode.NORMAL_CLOSURE) - self.assertGreater(t1 - t0, 2 * MS) with self.assertRaises(ConnectionClosedOK) as raised: await self.connection.recv() diff --git a/tests/utils.py b/tests/utils.py index 80a7b23e6..1dd96b541 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,6 @@ import contextlib import email.utils +import math import os import pathlib import platform @@ -67,6 +68,19 @@ MS = max(MS, 2.5 * time.get_clock_info("monotonic").resolution) +@contextlib.contextmanager +def assertDurationAtLeast(min_delay, clock): + t0 = clock() + yield + t1 = clock() + # uvloop's clock has low resolution, which leads to t1 - t0 being exactly + # equal to min_delay. Due to floating-point arithmetic precision, t1 - t0 + # can be slightly lower than min_delay e.g. 0.01999999999999602 vs. 0.02. + # Add tolerance to account for this effect. + if t1 - t0 + math.ulp(max(abs(t0), abs(t1))) < min_delay: + raise AssertionError(f"took {t1 - t0}s, less than {min_delay}s") + + class GeneratorTestCase(unittest.TestCase): """ Base class for testing generator-based coroutines. From b6303ec4a2af42bc1b5d8204a2474ffa6ff39780 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 4 Oct 2026 13:17:52 +0200 Subject: [PATCH 9/9] Add changelog. --- docs/project/changelog.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/project/changelog.rst b/docs/project/changelog.rst index 134ad29ba..b6ea9f391 100644 --- a/docs/project/changelog.rst +++ b/docs/project/changelog.rst @@ -32,6 +32,13 @@ notice. *In development* +Improvements +............ + +* Tested compatibility with uvloop_ in the :mod:`asyncio` implementation. + +.. _uvloop: https://uvloop.readthedocs.io/ + .. _17.2: 17.2