Skip to content

Commit 7253130

Browse files
msyycCopilot
andauthored
Apply testing improvements from typespec PR #10283 (#3424)
* Apply testing improvements from typespec PR #10283 - Replace xdist workerinput check with filelock-based server coordination in conftest.py for reliable multi-process server management - Add parallel test execution (-n auto) to all pytest commands in tox.ini - Add filelock>=3.12.0 dependency to base.txt Ported from: microsoft/typespec#10283 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * update * add changelog * Update dependencies * Sync shared files from typespec repo (2026-04-08 07:46:39) * Add changelog * Update dependencies (2026-04-08 07:47:45) * Fix publish pipeline test paths for typespec-python The typespec-python test directory was restructured from separate test/azure/ and test/unbranded/ directories into a consolidated tests/ directory with ci-azure and ci-unbranded tox environments. Update publish-release.yml to match the new structure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AutoPrFromHttpClientPython <AutoPrFromHttpClientPython>
1 parent 7fba86a commit 7253130

11 files changed

Lines changed: 124 additions & 73 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: internal
3+
packages:
4+
- "@autorest/python"
5+
- "@azure-tools/typespec-python"
6+
---
7+
Fix `test_sensitive_word` failing on Windows by replacing shell-based search with pure Python `pathlib` implementation.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: internal
3+
packages:
4+
- "@azure-tools/typespec-python"
5+
---
6+
7+
apply change of https://github.com/microsoft/typespec/pull/10283

eng/pipelines/publish-release.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,13 @@ extends:
8787
displayName: Execute Autorest DPG Version Tolerant Tests
8888
workingDirectory: $(Build.SourcesDirectory)/packages/autorest.python/
8989
- script: |
90-
cd test/azure
91-
tox run -e ci
90+
cd tests
91+
tox run -e ci-azure
9292
displayName: Execute Typespec Azure Tests
9393
workingDirectory: $(Build.SourcesDirectory)/packages/typespec-python/
9494
- script: |
95-
cd test/unbranded
96-
tox run -e ci
95+
cd tests
96+
tox run -e ci-unbranded
9797
displayName: Execute Typespec Unbranded Tests
9898
workingDirectory: $(Build.SourcesDirectory)/packages/typespec-python/
9999

eng/scripts/sync_from_typespec.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
1010
The typespec repo is the source of truth for:
1111
1. regenerate-common.ts — shared regeneration logic
12-
2. requirements — test dependency files (azure.txt, unbranded.txt under tests/requirements/)
12+
2. requirements — test dependency files (tests/requirements/)
1313
3. Test files — mock API tests under tests/mock_api/{shared,azure,unbranded}
1414
1515
Usage:
@@ -44,7 +44,7 @@
4444
_MARKER_PATTERN = re.compile(r"^# === (common .+ across repos) ===$")
4545
_END_MARKER_PATTERN = re.compile(r"^# === end (common .+ across repos) ===$")
4646

47-
_REQUIREMENTS_FILES = ["azure.txt", "unbranded.txt"]
47+
_REQUIREMENTS_FILES = ["azure.txt", "base.txt", "docs.txt", "lint.txt", "typecheck.txt", "unbranded.txt"]
4848

4949

5050
# ---------------------------------------------------------------------------

packages/autorest.python/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
},
3030
"homepage": "https://github.com/Azure/autorest.python/blob/main/README.md",
3131
"dependencies": {
32-
"@typespec/http-client-python": "~0.28.3",
32+
"@typespec/http-client-python": "https://artprodcus3.artifacts.visualstudio.com/A0fb41ef4-5012-48a9-bf39-4ee3de03ee35/29ec6040-b234-4e31-b139-33dc4287b756/_apis/artifact/cGlwZWxpbmVhcnRpZmFjdDovL2F6dXJlLXNkay9wcm9qZWN0SWQvMjllYzYwNDAtYjIzNC00ZTMxLWIxMzktMzNkYzQyODdiNzU2L2J1aWxkSWQvNjEyMjcwMy9hcnRpZmFjdE5hbWUvYnVpbGRfYXJ0aWZhY3RzX3B5dGhvbg2/content?format=file&subPath=%2Fpackages%2Ftypespec-http-client-python-0.28.3.tgz",
3333
"@autorest/system-requirements": "~1.0.2",
3434
"fs-extra": "~11.2.0",
3535
"tsx": "^4.21.0"
@@ -47,4 +47,4 @@
4747
"requirements.txt",
4848
"generator/"
4949
]
50-
}
50+
}

