diff --git a/.chronus/changes/fix-windows-sensitive-word-test-2026-4-8.md b/.chronus/changes/fix-windows-sensitive-word-test-2026-4-8.md new file mode 100644 index 0000000000..c17d840a0b --- /dev/null +++ b/.chronus/changes/fix-windows-sensitive-word-test-2026-4-8.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@autorest/python" + - "@azure-tools/typespec-python" +--- +Fix `test_sensitive_word` failing on Windows by replacing shell-based search with pure Python `pathlib` implementation. diff --git a/.chronus/changes/sync-testing-improvements-from-10283-2026-3-8-14-39-47.md b/.chronus/changes/sync-testing-improvements-from-10283-2026-3-8-14-39-47.md new file mode 100644 index 0000000000..4d72008e55 --- /dev/null +++ b/.chronus/changes/sync-testing-improvements-from-10283-2026-3-8-14-39-47.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@azure-tools/typespec-python" +--- + +apply change of https://github.com/microsoft/typespec/pull/10283 \ No newline at end of file diff --git a/eng/pipelines/publish-release.yml b/eng/pipelines/publish-release.yml index ec0bfb9d31..db3642c73a 100644 --- a/eng/pipelines/publish-release.yml +++ b/eng/pipelines/publish-release.yml @@ -87,13 +87,13 @@ extends: displayName: Execute Autorest DPG Version Tolerant Tests workingDirectory: $(Build.SourcesDirectory)/packages/autorest.python/ - script: | - cd test/azure - tox run -e ci + cd tests + tox run -e ci-azure displayName: Execute Typespec Azure Tests workingDirectory: $(Build.SourcesDirectory)/packages/typespec-python/ - script: | - cd test/unbranded - tox run -e ci + cd tests + tox run -e ci-unbranded displayName: Execute Typespec Unbranded Tests workingDirectory: $(Build.SourcesDirectory)/packages/typespec-python/ diff --git a/eng/scripts/sync_from_typespec.py b/eng/scripts/sync_from_typespec.py index 679ad7d1d1..bda34ca36a 100644 --- a/eng/scripts/sync_from_typespec.py +++ b/eng/scripts/sync_from_typespec.py @@ -9,7 +9,7 @@ The typespec repo is the source of truth for: 1. regenerate-common.ts — shared regeneration logic - 2. requirements — test dependency files (azure.txt, unbranded.txt under tests/requirements/) + 2. requirements — test dependency files (tests/requirements/) 3. Test files — mock API tests under tests/mock_api/{shared,azure,unbranded} Usage: @@ -44,7 +44,7 @@ _MARKER_PATTERN = re.compile(r"^# === (common .+ across repos) ===$") _END_MARKER_PATTERN = re.compile(r"^# === end (common .+ across repos) ===$") -_REQUIREMENTS_FILES = ["azure.txt", "unbranded.txt"] +_REQUIREMENTS_FILES = ["azure.txt", "base.txt", "docs.txt", "lint.txt", "typecheck.txt", "unbranded.txt"] # --------------------------------------------------------------------------- diff --git a/packages/autorest.python/package.json b/packages/autorest.python/package.json index 38003b610a..cd145e028f 100644 --- a/packages/autorest.python/package.json +++ b/packages/autorest.python/package.json @@ -29,7 +29,7 @@ }, "homepage": "https://github.com/Azure/autorest.python/blob/main/README.md", "dependencies": { - "@typespec/http-client-python": "~0.28.3", + "@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", "@autorest/system-requirements": "~1.0.2", "fs-extra": "~11.2.0", "tsx": "^4.21.0" @@ -47,4 +47,4 @@ "requirements.txt", "generator/" ] -} +} \ No newline at end of file diff --git a/packages/typespec-python/package.json b/packages/typespec-python/package.json index a152a41acd..394f6b9881 100644 --- a/packages/typespec-python/package.json +++ b/packages/typespec-python/package.json @@ -65,7 +65,7 @@ "@typespec/xml": ">=0.81.0 <1.0.0" }, "dependencies": { - "@typespec/http-client-python": "~0.28.3", + "@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", "fs-extra": "~11.2.0", "js-yaml": "~4.1.0", "semver": "~7.6.2", diff --git a/packages/typespec-python/tests/conftest.py b/packages/typespec-python/tests/conftest.py index daa766db90..2779f7b673 100644 --- a/packages/typespec-python/tests/conftest.py +++ b/packages/typespec-python/tests/conftest.py @@ -9,9 +9,11 @@ import time import urllib.request import urllib.error +import tempfile import pytest import importlib from pathlib import Path +from filelock import FileLock # Root of the typespec-python package ROOT = Path(__file__).parent.parent @@ -22,8 +24,13 @@ SERVER_PORT = 3000 SERVER_URL = f"http://{SERVER_HOST}:{SERVER_PORT}" +# Lock file for coordinating server startup across xdist workers +LOCK_FILE = Path(tempfile.gettempdir()) / "typespec_python_test_server.lock" +PID_FILE = Path(tempfile.gettempdir()) / "typespec_python_test_server.pid" + # Global server process reference (used by hooks) _server_process = None +_owns_server = False # Track if this process started the server def wait_for_server(url: str, timeout: int = 60, interval: float = 0.5) -> bool: @@ -99,51 +106,77 @@ def terminate_server_process(process): def pytest_configure(config): """Start the mock server before any tests run. - This hook runs in the controller process before xdist workers are spawned, - ensuring the server is ready for all workers. + Uses file locking to ensure only one process starts the server, + even when running with pytest-xdist. The controller process starts + the server and workers wait for it to be ready. """ - global _server_process - - # Only start server in the controller process (not in workers) - # xdist workers have workerinput attribute - if hasattr(config, "workerinput"): - return + global _server_process, _owns_server - # Check if server is already running (e.g., from a previous run) + # Check if server is already running (e.g., from a previous run or external process) if wait_for_server(SERVER_URL, timeout=1, interval=0.1): print(f"Mock API server already running at {SERVER_URL}") return - # Start the server - print(f"Starting mock API server...") - _server_process = start_server_process() - - # Check if process started successfully - if _server_process.poll() is not None: - pytest.exit(f"Mock API server process exited immediately with code {_server_process.returncode}") - - # Wait for server to be ready - if not wait_for_server(SERVER_URL, timeout=60): - # Check if process is still running - if _server_process.poll() is not None: - pytest.exit(f"Mock API server process died with code {_server_process.returncode}") - terminate_server_process(_server_process) - _server_process = None - pytest.exit(f"Mock API server failed to start within 60 seconds at {SERVER_URL}") + # Use file lock to ensure only one process starts the server + # This handles both xdist workers and multiple test runs + lock = FileLock(str(LOCK_FILE), timeout=120) - print(f"Mock API server ready at {SERVER_URL}") + try: + with lock: + # Double-check after acquiring lock (another process may have started it) + if wait_for_server(SERVER_URL, timeout=1, interval=0.1): + print(f"Mock API server already running at {SERVER_URL}") + return + + # We're the first process - start the server + print(f"Starting mock API server...") + _server_process = start_server_process() + _owns_server = True + + # Check if process started successfully + if _server_process.poll() is not None: + pytest.exit(f"Mock API server process exited immediately with code {_server_process.returncode}") + + # Write PID file so other processes know who owns the server + PID_FILE.write_text(str(_server_process.pid)) + + # Wait for server to be ready + if not wait_for_server(SERVER_URL, timeout=60): + if _server_process.poll() is not None: + pytest.exit(f"Mock API server process died with code {_server_process.returncode}") + terminate_server_process(_server_process) + _server_process = None + _owns_server = False + pytest.exit(f"Mock API server failed to start within 60 seconds at {SERVER_URL}") + + print(f"Mock API server ready at {SERVER_URL}") + + except TimeoutError: + # Another process is holding the lock for too long + # Check if server is available anyway + if wait_for_server(SERVER_URL, timeout=5): + print(f"Mock API server available at {SERVER_URL} (started by another process)") + else: + pytest.exit("Timeout waiting for server lock - another process may be stuck") def pytest_unconfigure(config): """Stop the mock server after all tests complete.""" - global _server_process + global _server_process, _owns_server - # Only stop server in the controller process - if hasattr(config, "workerinput"): + # Only stop the server if this process started it + if not _owns_server: return terminate_server_process(_server_process) _server_process = None + _owns_server = False + + # Clean up PID file + try: + PID_FILE.unlink(missing_ok=True) + except Exception: + pass @pytest.fixture(scope="session", autouse=True) diff --git a/packages/typespec-python/tests/mock_api/unbranded/test_unbranded.py b/packages/typespec-python/tests/mock_api/unbranded/test_unbranded.py index 092fca42af..f576368fc3 100644 --- a/packages/typespec-python/tests/mock_api/unbranded/test_unbranded.py +++ b/packages/typespec-python/tests/mock_api/unbranded/test_unbranded.py @@ -3,8 +3,6 @@ # Licensed under the MIT License. # ------------------------------------ import os -import re -from subprocess import getoutput from pathlib import Path import traceback from importlib import import_module @@ -33,33 +31,37 @@ def test_track_back(client: ScalarClient): assert "microsoft" not in track_back -def check_sensitive_word(folder: Path, word: str) -> str: - special_folders = ["__pycache__", "pytest_cache"] - if os.name == "nt": - skip_folders = "|".join(special_folders) - output = getoutput( - f"powershell \"ls -r -Path {folder} | where fullname -notmatch '{skip_folders}' | Select-String -Pattern '{word}'\"" - ).replace("\\", "/") - else: - skip_folders = "{" + ",".join(special_folders) + "}" - output = getoutput(f"grep -ri --exclude-dir={skip_folders} {word} {folder}") +_SKIP_DIRS = {"__pycache__", "pytest_cache", ".pytest_cache"} + +def check_sensitive_word(folder: Path, word: str) -> list[str]: + """Search for a word in all files under folder, return top-level subfolder names that contain it.""" result = set() - for item in re.findall(f"{folder.as_posix()}[^:]+", output.replace("\n", "")): - result.add(Path(item).relative_to(folder).parts[0]) - return sorted(list(result)) + for path in folder.rglob("*"): + if not path.is_file(): + continue + # Skip special directories + if _SKIP_DIRS & set(path.relative_to(folder).parts): + continue + try: + content = path.read_text(encoding="utf-8", errors="ignore") + except (OSError, UnicodeDecodeError): + continue + if word.lower() in content.lower(): + result.add(path.relative_to(folder).parts[0]) + return sorted(result) def test_sensitive_word(): check_folder = (Path(os.path.dirname(__file__)) / "../../generated/unbranded").resolve() assert [] == check_sensitive_word(check_folder, "azure") # after update spector, it shall also equal to [] - # expected = [ - # "authentication-oauth2", - # "authentication-noauth-union", - # "authentication-union", - # "setuppy-authentication-union", - # ] - # if (check_folder / "generation-subdir").exists(): - # expected.append("generation-subdir") - # assert sorted(expected) == sorted(check_sensitive_word(check_folder, "microsoft")) + expected = [ + "authentication-oauth2", + "authentication-noauth-union", + "authentication-union", + "setuppy-authentication-union", + ] + if (check_folder / "generation-subdir").exists(): + expected.append("generation-subdir") + assert sorted(expected) == sorted(check_sensitive_word(check_folder, "microsoft")) diff --git a/packages/typespec-python/tests/requirements/base.txt b/packages/typespec-python/tests/requirements/base.txt index 7dd9f6da1c..9331a4a1d8 100644 --- a/packages/typespec-python/tests/requirements/base.txt +++ b/packages/typespec-python/tests/requirements/base.txt @@ -9,3 +9,4 @@ isodate>=0.6.1 typing-extensions>=4.6.0 tox>=4.16.0 tox-uv>=1.0.0 +filelock>=3.12.0 diff --git a/packages/typespec-python/tests/tox.ini b/packages/typespec-python/tests/tox.ini index 5bd5a7f215..100580ca30 100644 --- a/packages/typespec-python/tests/tox.ini +++ b/packages/typespec-python/tests/tox.ini @@ -29,7 +29,7 @@ deps = -r {tox_root}/requirements/azure.txt commands = python {tox_root}/install_packages.py azure {tox_root} - pytest mock_api/azure mock_api/shared -v {posargs} + pytest mock_api/azure mock_api/shared -v -n auto {posargs} [testenv:test-unbranded] description = Run tests for unbranded flavor @@ -41,7 +41,7 @@ deps = -r {tox_root}/requirements/unbranded.txt commands = python {tox_root}/install_packages.py unbranded {tox_root} - pytest mock_api/unbranded mock_api/shared -v {posargs} + pytest mock_api/unbranded mock_api/shared -v -n auto {posargs} # ============================================================================= # Lint environments @@ -176,7 +176,7 @@ deps = commands = 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/" python {tox_root}/install_packages.py azure {tox_root} - pytest mock_api/azure mock_api/shared -v + pytest mock_api/azure mock_api/shared -v -n auto python {tox_root}/../eng/scripts/ci/run_pylint.py -t azure -s generated python {tox_root}/../eng/scripts/ci/run_mypy.py -t azure -s generated python {tox_root}/../eng/scripts/ci/run_pyright.py -t azure -s generated @@ -194,7 +194,7 @@ deps = commands = 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/" python {tox_root}/install_packages.py unbranded {tox_root} - pytest mock_api/unbranded mock_api/shared -v + pytest mock_api/unbranded mock_api/shared -v -n auto python {tox_root}/../eng/scripts/ci/run_pylint.py -t unbranded -s generated python {tox_root}/../eng/scripts/ci/run_mypy.py -t unbranded -s generated python {tox_root}/../eng/scripts/ci/run_pyright.py -t unbranded -s generated diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ff3711370..9a86eebfe9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,8 +60,8 @@ importers: specifier: ~1.0.2 version: 1.0.2 '@typespec/http-client-python': - specifier: ~0.28.3 - version: 0.28.3(y2blksstlsblh3iqmgy3brbone) + specifier: 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 + version: 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(y2blksstlsblh3iqmgy3brbone) fs-extra: specifier: ~11.2.0 version: 11.2.0 @@ -82,8 +82,8 @@ importers: packages/typespec-python: dependencies: '@typespec/http-client-python': - specifier: ~0.28.3 - version: 0.28.3(y2blksstlsblh3iqmgy3brbone) + specifier: 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 + version: 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(y2blksstlsblh3iqmgy3brbone) fs-extra: specifier: ~11.2.0 version: 11.2.0 @@ -1723,8 +1723,9 @@ packages: peerDependencies: '@typespec/compiler': ^1.11.0 - '@typespec/http-client-python@0.28.3': - resolution: {integrity: sha512-UkahPQB23vAYFY+7YtN1H+wG1+H642Hw8FlF5LAEtSx0bu2PNkrQe9W8COZWLoVKBuWh5oLJEzVZd8FRP9nBbg==} + '@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': + resolution: {tarball: 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} + version: 0.28.3 engines: {node: '>=20.0.0'} peerDependencies: '@azure-tools/typespec-autorest': '>=0.67.0 <1.0.0' @@ -6707,7 +6708,7 @@ snapshots: dependencies: '@typespec/compiler': 1.11.0(@types/node@25.0.10) - '@typespec/http-client-python@0.28.3(y2blksstlsblh3iqmgy3brbone)': + '@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(y2blksstlsblh3iqmgy3brbone)': dependencies: '@azure-tools/typespec-autorest': 0.67.0(7o3sem7p6i3t4jykacdovvl7ii) '@azure-tools/typespec-azure-core': 0.67.0(@typespec/compiler@1.11.0(@types/node@25.0.10))(@typespec/http@1.11.0(@typespec/compiler@1.11.0(@types/node@25.0.10))(@typespec/streams@0.81.0(@typespec/compiler@1.11.0(@types/node@25.0.10))))(@typespec/rest@0.81.0(@typespec/compiler@1.11.0(@types/node@25.0.10))(@typespec/http@1.11.0(@typespec/compiler@1.11.0(@types/node@25.0.10))(@typespec/streams@0.81.0(@typespec/compiler@1.11.0(@types/node@25.0.10)))))