Skip to content

Commit 45675a4

Browse files
committed
second checkpoint- ignore implemented
1 parent b2a3a8e commit 45675a4

File tree

5 files changed

+157
-1
lines changed

5 files changed

+157
-1
lines changed

python_files/unittestadapter/pvsc_utils.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,44 @@ def get_child_node(name: str, path: str, type_: TestNodeTypeEnum, root: TestNode
166166
return result # type:ignore
167167

168168

169+
# TODO: Unittest nested project exclusion - commented out for now, focusing on pytest first
170+
# def should_exclude_file(test_path: str) -> bool:
171+
# """Check if a test file should be excluded due to nested project ownership.
172+
#
173+
# Reads NESTED_PROJECTS_TO_IGNORE environment variable (JSON array of paths)
174+
# and checks if test_path is under any of those nested project directories.
175+
#
176+
# Args:
177+
# test_path: Absolute path to the test file
178+
#
179+
# Returns:
180+
# True if the file should be excluded, False otherwise
181+
# """
182+
# nested_projects_json = os.getenv("NESTED_PROJECTS_TO_IGNORE")
183+
# if not nested_projects_json:
184+
# return False
185+
#
186+
# try:
187+
# nested_paths = json.loads(nested_projects_json)
188+
# test_path_obj = pathlib.Path(test_path).resolve()
189+
#
190+
# # Check if test file is under any nested project path
191+
# for nested_path in nested_paths:
192+
# nested_path_obj = pathlib.Path(nested_path).resolve()
193+
# try:
194+
# test_path_obj.relative_to(nested_path_obj)
195+
# # If relative_to succeeds, test_path is under nested_path
196+
# return True
197+
# except ValueError:
198+
# # test_path is not under nested_path
199+
# continue
200+
#
201+
# return False
202+
# except Exception:
203+
# # On any error, don't exclude (safer to show tests than hide them)
204+
# return False
205+
206+
169207
def build_test_tree(
170208
suite: unittest.TestSuite, top_level_directory: str
171209
) -> Tuple[Union[TestNode, None], List[str]]:
@@ -251,6 +289,13 @@ def build_test_tree(
251289
# Find/build file node.
252290
path_components = [top_level_directory, *folders, py_filename]
253291
file_path = os.fsdecode(pathlib.PurePath("/".join(path_components)))
292+
293+
# PHASE 4: Check if file should be excluded (nested project ownership)
294+
# TODO: Commented out for now - focusing on pytest implementation first
295+
# if should_exclude_file(file_path):
296+
# # Skip this test - it belongs to a nested project
297+
# continue
298+
254299
current_node = get_child_node(
255300
py_filename, file_path, TestNodeTypeEnum.file, current_node
256301
)

src/client/testing/testController/common/projectAdapter.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ export interface ProjectAdapter {
8585
*/
8686
ownedTests?: DiscoveredTestNode;
8787

88+
/**
89+
* Absolute paths of nested projects to ignore during discovery.
90+
* Used to pass --ignore flags to pytest or exclusion filters to unittest.
91+
* Only populated for parent projects that contain nested child projects.
92+
*/
93+
nestedProjectPathsToIgnore?: string[];
94+
8895
// === LIFECYCLE ===
8996
/**
9097
* Whether discovery is currently running for this project.

src/client/testing/testController/controller.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,62 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
569569
}
570570
}
571571

572+
/**
573+
* Phase 3: Identifies which projects are nested within other projects in the same workspace.
574+
* Returns a map of parent project ID -> array of nested child project paths to ignore.
575+
*
576+
* Example: If ProjectB (alice/bob/) is nested in ProjectA (alice/),
577+
* returns: { "projectA-id": ["alice/bob"] }
578+
*
579+
* Uses simple path prefix matching - a project is nested if its path starts with
580+
* another project's path followed by a path separator.
581+
*/
582+
private computeNestedProjectIgnores(workspaceUri: Uri): Map<string, string[]> {
583+
const projectIgnores = new Map<string, string[]>();
584+
const projects = this.workspaceProjects.get(workspaceUri);
585+
586+
if (!projects || projects.size === 0) {
587+
return projectIgnores;
588+
}
589+
590+
const projectArray = Array.from(projects.values());
591+
592+
// For each project, find all other projects nested within it
593+
for (const parentProject of projectArray) {
594+
const nestedPaths: string[] = [];
595+
596+
for (const potentialChild of projectArray) {
597+
if (parentProject.projectId === potentialChild.projectId) {
598+
continue; // Skip self
599+
}
600+
601+
// Check if child is nested under parent
602+
const parentPath = parentProject.projectUri.fsPath;
603+
const childPath = potentialChild.projectUri.fsPath;
604+
605+
// Use path.sep for cross-platform compatibility (/ on Unix, \\ on Windows)
606+
if (childPath.startsWith(parentPath + path.sep)) {
607+
// Child is nested - add its path for ignoring
608+
nestedPaths.push(childPath);
609+
traceVerbose(
610+
`[test-by-project] Detected nested project: ${potentialChild.projectName} ` +
611+
`(${childPath}) under ${parentProject.projectName} (${parentPath})`,
612+
);
613+
}
614+
}
615+
616+
if (nestedPaths.length > 0) {
617+
projectIgnores.set(parentProject.projectId, nestedPaths);
618+
traceInfo(
619+
`[test-by-project] Project ${parentProject.projectName} will ignore ` +
620+
`${nestedPaths.length} nested project(s)`,
621+
);
622+
}
623+
}
624+
625+
return projectIgnores;
626+
}
627+
572628
/**
573629
* Phase 2: Discovers tests for all projects within a workspace (project-based testing).
574630
* Runs discovery in parallel for all projects and tracks file overlaps for Phase 3.
@@ -596,7 +652,24 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
596652
this.workspaceDiscoveryState.set(workspaceUri, discoveryState);
597653

598654
try {
599-
// Run discovery for all projects in parallel
655+
// PHASE 3: Compute nested project relationships BEFORE discovery
656+
const projectIgnores = this.computeNestedProjectIgnores(workspaceUri);
657+
658+
// Populate each project's ignore list by iterating through projects array directly
659+
for (const project of projects) {
660+
const ignorePaths = projectIgnores.get(project.projectId);
661+
if (ignorePaths && ignorePaths.length > 0) {
662+
project.nestedProjectPathsToIgnore = ignorePaths;
663+
traceInfo(
664+
`[test-by-project] Project ${project.projectName} configured to ignore ${ignorePaths.length} nested project(s): ` +
665+
`${ignorePaths.join(', ')}`,
666+
);
667+
} else {
668+
traceVerbose(`[test-by-project] Project ${project.projectName} has no nested projects to ignore`);
669+
}
670+
}
671+
672+
// Run discovery for all projects in parallel (now with ignore lists populated)
600673
// Each project will populate TestItems independently via existing flow
601674
await Promise.all(projects.map((project) => this.discoverProject(project, discoveryState)));
602675

src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,25 @@ export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter {
7878
let { pytestArgs } = settings.testing;
7979
const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath;
8080
pytestArgs = await handleSymlinkAndRootDir(cwd, pytestArgs);
81+
82+
// PHASE 4: Add --ignore flags for nested projects
83+
traceVerbose(
84+
`[test-by-project] Checking for nested projects to ignore. Project: ${project?.projectName}, ` +
85+
`nestedProjectPathsToIgnore length: ${project?.nestedProjectPathsToIgnore?.length ?? 0}`,
86+
);
87+
if (project?.nestedProjectPathsToIgnore?.length) {
88+
const ignoreArgs = project.nestedProjectPathsToIgnore.map((nestedPath) => `--ignore=${nestedPath}`);
89+
pytestArgs = [...pytestArgs, ...ignoreArgs];
90+
traceInfo(
91+
`[test-by-project] Project ${project.projectName} ignoring ${ignoreArgs.length} ` +
92+
`nested project(s): ${ignoreArgs.join(' ')}`,
93+
);
94+
} else {
95+
traceVerbose(
96+
`[test-by-project] No nested projects to ignore for project: ${project?.projectName ?? 'unknown'}`,
97+
);
98+
}
99+
81100
const commandArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs);
82101
traceVerbose(
83102
`Running pytest discovery with command: ${commandArgs.join(' ')} for workspace ${uri.fsPath}.`,

src/client/testing/testController/unittest/testDiscoveryAdapter.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,18 @@ export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter {
9191
mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath;
9292
}
9393

94+
// PHASE 4: Pass exclusion list via environment variable for unittest
95+
// TODO: unittest doesn't have a built-in --ignore flag like pytest, so we'll need to pass the
96+
// nested project paths via environment and handle filtering in Python-side discovery.py
97+
// Commenting out for now - focusing on pytest implementation first
98+
// if (project?.nestedProjectPathsToIgnore?.length) {
99+
// mutableEnv.NESTED_PROJECTS_TO_IGNORE = JSON.stringify(project.nestedProjectPathsToIgnore);
100+
// traceInfo(
101+
// `[test-by-project] Project ${project.projectName} will exclude ${project.nestedProjectPathsToIgnore.length} ` +
102+
// `nested project(s) in Python-side unittest discovery`
103+
// );
104+
// }
105+
94106
// Setup process handlers (shared by both execution paths)
95107
const handlers = createProcessHandlers('unittest', uri, cwd, this.resultResolver, deferredTillExecClose);
96108

0 commit comments

Comments
 (0)