packages/typespec-python/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
"@typespec/xml": ">=0.81.0 <1.0.0"
6666
},
6767
"dependencies": {
68-
"@typespec/http-client-python": "~0.28.3",
68+
"@typespec/http-client-python": "https://artprodcus3.artifacts.visualstudio.com/A0fb41ef4-5012-48a9-bf39-4ee3de03ee35/29ec6040-b234-4e31-b139-33dc4287b756/_apis/artifact/cGlwZWxpbmVhcnRpZmFjdDovL2F6dXJlLXNkay9wcm9qZWN0SWQvMjllYzYwNDAtYjIzNC00ZTMxLWIxMzktMzNkYzQyODdiNzU2L2J1aWxkSWQvNjEyMjcwMy9hcnRpZmFjdE5hbWUvYnVpbGRfYXJ0aWZhY3RzX3B5dGhvbg2/content?format=file&subPath=%2Fpackages%2Ftypespec-http-client-python-0.28.3.tgz",
6969
"fs-extra": "~11.2.0",
7070
"js-yaml": "~4.1.0",
7171
"semver": "~7.6.2",

packages/typespec-python/tests/conftest.py

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
import time
1010
import urllib.request
1111
import urllib.error
12+
import tempfile
1213
import pytest
1314
import importlib
1415
from pathlib import Path
16+
from filelock import FileLock
1517

1618
# Root of the typespec-python package
1719
ROOT = Path(__file__).parent.parent
@@ -22,8 +24,13 @@
2224
SERVER_PORT = 3000
2325
SERVER_URL = f"http://{SERVER_HOST}:{SERVER_PORT}"
2426

27+
# Lock file for coordinating server startup across xdist workers
28+
LOCK_FILE = Path(tempfile.gettempdir()) / "typespec_python_test_server.lock"
29+
PID_FILE = Path(tempfile.gettempdir()) / "typespec_python_test_server.pid"
30+
2531
# Global server process reference (used by hooks)
2632
_server_process = None
33+
_owns_server = False # Track if this process started the server
2734

2835

2936
def wait_for_server(url: str, timeout: int = 60, interval: float = 0.5) -> bool:
@@ -99,51 +106,77 @@ def terminate_server_process(process):
99106
def pytest_configure(config):
100107
"""Start the mock server before any tests run.
101108
102-
This hook runs in the controller process before xdist workers are spawned,
103-
ensuring the server is ready for all workers.
109+
Uses file locking to ensure only one process starts the server,
110+
even when running with pytest-xdist. The controller process starts
111+
the server and workers wait for it to be ready.
104112
"""
105-
global _server_process
106-
107-
# Only start server in the controller process (not in workers)
108-
# xdist workers have workerinput attribute
109-
if hasattr(config, "workerinput"):
110-
return
113+
global _server_process, _owns_server
111114

112-
# Check if server is already running (e.g., from a previous run)
115+
# Check if server is already running (e.g., from a previous run or external process)
113116
if wait_for_server(SERVER_URL, timeout=1, interval=0.1):
114117
print(f"Mock API server already running at {SERVER_URL}")
115118
return
116119

117-
# Start the server
118-
print(f"Starting mock API server...")
119-
_server_process = start_server_process()
120-
121-
# Check if process started successfully
122-
if _server_process.poll() is not None:
123-
pytest.exit(f"Mock API server process exited immediately with code {_server_process.returncode}")
124-
125-
# Wait for server to be ready
126-
if not wait_for_server(SERVER_URL, timeout=60):
127-
# Check if process is still running
128-
if _server_process.poll() is not None:
129-
pytest.exit(f"Mock API server process died with code {_server_process.returncode}")
130-
terminate_server_process(_server_process)
131-
_server_process = None
132-
pytest.exit(f"Mock API server failed to start within 60 seconds at {SERVER_URL}")
120+
# Use file lock to ensure only one process starts the server
121+
# This handles both xdist workers and multiple test runs
122+
lock = FileLock(str(LOCK_FILE), timeout=120)
133123

134-
print(f"Mock API server ready at {SERVER_URL}")
124+
try:
125+
with lock:
126+
# Double-check after acquiring lock (another process may have started it)
127+
if wait_for_server(SERVER_URL, timeout=1, interval=0.1):
128+
print(f"Mock API server already running at {SERVER_URL}")
129+
return
130+
131+
# We're the first process - start the server
132+
print(f"Starting mock API server...")
133+
_server_process = start_server_process()
134+
_owns_server = True
135+
136+
# Check if process started successfully
137+
if _server_process.poll() is not None:
138+
pytest.exit(f"Mock API server process exited immediately with code {_server_process.returncode}")
139+
140+
# Write PID file so other processes know who owns the server
141+
PID_FILE.write_text(str(_server_process.pid))
142+
143+
# Wait for server to be ready
144+
if not wait_for_server(SERVER_URL, timeout=60):
145+
if _server_process.poll() is not None:
146+
pytest.exit(f"Mock API server process died with code {_server_process.returncode}")
147+
terminate_server_process(_server_process)
148+
_server_process = None
149+
_owns_server = False
150+
pytest.exit(f"Mock API server failed to start within 60 seconds at {SERVER_URL}")
151+
152+
print(f"Mock API server ready at {SERVER_URL}")
153+
154+
except TimeoutError:
155+
# Another process is holding the lock for too long
156+
# Check if server is available anyway
157+
if wait_for_server(SERVER_URL, timeout=5):
158+
print(f"Mock API server available at {SERVER_URL} (started by another process)")
159+
else:
160+
pytest.exit("Timeout waiting for server lock - another process may be stuck")
135161

136162

137163
def pytest_unconfigure(config):
138164
"""Stop the mock server after all tests complete."""
139-
global _server_process
165+
global _server_process, _owns_server
140166

141-
# Only stop server in the controller process
142-
if hasattr(config, "workerinput"):
167+
# Only stop the server if this process started it
168+
if not _owns_server:
143169
return
144170

145171
terminate_server_process(_server_process)
146172
_server_process = None
173+
_owns_server = False
174+
175+
# Clean up PID file
176+
try:
177+
PID_FILE.unlink(missing_ok=True)
178+
except Exception:
179+
pass
147180

148181

149182
@pytest.fixture(scope="session", autouse=True)

packages/typespec-python/tests/mock_api/unbranded/test_unbranded.py

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
# Licensed under the MIT License.
44
# ------------------------------------
55
import os
6-
import re
7-
from subprocess import getoutput
86
from pathlib import Path
97
import traceback
108
from importlib import import_module
@@ -33,33 +31,37 @@ def test_track_back(client: ScalarClient):
3331
assert "microsoft" not in track_back
3432

3533

36-
def check_sensitive_word(folder: Path, word: str) -> str:
37-
special_folders = ["__pycache__", "pytest_cache"]
38-
if os.name == "nt":
39-
skip_folders = "|".join(special_folders)
40-
output = getoutput(
41-
f"powershell \"ls -r -Path {folder} | where fullname -notmatch '{skip_folders}' | Select-String -Pattern '{word}'\""
42-
).replace("\\", "/")
43-
else:
44-
skip_folders = "{" + ",".join(special_folders) + "}"
45-
output = getoutput(f"grep -ri --exclude-dir={skip_folders} {word} {folder}")
34+
_SKIP_DIRS = {"__pycache__", "pytest_cache", ".pytest_cache"}
4635

36+
37+
def check_sensitive_word(folder: Path, word: str) -> list[str]:
38+
"""Search for a word in all files under folder, return top-level subfolder names that contain it."""
4739
result = set()
48-
for item in re.findall(f"{folder.as_posix()}[^:]+", output.replace("\n", "")):
49-
result.add(Path(item).relative_to(folder).parts[0])
50-
return sorted(list(result))
40+
for path in folder.rglob("*"):
41+
if not path.is_file():
42+
continue
43+
# Skip special directories
44+
if _SKIP_DIRS & set(path.relative_to(folder).parts):
45+
continue
46+
try:
47+
content = path.read_text(encoding="utf-8", errors="ignore")
48+
except (OSError, UnicodeDecodeError):
49+
continue
50+
if word.lower() in content.lower():
51+
result.add(path.relative_to(folder).parts[0])
52+
return sorted(result)
5153

5254

5355
def test_sensitive_word():
5456
check_folder = (Path(os.path.dirname(__file__)) / "../../generated/unbranded").resolve()
5557
assert [] == check_sensitive_word(check_folder, "azure")
5658
# after update spector, it shall also equal to []
57-
# expected = [
58-
# "authentication-oauth2",
59-
# "authentication-noauth-union",
60-
# "authentication-union",
61-
# "setuppy-authentication-union",
62-
# ]
63-
# if (check_folder / "generation-subdir").exists():
64-
# expected.append("generation-subdir")
65-
# assert sorted(expected) == sorted(check_sensitive_word(check_folder, "microsoft"))
59+
expected = [
60+
"authentication-oauth2",
61+
"authentication-noauth-union",
62+
"authentication-union",
63+
"setuppy-authentication-union",
64+
]
65+
if (check_folder / "generation-subdir").exists():
66+
expected.append("generation-subdir")
67+
assert sorted(expected) == sorted(check_sensitive_word(check_folder, "microsoft"))

packages/typespec-python/tests/requirements/base.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ isodate>=0.6.1
99
typing-extensions>=4.6.0
1010
tox>=4.16.0
1111
tox-uv>=1.0.0
12+
filelock>=3.12.0

packages/typespec-python/tests/tox.ini

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ deps =
2929
-r {tox_root}/requirements/azure.txt
3030
commands =
3131
python {tox_root}/install_packages.py azure {tox_root}
32-
pytest mock_api/azure mock_api/shared -v {posargs}
32+
pytest mock_api/azure mock_api/shared -v -n auto {posargs}
3333

3434
[testenv:test-unbranded]
3535
description = Run tests for unbranded flavor
@@ -41,7 +41,7 @@ deps =
4141
-r {tox_root}/requirements/unbranded.txt
4242
commands =
4343
python {tox_root}/install_packages.py unbranded {tox_root}
44-
pytest mock_api/unbranded mock_api/shared -v {posargs}
44+
pytest mock_api/unbranded mock_api/shared -v -n auto {posargs}
4545

4646
# =============================================================================
4747
# Lint environments
@@ -176,7 +176,7 @@ deps =
176176
commands =
177177
uv pip install azure-pylint-guidelines-checker==0.5.2 --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/"
178178
python {tox_root}/install_packages.py azure {tox_root}
179-
pytest mock_api/azure mock_api/shared -v
179+
pytest mock_api/azure mock_api/shared -v -n auto
180180
python {tox_root}/../eng/scripts/ci/run_pylint.py -t azure -s generated
181181
python {tox_root}/../eng/scripts/ci/run_mypy.py -t azure -s generated
182182
python {tox_root}/../eng/scripts/ci/run_pyright.py -t azure -s generated
@@ -194,7 +194,7 @@ deps =
194194
commands =
195195
uv pip install azure-pylint-guidelines-checker==0.5.2 --index-url="https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/"
196196
python {tox_root}/install_packages.py unbranded {tox_root}
197-
pytest mock_api/unbranded mock_api/shared -v
197+
pytest mock_api/unbranded mock_api/shared -v -n auto
198198
python {tox_root}/../eng/scripts/ci/run_pylint.py -t unbranded -s generated
199199
python {tox_root}/../eng/scripts/ci/run_mypy.py -t unbranded -s generated
200200
python {tox_root}/../eng/scripts/ci/run_pyright.py -t unbranded -s generated

0 commit comments

Comments
 (0)