From fc70bc09dfba784ef0baec24b325ef872ac354ac Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Mon, 2 Nov 2026 10:24:21 +0100 Subject: [PATCH] Python: Fix native changed-package path routing --- .github/workflows/python-tests.yml | 6 ++ .../scripts/run_tasks_in_changed_packages.py | 13 ++-- .../test_run_tasks_in_changed_packages.py | 67 +++++++++++++++++++ 3 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 python/scripts/tests/test_run_tasks_in_changed_packages.py diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 5ae9ca2ce38..c2204ffddf8 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -99,6 +99,12 @@ jobs: run: uv run poe test -A --junitxml=pytest.xml working-directory: ./python + - name: Run changed-package routing tests + run: >- + uv run --locked --group dev --group tool-runtime + python -m pytest scripts/tests/test_run_tasks_in_changed_packages.py --import-mode=importlib -q + working-directory: ./python + # Surface failing tests - name: Surface failing tests if: always() diff --git a/python/scripts/run_tasks_in_changed_packages.py b/python/scripts/run_tasks_in_changed_packages.py index 46e742c3a0c..93cd0c967f7 100644 --- a/python/scripts/run_tasks_in_changed_packages.py +++ b/python/scripts/run_tasks_in_changed_packages.py @@ -6,6 +6,7 @@ from pathlib import Path from rich import print + from scripts.task_runner import build_work_items, discover_projects, run_tasks # Tasks that need to run in all packages when core changes (type info propagates) @@ -24,15 +25,15 @@ def get_changed_packages( core_package_changed = False for file_path in changed_files: - # Strip 'python/' prefix if present (when git diff is run from repo root) - file_path_str = str(file_path) - if file_path_str.startswith("python/"): - file_path_str = file_path_str[7:] # Remove 'python/' prefix + # Strip the repository prefix using native separators so POSIX backslashes remain filename characters. + file_path_obj = Path(file_path) + if file_path_obj.parts and file_path_obj.parts[0] == "python": + file_path_obj = Path(*file_path_obj.parts[1:]) # Convert to absolute path if relative - abs_path = Path(file_path_str) + abs_path = file_path_obj if not abs_path.is_absolute(): - abs_path = workspace_root / file_path_str + abs_path = workspace_root / file_path_obj # Check which package this file belongs to for project in projects: diff --git a/python/scripts/tests/test_run_tasks_in_changed_packages.py b/python/scripts/tests/test_run_tasks_in_changed_packages.py new file mode 100644 index 00000000000..6a85156aa28 --- /dev/null +++ b/python/scripts/tests/test_run_tasks_in_changed_packages.py @@ -0,0 +1,67 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Regression tests for changed-package path routing.""" + +from __future__ import annotations + +import os +from pathlib import Path + +import pytest + +from scripts.run_tasks_in_changed_packages import get_changed_packages + +PROJECTS = [Path("packages/core"), Path("packages/redis")] + + +@pytest.mark.parametrize("package", ["core", "redis"]) +@pytest.mark.parametrize("spelling", ["forward", "native", "dot-forward", "dot-native"]) +def test_repository_relative_paths_select_package(tmp_path: Path, package: str, spelling: str) -> None: + workspace_root = tmp_path / "python" + relative = Path("python") / "packages" / package / "example.py" + paths = { + "forward": f"python/packages/{package}/example.py", + "native": str(relative), + "dot-forward": f"./python/packages/{package}/example.py", + "dot-native": f".{os.sep}{relative}", + } + + assert get_changed_packages(PROJECTS, [paths[spelling]], workspace_root) == ( + {Path("packages") / package}, + package == "core", + ) + + +def test_existing_workspace_relative_and_absolute_paths_select_core(tmp_path: Path) -> None: + workspace_root = tmp_path / "python" + relative = Path("packages/core/example.py") + paths = [ + relative.as_posix(), + str(relative), + f"./{relative.as_posix()}", + f".{os.sep}{relative}", + str(workspace_root / relative), + ] + + for file_path in paths: + assert get_changed_packages(PROJECTS, [file_path], workspace_root) == ({PROJECTS[0]}, True), file_path + + +@pytest.mark.skipif(os.name != "nt", reason="UNC paths use native Windows semantics.") +def test_unc_absolute_path_selects_core() -> None: + workspace_root = Path(r"\\server\share\repo\python") + file_path = workspace_root / PROJECTS[0] / "example.py" + + assert get_changed_packages(PROJECTS, [str(file_path)], workspace_root) == ({PROJECTS[0]}, True) + + +@pytest.mark.skipif(os.name == "nt", reason="Backslash is a path separator on Windows.") +def test_posix_literal_backslash_package_keeps_its_identity(tmp_path: Path) -> None: + workspace_root = tmp_path / "python" + literal_package = Path(r"packages/core\notes") + file_path = Path("python") / literal_package / "example.py" + + assert get_changed_packages([*PROJECTS, literal_package], [str(file_path)], workspace_root) == ( + {literal_package}, + False, + )