From 349ce77af6596b58bcd7f2c4151c821b1ff943d2 Mon Sep 17 00:00:00 2001 From: Wasim Date: Sat, 17 Oct 2026 16:00:33 +0530 Subject: [PATCH 1/2] fix(testing): propagate PROJECT_ROOT_PATH and PYTHONPATH during test debugging Fixes #26155 When debugging tests (e.g. unittest with custom start directory `-s`), the debug configuration previously dropped `PROJECT_ROOT_PATH` and failed to assign the updated `PYTHONPATH` resulting from `addPathToPythonpath`. In addition, `execution.py` and `discovery.py` now consistently default `top_level_dir` to `PROJECT_ROOT_PATH` while preserving any explicitly specified `-t` option. --- .../tests/unittestadapter/test_execution.py | 42 +++++++++++++++++++ python_files/unittestadapter/discovery.py | 5 ++- python_files/unittestadapter/execution.py | 4 ++ src/client/testing/common/debugLauncher.ts | 6 ++- .../testing/common/debugLauncher.unit.test.ts | 4 ++ 5 files changed, 58 insertions(+), 3 deletions(-) diff --git a/python_files/tests/unittestadapter/test_execution.py b/python_files/tests/unittestadapter/test_execution.py index cab03f0b5dc4..e8d771b9b1b3 100644 --- a/python_files/tests/unittestadapter/test_execution.py +++ b/python_files/tests/unittestadapter/test_execution.py @@ -472,3 +472,45 @@ def test_symlink_with_project_root_path(mock_send_run_data) -> None: # noqa: AR assert actual["cwd"] == os.fsdecode(destination), ( f"CWD does not match symlink path: expected {os.fsdecode(destination)}, got {actual['cwd']}" ) + + +def test_project_root_path_defaults_top_level_dir(mock_send_run_data) -> None: # noqa: ARG001 + """Test that project_root_path acts as top_level_dir during execution when start_dir is a subdir. + + When tests are in a subdirectory (e.g. ./test_outer_folder/test_inner_folder) and top_level_dir + is None, passing project_root_path as top_level_dir ensures the discovered test IDs match + those discovered during the discovery phase. + """ + project_path = TEST_DATA_PATH / "utils_complex_tree" + start_dir = os.fsdecode( + pathlib.PurePath( + TEST_DATA_PATH, + "utils_complex_tree", + "test_outer_folder", + "test_inner_folder", + ) + ) + pattern = "test_*.py" + top_level_dir = os.fsdecode(project_path) + test_ids = [ + "test_outer_folder.test_inner_folder.test_utils_complex_tree.TreeOne.test_one", + ] + + os.environ["TEST_RUN_PIPE"] = "fake" + + actual = run_tests( + start_dir, + test_ids, + pattern, + None, + 1, + None, + project_root_path=top_level_dir, + ) + + assert actual["status"] == "success" + assert actual["cwd"] == os.fsdecode(project_path) + assert actual["result"] is not None + assert test_ids[0] in actual["result"] + assert actual["result"][test_ids[0]]["outcome"] == "success" + diff --git a/python_files/unittestadapter/discovery.py b/python_files/unittestadapter/discovery.py index c864ac76916b..3b8b3da65709 100644 --- a/python_files/unittestadapter/discovery.py +++ b/python_files/unittestadapter/discovery.py @@ -142,9 +142,10 @@ def discover_tests( raise VSCodeUnittestError(error_msg) # noqa: B904 else: # Check for PROJECT_ROOT_PATH environment variable (project-based testing). - # When set, this overrides top_level_dir to root the test tree at the project directory. + # When set, this overrides top_level_dir to root the test tree at the project directory + # if an explicit top-level directory was not provided. project_root_path = os.environ.get("PROJECT_ROOT_PATH") - if project_root_path: + if project_root_path and not top_level_dir: top_level_dir = project_root_path # Perform regular unittest test discovery. diff --git a/python_files/unittestadapter/execution.py b/python_files/unittestadapter/execution.py index 422f246d3476..a19adcc0bb06 100644 --- a/python_files/unittestadapter/execution.py +++ b/python_files/unittestadapter/execution.py @@ -210,6 +210,8 @@ def run_tests( (used for project-based testing to root test tree at project) """ cwd = os.path.abspath(project_root_path or start_dir) # noqa: PTH100 + if top_level_dir is None and project_root_path: + top_level_dir = project_root_path if "/" in start_dir: # is a subdir parent_dir = os.path.dirname(start_dir) # noqa: PTH120 sys.path.insert(0, parent_dir) @@ -373,6 +375,8 @@ def send_run_data(raw_data, test_run_pipe): # Update the module-level variable for send_run_data to use # pylint: disable=global-statement globals()["PROJECT_ROOT_PATH"] = project_root_path + if not top_level_dir: + top_level_dir = project_root_path # Perform regular unittest execution. # Pass project_root_path so the payload's cwd matches the project root. diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 037bfb265088..5bcf89085709 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -311,6 +311,10 @@ export class DebugLauncher implements ITestDebugLauncher { ); } + if (options.project) { + launchArgs.env.PROJECT_ROOT_PATH = options.project.uri.fsPath; + } + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'python_files'); // check if PYTHONPATH is already set in the environment variables if (launchArgs.env) { @@ -322,7 +326,7 @@ export class DebugLauncher implements ITestDebugLauncher { } // add the plugin path or cwd to PYTHONPATH if it is not already there using the following function // this function will handle if PYTHONPATH is undefined - addPathToPythonpath(additionalPythonPath, launchArgs.env.PYTHONPATH); + launchArgs.env.PYTHONPATH = addPathToPythonpath(additionalPythonPath, launchArgs.env.PYTHONPATH); } // Clear out purpose so we can detect if the configuration was used to diff --git a/src/test/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index 86e862103bf6..d56110391a1a 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -793,6 +793,8 @@ suite('Unit Tests - Debug Launcher', () => { expect(capturedConfigs).to.have.length(1); expect(capturedConfigs[0].name).to.equal('Debug Tests: myproject (Python 3.11)'); + expect(capturedConfigs[0].env?.PROJECT_ROOT_PATH).to.equal(Uri.file('one/two/three').fsPath); + expect(capturedConfigs[0].env?.PYTHONPATH).to.include(path.join(EXTENSION_ROOT_DIR, 'python_files')); }); test('should use default python when no project provided', async () => { @@ -812,6 +814,8 @@ suite('Unit Tests - Debug Launcher', () => { expect(capturedConfigs).to.have.length(1); // Should use the default 'python' from interpreterService mock expect(capturedConfigs[0].python).to.equal('python'); + expect(capturedConfigs[0].env?.PROJECT_ROOT_PATH).to.be.undefined; + expect(capturedConfigs[0].env?.PYTHONPATH).to.include(path.join(EXTENSION_ROOT_DIR, 'python_files')); }); test('should add unique session marker to launch config', async () => { From cf0535d97d9fa25b93a70e68810d9a0a2c1cd15f Mon Sep 17 00:00:00 2001 From: Wasim Date: Sun, 18 Oct 2026 17:23:20 +0530 Subject: [PATCH 2/2] style: format test_execution.py with ruff --- python_files/tests/unittestadapter/test_execution.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python_files/tests/unittestadapter/test_execution.py b/python_files/tests/unittestadapter/test_execution.py index e8d771b9b1b3..2482d6069bf0 100644 --- a/python_files/tests/unittestadapter/test_execution.py +++ b/python_files/tests/unittestadapter/test_execution.py @@ -513,4 +513,3 @@ def test_project_root_path_defaults_top_level_dir(mock_send_run_data) -> None: assert actual["result"] is not None assert test_ids[0] in actual["result"] assert actual["result"][test_ids[0]]["outcome"] == "success" -