From d2af7b5673fceaa5e67d2a8c00daad13d79d8ec0 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 16 Jul 2026 18:11:52 +0800 Subject: [PATCH 1/3] Skip dev_requirements/sdist install for changelog when using apistub The apistub path in the breaking-change check builds the code report via static analysis and never imports the target package, so installing the package's dev_requirements.txt and building/installing the sdist is unnecessary. This also avoids a local wheel-build failure (WinError 183 from stale in-source build/ dirs) during changelog generation, which always uses --use-apistub. Guard both steps behind 'not use_apistub' and add tests. --- eng/tools/azure-sdk-tools/azpysdk/breaking.py | 34 ++++--- .../azure-sdk-tools/tests/test_breaking.py | 96 +++++++++++++++++++ 2 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 eng/tools/azure-sdk-tools/tests/test_breaking.py diff --git a/eng/tools/azure-sdk-tools/azpysdk/breaking.py b/eng/tools/azure-sdk-tools/azpysdk/breaking.py index f204f1794e39..83f289963cdd 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/breaking.py +++ b/eng/tools/azure-sdk-tools/azpysdk/breaking.py @@ -128,8 +128,15 @@ def run(self, args: argparse.Namespace) -> int: ) logger.info(f"Processing {package_name} for breaking check...") - # install dependencies - self.install_dev_reqs(executable, args, package_dir) + use_apistub = getattr(args, "use_apistub", False) + + # The apistub path builds the code report via static analysis (apistub generates + # api.md and it is converted to a report); the target package is never imported. + # So installing dev requirements and building/installing the package sdist is only + # needed for the default (import-based) path. Skip both in apistub mode. + if not use_apistub: + # install dependencies + self.install_dev_reqs(executable, args, package_dir) try: install_into_venv( @@ -144,17 +151,18 @@ def run(self, args: argparse.Namespace) -> int: results.append(1) continue - create_package_and_install( - distribution_directory=staging_directory, - target_setup=package_dir, - skip_install=False, - cache_dir=None, - work_dir=staging_directory, - force_create=False, - package_type="sdist", - pre_download_disabled=False, - python_executable=executable, - ) + if not use_apistub: + create_package_and_install( + distribution_directory=staging_directory, + target_setup=package_dir, + skip_install=False, + cache_dir=None, + work_dir=staging_directory, + force_create=False, + package_type="sdist", + pre_download_disabled=False, + python_executable=executable, + ) try: cmd = [ diff --git a/eng/tools/azure-sdk-tools/tests/test_breaking.py b/eng/tools/azure-sdk-tools/tests/test_breaking.py new file mode 100644 index 000000000000..1c93f55ef40f --- /dev/null +++ b/eng/tools/azure-sdk-tools/tests/test_breaking.py @@ -0,0 +1,96 @@ +import argparse +import os +import sys + +from unittest.mock import MagicMock, patch + +from azpysdk.breaking import breaking + + +def _make_args(use_apistub=False, changelog=True, isolate=False): + """Build an argparse.Namespace with every attribute breaking.run() reads.""" + return argparse.Namespace( + target=".", + isolate=isolate, + command="breaking", + service=None, + target_module=None, + in_venv=False, + stable_version=None, + changelog=changelog, + code_report=False, + source_report=None, + target_report=None, + latest_pypi_version=False, + use_apistub=use_apistub, + debug=False, + ) + + +def _run_breaking(args, tmp_path): + """Invoke breaking.run() with all external side effects mocked out. + + Returns a dict of the mocks that assertions can inspect. + """ + chk = breaking() + staging = str(tmp_path / "staging") + os.makedirs(staging, exist_ok=True) + fake_parsed = MagicMock() + fake_parsed.folder = str(tmp_path) + fake_parsed.name = "azure-core" + + with patch("azpysdk.breaking.set_envvar_defaults"), patch( + "azpysdk.breaking.install_into_venv" + ) as install_into_venv, patch("azpysdk.breaking.create_package_and_install") as create_package_and_install, patch( + "azpysdk.breaking.check_call" + ) as check_call, patch.object( + chk, "get_targeted_directories", return_value=[fake_parsed] + ), patch.object( + chk, "get_executable", return_value=(sys.executable, staging) + ), patch.object( + chk, "install_dev_reqs" + ) as install_dev_reqs: + result = chk.run(args) + + return { + "result": result, + "install_dev_reqs": install_dev_reqs, + "install_into_venv": install_into_venv, + "create_package_and_install": create_package_and_install, + "check_call": check_call, + } + + +class TestBreakingUseApistubGuard: + """The apistub path builds the report via static analysis, so it must not install the + package's dev requirements nor build/install the target package sdist.""" + + def test_use_apistub_skips_dev_reqs_and_sdist_install(self, tmp_path): + mocks = _run_breaking(_make_args(use_apistub=True), tmp_path) + + # The failing/expensive steps are skipped in apistub mode. + mocks["install_dev_reqs"].assert_not_called() + mocks["create_package_and_install"].assert_not_called() + + # jsondiff + breaking-change checker are still required by the detector. + mocks["install_into_venv"].assert_called_once() + + # The detector still runs, and it receives --use-apistub. + mocks["check_call"].assert_called_once() + detector_cmd = mocks["check_call"].call_args.args[0] + assert "--use-apistub" in detector_cmd + assert mocks["result"] == 0 + + def test_default_path_installs_dev_reqs_and_sdist(self, tmp_path): + mocks = _run_breaking(_make_args(use_apistub=False), tmp_path) + + # The import-based path needs the package (and its deps) installed. + mocks["install_dev_reqs"].assert_called_once() + mocks["create_package_and_install"].assert_called_once() + + mocks["install_into_venv"].assert_called_once() + + mocks["check_call"].assert_called_once() + detector_cmd = mocks["check_call"].call_args.args[0] + assert "--use-apistub" not in detector_cmd + assert mocks["result"] == 0 From 1abdda4b0e8480aa19a12d796cf87fc833e163ba Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 16 Jul 2026 18:37:02 +0800 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- eng/tools/azure-sdk-tools/tests/test_breaking.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/eng/tools/azure-sdk-tools/tests/test_breaking.py b/eng/tools/azure-sdk-tools/tests/test_breaking.py index 1c93f55ef40f..b9968cf69af8 100644 --- a/eng/tools/azure-sdk-tools/tests/test_breaking.py +++ b/eng/tools/azure-sdk-tools/tests/test_breaking.py @@ -50,7 +50,11 @@ def _run_breaking(args, tmp_path): ), patch.object( chk, "install_dev_reqs" ) as install_dev_reqs: - result = chk.run(args) +original_cwd = os.getcwd() + try: + result = chk.run(args) + finally: + os.chdir(original_cwd) return { "result": result, From 2ded0320041fe2a72408e88a4ca34071b3701e1d Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Fri, 17 Jul 2026 09:46:19 +0800 Subject: [PATCH 3/3] Fix indentation syntax error in test_breaking.py --- eng/tools/azure-sdk-tools/tests/test_breaking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/tools/azure-sdk-tools/tests/test_breaking.py b/eng/tools/azure-sdk-tools/tests/test_breaking.py index b9968cf69af8..15049696ee40 100644 --- a/eng/tools/azure-sdk-tools/tests/test_breaking.py +++ b/eng/tools/azure-sdk-tools/tests/test_breaking.py @@ -39,6 +39,7 @@ def _run_breaking(args, tmp_path): fake_parsed.folder = str(tmp_path) fake_parsed.name = "azure-core" + original_cwd = os.getcwd() with patch("azpysdk.breaking.set_envvar_defaults"), patch( "azpysdk.breaking.install_into_venv" ) as install_into_venv, patch("azpysdk.breaking.create_package_and_install") as create_package_and_install, patch( @@ -50,7 +51,6 @@ def _run_breaking(args, tmp_path): ), patch.object( chk, "install_dev_reqs" ) as install_dev_reqs: -original_cwd = os.getcwd() try: result = chk.run(args) finally: