From c54ccc7e006d058d732a2f417514889f944bf04e Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Wed, 10 Jun 2026 11:44:55 +0800 Subject: [PATCH 1/2] Gracefully stop mock test server to flush spec coverage --- ...aceful-stop-mock-server-2026-6-10-0-0-0.md | 7 +++ packages/http-client-python/tests/conftest.py | 55 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 .chronus/changes/python-graceful-stop-mock-server-2026-6-10-0-0-0.md diff --git a/.chronus/changes/python-graceful-stop-mock-server-2026-6-10-0-0-0.md b/.chronus/changes/python-graceful-stop-mock-server-2026-6-10-0-0-0.md new file mode 100644 index 00000000000..8d38d9b5ecb --- /dev/null +++ b/.chronus/changes/python-graceful-stop-mock-server-2026-6-10-0-0-0.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/http-client-python" +--- + +Gracefully stop the mock test server at the end of the test session so spec coverage is flushed to disk diff --git a/packages/http-client-python/tests/conftest.py b/packages/http-client-python/tests/conftest.py index a0badda9ef7..ca9a445bfab 100644 --- a/packages/http-client-python/tests/conftest.py +++ b/packages/http-client-python/tests/conftest.py @@ -96,6 +96,61 @@ def terminate_server_process(process): pass +def graceful_stop_server(timeout: float = 30.0) -> None: + """Gracefully stop the mock server so it writes its coverage file. + + The tsp-spector server only persists spec-coverage.json from its process + ``exit`` handler, which is triggered by the ``tsp-spector server stop`` + command (it posts to the ``/.admin/stop`` admin endpoint and the server then + calls ``process.exit(0)``). A hard kill of the process skips that handler and + leaves no coverage file. Stopping the server via the CLI here lets coverage + be written by the test run itself, so no extra pipeline step is required to + flush coverage before uploading it. + """ + env = os.environ.copy() + node_bin = str(ROOT / "node_modules" / ".bin") + env["PATH"] = f"{node_bin}{os.pathsep}{env.get('PATH', '')}" + try: + subprocess.run( + f"npx tsp-spector server stop --port {SERVER_PORT}", + shell=True, + cwd=str(ROOT), + env=env, + capture_output=True, + check=False, + timeout=30, + ) + except Exception: + # Server already stopped or never started — nothing to do. + return + + # Coverage is written synchronously in the server's exit handler. Wait until + # the server is no longer reachable to ensure the file is flushed to disk. + deadline = time.time() + timeout + while time.time() < deadline: + try: + urllib.request.urlopen(SERVER_URL, timeout=1) + except urllib.error.HTTPError: + pass # Server up but returned an error response — still running. + except (urllib.error.URLError, OSError): + return # Server is down — coverage has been flushed. + time.sleep(0.3) + + +def pytest_unconfigure(config): + """Stop the shared mock server once the whole test session is finished. + + Under pytest-xdist the server must outlive individual workers (which may + finish at different times), so it is intentionally not stopped in the + session-scoped fixture teardown. This hook runs after all workers complete: + only the controller process (no ``workerinput``) gracefully stops the + server so the coverage file is written. + """ + if hasattr(config, "workerinput"): + return # xdist worker — leave the shared server running for others. + graceful_stop_server() + + @pytest.fixture(scope="session", autouse=True) def testserver(): """Start the mock API server, coordinated across xdist workers via file lock. From 07b078faa835fad1c77e6c2aca2595368c3d7686 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Wed, 10 Jun 2026 11:47:07 +0800 Subject: [PATCH 2/2] update --- packages/http-client-python/eng/scripts/ci/run-tests.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/http-client-python/eng/scripts/ci/run-tests.ts b/packages/http-client-python/eng/scripts/ci/run-tests.ts index dbd27fc7b1a..5917596169e 100644 --- a/packages/http-client-python/eng/scripts/ci/run-tests.ts +++ b/packages/http-client-python/eng/scripts/ci/run-tests.ts @@ -395,8 +395,9 @@ async function main(): Promise { if (runGenerator || runBoth) { console.log(`\n${pc.bold("=== Generator Tests (Python) ===")}\n`); - // Determine flavors - const flavors = argv.values.flavor === "all" ? ["azure", "unbranded"] : [argv.values.flavor!]; + // Determine flavors (default to both and "unbranded" first then "azure" so that spec-coverage.json + // could record all results in one run) + const flavors = argv.values.flavor === "all" ? ["unbranded", "azure"] : [argv.values.flavor!]; // Determine environments let baseEnvs: string[];