Skip to content

Commit b497a41

Browse files
Copiloteleanorjboyd
andcommitted
Add unit tests for project root finder utility
- Created comprehensive unit tests for projectRootFinder - 8/12 tests passing - helper functions working correctly - Some stub configuration issues with findFiles to be resolved - Core functionality (isFileInProject, findProjectForTestItem) fully tested Co-authored-by: eleanorjboyd <[email protected]>
1 parent 243af23 commit b497a41

3 files changed

Lines changed: 216 additions & 5 deletions

File tree

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
// Licensed under the MIT License.
33

44
import * as path from 'path';
5-
import { Uri, workspace, RelativePattern } from 'vscode';
5+
import { Uri, RelativePattern } from 'vscode';
66
import { traceVerbose } from '../../../logging';
77
import { TestProvider } from '../../types';
8+
import * as workspaceApis from '../../../common/vscodeApis/workspaceApis';
89

910
/**
1011
* Markers that indicate a Python project root for different test frameworks
@@ -49,7 +50,7 @@ export async function findProjectRoots(workspaceUri: Uri, testProvider: TestProv
4950
// Exclude common directories to improve performance
5051
const pattern = `**/${marker}`;
5152
const exclude = '**/node_modules/**,**/.venv/**,**/venv/**,**/__pycache__/**,**/.git/**';
52-
const foundFiles = await workspace.findFiles(
53+
const foundFiles = await workspaceApis.findFiles(
5354
new RelativePattern(workspaceUri, pattern),
5455
exclude,
5556
100, // Limit to 100 projects max

src/client/testing/testController/controller.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import { ITestDebugLauncher } from '../common/types';
5252
import { PythonResultResolver } from './common/resultResolver';
5353
import { onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis';
5454
import { IEnvironmentVariablesProvider } from '../../common/variables/types';
55-
import { findProjectRoots, ProjectRoot, findProjectForTestItem, isFileInProject } from './common/projectRootFinder';
55+
import { findProjectRoots, ProjectRoot, isFileInProject } from './common/projectRootFinder';
5656

5757
// Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types.
5858
type EventPropertyType = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_TRIGGER];
@@ -187,13 +187,13 @@ export class PythonTestController implements ITestController, IExtensionSingleAc
187187

188188
/**
189189
* Creates a test adapter for a specific project root within a workspace.
190-
* @param workspaceUri The workspace folder URI
190+
* @param _workspaceUri The workspace folder URI (for future use)
191191
* @param projectRoot The project root to create adapter for
192192
* @param testProvider The test provider (pytest or unittest)
193193
* @returns A ProjectTestAdapter instance
194194
*/
195195
private createProjectAdapter(
196-
workspaceUri: Uri,
196+
_workspaceUri: Uri,
197197
projectRoot: ProjectRoot,
198198
testProvider: TestProvider,
199199
): ProjectTestAdapter {
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as assert from 'assert';
5+
import * as path from 'path';
6+
import * as sinon from 'sinon';
7+
import { Uri } from 'vscode';
8+
import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis';
9+
import {
10+
findProjectRoots,
11+
isFileInProject,
12+
findProjectForTestItem,
13+
ProjectRoot,
14+
} from '../../../../client/testing/testController/common/projectRootFinder';
15+
16+
suite('Project Root Finder Tests', () => {
17+
let findFilesStub: sinon.SinonStub;
18+
19+
setup(() => {
20+
findFilesStub = sinon.stub(workspaceApis, 'findFiles');
21+
});
22+
23+
teardown(() => {
24+
sinon.restore();
25+
});
26+
27+
suite('findProjectRoots', () => {
28+
test('should return workspace root when no project markers found', async () => {
29+
const workspaceUri = Uri.file('/workspace');
30+
findFilesStub.resolves([]);
31+
32+
const result = await findProjectRoots(workspaceUri, 'pytest');
33+
34+
assert.strictEqual(result.length, 1);
35+
assert.strictEqual(result[0].uri.fsPath, workspaceUri.fsPath);
36+
assert.strictEqual(result[0].markerFile, 'none');
37+
});
38+
39+
test('should detect single pytest project with pytest.ini', async () => {
40+
const workspaceUri = Uri.file('/workspace');
41+
const projectMarker = Uri.file('/workspace/pytest.ini');
42+
43+
let callCount = 0;
44+
findFilesStub.callsFake(() => {
45+
// Return marker on first call (pytest.ini), empty on others
46+
if (callCount++ === 0) {
47+
return Promise.resolve([projectMarker]);
48+
}
49+
return Promise.resolve([]);
50+
});
51+
52+
const result = await findProjectRoots(workspaceUri, 'pytest');
53+
54+
assert.strictEqual(result.length, 1);
55+
assert.strictEqual(result[0].uri.fsPath, workspaceUri.fsPath);
56+
assert.strictEqual(result[0].markerFile, 'pytest.ini');
57+
});
58+
59+
test('should detect multiple projects with different markers', async () => {
60+
const workspaceUri = Uri.file('/workspace');
61+
const project1Marker = Uri.file('/workspace/project1/pyproject.toml');
62+
const project2Marker = Uri.file('/workspace/project2/setup.py');
63+
64+
findFilesStub.callsFake((pattern: any) => {
65+
const patternStr = pattern.pattern || pattern;
66+
if (typeof patternStr === 'string' && patternStr.includes('pyproject.toml')) {
67+
return Promise.resolve([project1Marker]);
68+
} else if (typeof patternStr === 'string' && patternStr.includes('setup.py')) {
69+
return Promise.resolve([project2Marker]);
70+
}
71+
return Promise.resolve([]);
72+
});
73+
74+
const result = await findProjectRoots(workspaceUri, 'pytest');
75+
76+
assert.strictEqual(result.length, 2);
77+
const paths = result.map(p => p.uri.fsPath).sort();
78+
assert.strictEqual(paths[0], path.join(workspaceUri.fsPath, 'project1'));
79+
assert.strictEqual(paths[1], path.join(workspaceUri.fsPath, 'project2'));
80+
});
81+
82+
test('should filter out nested projects', async () => {
83+
const workspaceUri = Uri.file('/workspace');
84+
const parentMarker = Uri.file('/workspace/pyproject.toml');
85+
const nestedMarker = Uri.file('/workspace/subproject/pyproject.toml');
86+
87+
findFilesStub.callsFake((pattern: any) => {
88+
const patternStr = pattern.pattern || pattern;
89+
if (typeof patternStr === 'string' && patternStr.includes('pyproject.toml')) {
90+
return Promise.resolve([parentMarker, nestedMarker]);
91+
}
92+
return Promise.resolve([]);
93+
});
94+
95+
const result = await findProjectRoots(workspaceUri, 'pytest');
96+
97+
// Should only return the parent project, filtering out nested one
98+
assert.strictEqual(result.length, 1);
99+
assert.strictEqual(result[0].uri.fsPath, workspaceUri.fsPath);
100+
assert.strictEqual(result[0].markerFile, 'pyproject.toml');
101+
});
102+
103+
test('should use unittest markers for unittest provider', async () => {
104+
const workspaceUri = Uri.file('/workspace');
105+
const projectMarker = Uri.file('/workspace/setup.py');
106+
107+
findFilesStub.callsFake((pattern: any) => {
108+
const patternStr = pattern.pattern || pattern;
109+
if (typeof patternStr === 'string' && patternStr.includes('setup.py')) {
110+
return Promise.resolve([projectMarker]);
111+
}
112+
return Promise.resolve([]);
113+
});
114+
115+
const result = await findProjectRoots(workspaceUri, 'unittest');
116+
117+
assert.strictEqual(result.length, 1);
118+
assert.strictEqual(result[0].markerFile, 'setup.py');
119+
});
120+
});
121+
122+
suite('isFileInProject', () => {
123+
test('should return true for file in project root', () => {
124+
const projectRoot: ProjectRoot = {
125+
uri: Uri.file('/workspace/project1'),
126+
markerFile: 'pyproject.toml',
127+
};
128+
const filePath = path.join('/workspace', 'project1', 'test_file.py');
129+
130+
const result = isFileInProject(filePath, projectRoot);
131+
132+
assert.strictEqual(result, true);
133+
});
134+
135+
test('should return true for file in subdirectory of project', () => {
136+
const projectRoot: ProjectRoot = {
137+
uri: Uri.file('/workspace/project1'),
138+
markerFile: 'pyproject.toml',
139+
};
140+
const filePath = path.join('/workspace', 'project1', 'subdir', 'test_file.py');
141+
142+
const result = isFileInProject(filePath, projectRoot);
143+
144+
assert.strictEqual(result, true);
145+
});
146+
147+
test('should return false for file outside project', () => {
148+
const projectRoot: ProjectRoot = {
149+
uri: Uri.file('/workspace/project1'),
150+
markerFile: 'pyproject.toml',
151+
};
152+
const filePath = path.join('/workspace', 'project2', 'test_file.py');
153+
154+
const result = isFileInProject(filePath, projectRoot);
155+
156+
assert.strictEqual(result, false);
157+
});
158+
159+
test('should return true for exact project root path', () => {
160+
const projectRoot: ProjectRoot = {
161+
uri: Uri.file('/workspace/project1'),
162+
markerFile: 'pyproject.toml',
163+
};
164+
const filePath = '/workspace/project1';
165+
166+
const result = isFileInProject(filePath, projectRoot);
167+
168+
assert.strictEqual(result, true);
169+
});
170+
});
171+
172+
suite('findProjectForTestItem', () => {
173+
test('should find correct project for test item', () => {
174+
const projects: ProjectRoot[] = [
175+
{ uri: Uri.file('/workspace/project1'), markerFile: 'pyproject.toml' },
176+
{ uri: Uri.file('/workspace/project2'), markerFile: 'setup.py' },
177+
];
178+
const testItemUri = Uri.file('/workspace/project1/tests/test_foo.py');
179+
180+
const result = findProjectForTestItem(testItemUri, projects);
181+
182+
assert.ok(result);
183+
assert.strictEqual(result.uri.fsPath, projects[0].uri.fsPath);
184+
});
185+
186+
test('should return deepest matching project for nested structure', () => {
187+
const projects: ProjectRoot[] = [
188+
{ uri: Uri.file('/workspace'), markerFile: 'pyproject.toml' },
189+
{ uri: Uri.file('/workspace/subproject'), markerFile: 'pyproject.toml' },
190+
];
191+
const testItemUri = Uri.file('/workspace/subproject/tests/test_foo.py');
192+
193+
const result = findProjectForTestItem(testItemUri, projects);
194+
195+
assert.ok(result);
196+
assert.strictEqual(result.uri.fsPath, projects[1].uri.fsPath);
197+
});
198+
199+
test('should return undefined when no matching project found', () => {
200+
const projects: ProjectRoot[] = [
201+
{ uri: Uri.file('/workspace/project1'), markerFile: 'pyproject.toml' },
202+
];
203+
const testItemUri = Uri.file('/different/workspace/test_foo.py');
204+
205+
const result = findProjectForTestItem(testItemUri, projects);
206+
207+
assert.strictEqual(result, undefined);
208+
});
209+
});
210+
});

0 commit comments

Comments
 (0)