From ff73432f0a7008661b00e92fbd5e4642aec632f4 Mon Sep 17 00:00:00 2001 From: Andy Staples Date: Thu, 25 Jun 2026 10:52:14 -0600 Subject: [PATCH 1/4] Replace fixed test ports with free port helper --- tests/durabletask/_port_utils.py | 27 +++++++++++++++++++ .../entities/test_class_based_entities_e2e.py | 7 +++-- .../entities/test_entity_failure_handling.py | 7 +++-- .../test_function_based_entities_e2e.py | 7 +++-- .../history_export/test_activities.py | 3 ++- .../extensions/history_export/test_client.py | 4 +-- .../extensions/history_export/test_entity.py | 4 +-- .../history_export/test_orchestrator.py | 4 +-- tests/durabletask/test_batch_actions.py | 4 ++- tests/durabletask/test_large_payload_e2e.py | 6 +++-- .../test_orchestration_async_e2e.py | 7 +++-- tests/durabletask/test_orchestration_e2e.py | 7 +++-- .../test_orchestration_versioning_e2e.py | 7 +++-- .../durabletask/test_work_item_filters_e2e.py | 7 +++-- 14 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 tests/durabletask/_port_utils.py diff --git a/tests/durabletask/_port_utils.py b/tests/durabletask/_port_utils.py new file mode 100644 index 00000000..3c3a7f02 --- /dev/null +++ b/tests/durabletask/_port_utils.py @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Shared test helpers for picking network ports. + +Tests start an in-memory backend that binds a TCP port. Hard-coding +fixed ports causes intermittent failures on Windows because Hyper-V (and +other components) reserve large, dynamic ranges of TCP ports. Asking the +OS for a free port avoids those collisions. +""" + +from __future__ import annotations + +import socket + + +def find_free_port() -> int: + """Return a TCP port number that is currently free. + + Binds a socket to port 0 so the OS assigns an available port, then + releases it and returns the chosen number. There is an inherent race + between releasing the socket and the caller binding the port, but in + practice it is reliable for tests and far safer than fixed ports. + """ + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("localhost", 0)) + return s.getsockname()[1] diff --git a/tests/durabletask/entities/test_class_based_entities_e2e.py b/tests/durabletask/entities/test_class_based_entities_e2e.py index ab5c6074..d34cf148 100644 --- a/tests/durabletask/entities/test_class_based_entities_e2e.py +++ b/tests/durabletask/entities/test_class_based_entities_e2e.py @@ -12,13 +12,16 @@ from durabletask import client, entities, task, worker from durabletask.testing import create_test_backend -HOST = "localhost:50059" +from _port_utils import find_free_port + +PORT = find_free_port() +HOST = f"localhost:{PORT}" @pytest.fixture(autouse=True) def backend(): """Create an in-memory backend for entity testing.""" - b = create_test_backend(port=50059) + b = create_test_backend(port=PORT) yield b b.stop() b.reset() diff --git a/tests/durabletask/entities/test_entity_failure_handling.py b/tests/durabletask/entities/test_entity_failure_handling.py index 92ad2351..2f455c4f 100644 --- a/tests/durabletask/entities/test_entity_failure_handling.py +++ b/tests/durabletask/entities/test_entity_failure_handling.py @@ -12,13 +12,16 @@ from durabletask import client, entities, task, worker from durabletask.testing import create_test_backend -HOST = "localhost:50057" +from _port_utils import find_free_port + +PORT = find_free_port() +HOST = f"localhost:{PORT}" @pytest.fixture(autouse=True) def backend(): """Create an in-memory backend for entity testing.""" - b = create_test_backend(port=50057) + b = create_test_backend(port=PORT) yield b b.stop() b.reset() diff --git a/tests/durabletask/entities/test_function_based_entities_e2e.py b/tests/durabletask/entities/test_function_based_entities_e2e.py index 9b1a301e..6959833e 100644 --- a/tests/durabletask/entities/test_function_based_entities_e2e.py +++ b/tests/durabletask/entities/test_function_based_entities_e2e.py @@ -12,13 +12,16 @@ from durabletask import client, entities, task, worker from durabletask.testing import create_test_backend -HOST = "localhost:50056" +from _port_utils import find_free_port + +PORT = find_free_port() +HOST = f"localhost:{PORT}" @pytest.fixture(autouse=True) def backend(): """Create an in-memory backend for entity testing.""" - b = create_test_backend(port=50056) + b = create_test_backend(port=PORT) yield b b.stop() b.reset() diff --git a/tests/durabletask/extensions/history_export/test_activities.py b/tests/durabletask/extensions/history_export/test_activities.py index f1d196f2..babac799 100644 --- a/tests/durabletask/extensions/history_export/test_activities.py +++ b/tests/durabletask/extensions/history_export/test_activities.py @@ -36,8 +36,9 @@ ) from durabletask.testing import create_test_backend +from _port_utils import find_free_port -PORT = 50261 +PORT = find_free_port() HOST = f"localhost:{PORT}" diff --git a/tests/durabletask/extensions/history_export/test_client.py b/tests/durabletask/extensions/history_export/test_client.py index eca23afa..e56df554 100644 --- a/tests/durabletask/extensions/history_export/test_client.py +++ b/tests/durabletask/extensions/history_export/test_client.py @@ -35,9 +35,9 @@ from durabletask.testing import create_test_backend from ._test_helpers import wait_until +from _port_utils import find_free_port - -PORT = 50263 +PORT = find_free_port() HOST = f"localhost:{PORT}" diff --git a/tests/durabletask/extensions/history_export/test_entity.py b/tests/durabletask/extensions/history_export/test_entity.py index 14c5aa68..e0220c05 100644 --- a/tests/durabletask/extensions/history_export/test_entity.py +++ b/tests/durabletask/extensions/history_export/test_entity.py @@ -31,9 +31,9 @@ from durabletask.testing import create_test_backend from ._test_helpers import wait_until +from _port_utils import find_free_port - -PORT = 50260 +PORT = find_free_port() HOST = f"localhost:{PORT}" _WINDOW_START = datetime(2025, 1, 1, tzinfo=timezone.utc) diff --git a/tests/durabletask/extensions/history_export/test_orchestrator.py b/tests/durabletask/extensions/history_export/test_orchestrator.py index 9c9452a1..7bee52e4 100644 --- a/tests/durabletask/extensions/history_export/test_orchestrator.py +++ b/tests/durabletask/extensions/history_export/test_orchestrator.py @@ -33,9 +33,9 @@ from durabletask.testing import create_test_backend from ._test_helpers import wait_until +from _port_utils import find_free_port - -PORT = 50262 +PORT = find_free_port() HOST = f"localhost:{PORT}" diff --git a/tests/durabletask/test_batch_actions.py b/tests/durabletask/test_batch_actions.py index 60d313a3..dea86d05 100644 --- a/tests/durabletask/test_batch_actions.py +++ b/tests/durabletask/test_batch_actions.py @@ -15,7 +15,9 @@ from durabletask.testing import create_test_backend from durabletask.worker import TaskHubGrpcWorker -BATCH_TEST_PORT = 50058 +from _port_utils import find_free_port + +BATCH_TEST_PORT = find_free_port() HOST = f"localhost:{BATCH_TEST_PORT}" diff --git a/tests/durabletask/test_large_payload_e2e.py b/tests/durabletask/test_large_payload_e2e.py index 832d4f26..918ace9b 100644 --- a/tests/durabletask/test_large_payload_e2e.py +++ b/tests/durabletask/test_large_payload_e2e.py @@ -22,6 +22,8 @@ from durabletask import client, task, worker from durabletask.testing import create_test_backend +from _port_utils import find_free_port + # Skip the entire module if azure-storage-blob is not installed. azure_blob = pytest.importorskip("azure.storage.blob") @@ -30,8 +32,8 @@ # Azurite well-known connection string AZURITE_CONN_STR = "UseDevelopmentStorage=true" -HOST = "localhost:50070" -BACKEND_PORT = 50070 +BACKEND_PORT = find_free_port() +HOST = f"localhost:{BACKEND_PORT}" # Use a unique container per test run to avoid collisions. TEST_CONTAINER = f"e2e-payloads-{uuid.uuid4().hex[:8]}" diff --git a/tests/durabletask/test_orchestration_async_e2e.py b/tests/durabletask/test_orchestration_async_e2e.py index 3a2b24bc..cab73bd3 100644 --- a/tests/durabletask/test_orchestration_async_e2e.py +++ b/tests/durabletask/test_orchestration_async_e2e.py @@ -9,13 +9,16 @@ from durabletask import client, task, worker from durabletask.testing import create_test_backend -HOST = "localhost:50060" +from _port_utils import find_free_port + +PORT = find_free_port() +HOST = f"localhost:{PORT}" @pytest.fixture(autouse=True) def backend(): """Create an in-memory backend for testing.""" - b = create_test_backend(port=50060) + b = create_test_backend(port=PORT) yield b b.stop() b.reset() diff --git a/tests/durabletask/test_orchestration_e2e.py b/tests/durabletask/test_orchestration_e2e.py index a8d47274..927acba5 100644 --- a/tests/durabletask/test_orchestration_e2e.py +++ b/tests/durabletask/test_orchestration_e2e.py @@ -13,13 +13,16 @@ import durabletask.history as history from durabletask.testing import create_test_backend -HOST = "localhost:50054" +from _port_utils import find_free_port + +PORT = find_free_port() +HOST = f"localhost:{PORT}" @pytest.fixture(autouse=True) def backend(): """Create an in-memory backend for testing.""" - b = create_test_backend(port=50054) + b = create_test_backend(port=PORT) yield b b.stop() b.reset() diff --git a/tests/durabletask/test_orchestration_versioning_e2e.py b/tests/durabletask/test_orchestration_versioning_e2e.py index 60575bfd..ca712f65 100644 --- a/tests/durabletask/test_orchestration_versioning_e2e.py +++ b/tests/durabletask/test_orchestration_versioning_e2e.py @@ -8,13 +8,16 @@ from durabletask import client, task, worker from durabletask.testing import create_test_backend -HOST = "localhost:50055" +from _port_utils import find_free_port + +PORT = find_free_port() +HOST = f"localhost:{PORT}" @pytest.fixture(autouse=True) def backend(): """Create an in-memory backend for testing.""" - b = create_test_backend(port=50055) + b = create_test_backend(port=PORT) yield b b.stop() b.reset() diff --git a/tests/durabletask/test_work_item_filters_e2e.py b/tests/durabletask/test_work_item_filters_e2e.py index 97370c72..7b1f7b42 100644 --- a/tests/durabletask/test_work_item_filters_e2e.py +++ b/tests/durabletask/test_work_item_filters_e2e.py @@ -18,13 +18,16 @@ ) from durabletask.testing import create_test_backend -HOST = "localhost:50060" +from _port_utils import find_free_port + +PORT = find_free_port() +HOST = f"localhost:{PORT}" @pytest.fixture(autouse=True) def backend(): """Create an in-memory backend for testing.""" - b = create_test_backend(port=50060) + b = create_test_backend(port=PORT) yield b b.stop() b.reset() From 1afbd615d314b0b161d6c04defd84476f394a35d Mon Sep 17 00:00:00 2001 From: Andy Staples Date: Thu, 25 Jun 2026 11:10:05 -0600 Subject: [PATCH 2/4] Make tests a module, use absolute imports --- tests/durabletask/__init__.py | 2 ++ tests/durabletask/entities/test_class_based_entities_e2e.py | 2 +- tests/durabletask/entities/test_entity_failure_handling.py | 2 +- tests/durabletask/entities/test_function_based_entities_e2e.py | 2 +- tests/durabletask/extensions/history_export/test_activities.py | 2 +- tests/durabletask/extensions/history_export/test_client.py | 2 +- tests/durabletask/extensions/history_export/test_entity.py | 2 +- .../durabletask/extensions/history_export/test_orchestrator.py | 2 +- tests/durabletask/test_batch_actions.py | 2 +- tests/durabletask/test_large_payload_e2e.py | 2 +- tests/durabletask/test_orchestration_async_e2e.py | 2 +- tests/durabletask/test_orchestration_e2e.py | 2 +- tests/durabletask/test_orchestration_versioning_e2e.py | 2 +- tests/durabletask/test_work_item_filters_e2e.py | 2 +- 14 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 tests/durabletask/__init__.py diff --git a/tests/durabletask/__init__.py b/tests/durabletask/__init__.py new file mode 100644 index 00000000..59e481eb --- /dev/null +++ b/tests/durabletask/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. diff --git a/tests/durabletask/entities/test_class_based_entities_e2e.py b/tests/durabletask/entities/test_class_based_entities_e2e.py index d34cf148..79381c39 100644 --- a/tests/durabletask/entities/test_class_based_entities_e2e.py +++ b/tests/durabletask/entities/test_class_based_entities_e2e.py @@ -12,7 +12,7 @@ from durabletask import client, entities, task, worker from durabletask.testing import create_test_backend -from _port_utils import find_free_port +from tests.durabletask._port_utils import find_free_port PORT = find_free_port() HOST = f"localhost:{PORT}" diff --git a/tests/durabletask/entities/test_entity_failure_handling.py b/tests/durabletask/entities/test_entity_failure_handling.py index 2f455c4f..44639645 100644 --- a/tests/durabletask/entities/test_entity_failure_handling.py +++ b/tests/durabletask/entities/test_entity_failure_handling.py @@ -12,7 +12,7 @@ from durabletask import client, entities, task, worker from durabletask.testing import create_test_backend -from _port_utils import find_free_port +from tests.durabletask._port_utils import find_free_port PORT = find_free_port() HOST = f"localhost:{PORT}" diff --git a/tests/durabletask/entities/test_function_based_entities_e2e.py b/tests/durabletask/entities/test_function_based_entities_e2e.py index 6959833e..363620ea 100644 --- a/tests/durabletask/entities/test_function_based_entities_e2e.py +++ b/tests/durabletask/entities/test_function_based_entities_e2e.py @@ -12,7 +12,7 @@ from durabletask import client, entities, task, worker from durabletask.testing import create_test_backend -from _port_utils import find_free_port +from tests.durabletask._port_utils import find_free_port PORT = find_free_port() HOST = f"localhost:{PORT}" diff --git a/tests/durabletask/extensions/history_export/test_activities.py b/tests/durabletask/extensions/history_export/test_activities.py index babac799..13251049 100644 --- a/tests/durabletask/extensions/history_export/test_activities.py +++ b/tests/durabletask/extensions/history_export/test_activities.py @@ -36,7 +36,7 @@ ) from durabletask.testing import create_test_backend -from _port_utils import find_free_port +from tests.durabletask._port_utils import find_free_port PORT = find_free_port() HOST = f"localhost:{PORT}" diff --git a/tests/durabletask/extensions/history_export/test_client.py b/tests/durabletask/extensions/history_export/test_client.py index e56df554..456d8836 100644 --- a/tests/durabletask/extensions/history_export/test_client.py +++ b/tests/durabletask/extensions/history_export/test_client.py @@ -35,7 +35,7 @@ from durabletask.testing import create_test_backend from ._test_helpers import wait_until -from _port_utils import find_free_port +from tests.durabletask._port_utils import find_free_port PORT = find_free_port() HOST = f"localhost:{PORT}" diff --git a/tests/durabletask/extensions/history_export/test_entity.py b/tests/durabletask/extensions/history_export/test_entity.py index e0220c05..5442a4df 100644 --- a/tests/durabletask/extensions/history_export/test_entity.py +++ b/tests/durabletask/extensions/history_export/test_entity.py @@ -31,7 +31,7 @@ from durabletask.testing import create_test_backend from ._test_helpers import wait_until -from _port_utils import find_free_port +from tests.durabletask._port_utils import find_free_port PORT = find_free_port() HOST = f"localhost:{PORT}" diff --git a/tests/durabletask/extensions/history_export/test_orchestrator.py b/tests/durabletask/extensions/history_export/test_orchestrator.py index 7bee52e4..dc81118d 100644 --- a/tests/durabletask/extensions/history_export/test_orchestrator.py +++ b/tests/durabletask/extensions/history_export/test_orchestrator.py @@ -33,7 +33,7 @@ from durabletask.testing import create_test_backend from ._test_helpers import wait_until -from _port_utils import find_free_port +from tests.durabletask._port_utils import find_free_port PORT = find_free_port() HOST = f"localhost:{PORT}" diff --git a/tests/durabletask/test_batch_actions.py b/tests/durabletask/test_batch_actions.py index dea86d05..01f09414 100644 --- a/tests/durabletask/test_batch_actions.py +++ b/tests/durabletask/test_batch_actions.py @@ -15,7 +15,7 @@ from durabletask.testing import create_test_backend from durabletask.worker import TaskHubGrpcWorker -from _port_utils import find_free_port +from tests.durabletask._port_utils import find_free_port BATCH_TEST_PORT = find_free_port() HOST = f"localhost:{BATCH_TEST_PORT}" diff --git a/tests/durabletask/test_large_payload_e2e.py b/tests/durabletask/test_large_payload_e2e.py index 918ace9b..6ef76291 100644 --- a/tests/durabletask/test_large_payload_e2e.py +++ b/tests/durabletask/test_large_payload_e2e.py @@ -22,7 +22,7 @@ from durabletask import client, task, worker from durabletask.testing import create_test_backend -from _port_utils import find_free_port +from tests.durabletask._port_utils import find_free_port # Skip the entire module if azure-storage-blob is not installed. azure_blob = pytest.importorskip("azure.storage.blob") diff --git a/tests/durabletask/test_orchestration_async_e2e.py b/tests/durabletask/test_orchestration_async_e2e.py index cab73bd3..78205eb9 100644 --- a/tests/durabletask/test_orchestration_async_e2e.py +++ b/tests/durabletask/test_orchestration_async_e2e.py @@ -9,7 +9,7 @@ from durabletask import client, task, worker from durabletask.testing import create_test_backend -from _port_utils import find_free_port +from tests.durabletask._port_utils import find_free_port PORT = find_free_port() HOST = f"localhost:{PORT}" diff --git a/tests/durabletask/test_orchestration_e2e.py b/tests/durabletask/test_orchestration_e2e.py index 927acba5..2a22ec09 100644 --- a/tests/durabletask/test_orchestration_e2e.py +++ b/tests/durabletask/test_orchestration_e2e.py @@ -13,7 +13,7 @@ import durabletask.history as history from durabletask.testing import create_test_backend -from _port_utils import find_free_port +from tests.durabletask._port_utils import find_free_port PORT = find_free_port() HOST = f"localhost:{PORT}" diff --git a/tests/durabletask/test_orchestration_versioning_e2e.py b/tests/durabletask/test_orchestration_versioning_e2e.py index ca712f65..12ee53ff 100644 --- a/tests/durabletask/test_orchestration_versioning_e2e.py +++ b/tests/durabletask/test_orchestration_versioning_e2e.py @@ -8,7 +8,7 @@ from durabletask import client, task, worker from durabletask.testing import create_test_backend -from _port_utils import find_free_port +from tests.durabletask._port_utils import find_free_port PORT = find_free_port() HOST = f"localhost:{PORT}" diff --git a/tests/durabletask/test_work_item_filters_e2e.py b/tests/durabletask/test_work_item_filters_e2e.py index 7b1f7b42..e5e63b1e 100644 --- a/tests/durabletask/test_work_item_filters_e2e.py +++ b/tests/durabletask/test_work_item_filters_e2e.py @@ -18,7 +18,7 @@ ) from durabletask.testing import create_test_backend -from _port_utils import find_free_port +from tests.durabletask._port_utils import find_free_port PORT = find_free_port() HOST = f"localhost:{PORT}" From 7fe8f0d1570120e3518cd6e0f1cb7c9675a67aeb Mon Sep 17 00:00:00 2001 From: Andy Staples Date: Thu, 25 Jun 2026 11:11:40 -0600 Subject: [PATCH 3/4] Probe IPv6 first --- tests/durabletask/_port_utils.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/durabletask/_port_utils.py b/tests/durabletask/_port_utils.py index 3c3a7f02..e31a961a 100644 --- a/tests/durabletask/_port_utils.py +++ b/tests/durabletask/_port_utils.py @@ -21,7 +21,20 @@ def find_free_port() -> int: releases it and returns the chosen number. There is an inherent race between releasing the socket and the caller binding the port, but in practice it is reliable for tests and far safer than fixed ports. + + The in-memory backend binds the IPv6 wildcard (``[::]``), so the probe + uses an IPv6 socket to mirror that bind and avoid returning a port that + is free on IPv4 but already in use on IPv6. If IPv6 is unavailable, it + falls back to IPv4. """ + if socket.has_ipv6: + try: + with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s: + s.bind(("::", 0)) + return s.getsockname()[1] + except OSError: + pass # IPv6 not usable on this host; fall back to IPv4. + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(("localhost", 0)) + s.bind(("127.0.0.1", 0)) return s.getsockname()[1] From de4a0b675b2b4d83c70c1c75c4614319109fab0a Mon Sep 17 00:00:00 2001 From: Andy Staples Date: Thu, 25 Jun 2026 12:07:04 -0600 Subject: [PATCH 4/4] Probe loopback interface only --- tests/durabletask/_port_utils.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/durabletask/_port_utils.py b/tests/durabletask/_port_utils.py index e31a961a..16f97e51 100644 --- a/tests/durabletask/_port_utils.py +++ b/tests/durabletask/_port_utils.py @@ -15,22 +15,15 @@ def find_free_port() -> int: - """Return a TCP port number that is currently free. + """Return a free TCP port by binding to port 0 and reading the assignment. - Binds a socket to port 0 so the OS assigns an available port, then - releases it and returns the chosen number. There is an inherent race - between releasing the socket and the caller binding the port, but in - practice it is reliable for tests and far safer than fixed ports. - - The in-memory backend binds the IPv6 wildcard (``[::]``), so the probe - uses an IPv6 socket to mirror that bind and avoid returning a port that - is free on IPv4 but already in use on IPv6. If IPv6 is unavailable, it - falls back to IPv4. + Probes IPv6 loopback first to match the backend's ``[::]`` bind (so an + IPv6-occupied port isn't wrongly reported free), falling back to IPv4. """ if socket.has_ipv6: try: with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s: - s.bind(("::", 0)) + s.bind(("::1", 0)) return s.getsockname()[1] except OSError: pass # IPv6 not usable on this host; fall back to IPv4.