Skip to content

Commit 28b34dc

Browse files
committed
tests for controller
1 parent cf2e75c commit 28b34dc

File tree

1 file changed

+268
-0
lines changed

1 file changed

+268
-0
lines changed
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as assert from 'assert';
5+
import * as sinon from 'sinon';
6+
import type { TestController, Uri } from 'vscode';
7+
8+
// We must mutate the actual mocked vscode module export (not an __importStar copy),
9+
// otherwise `tests.createTestController` will still be undefined inside the controller module.
10+
// eslint-disable-next-line @typescript-eslint/no-var-requires
11+
const vscodeApi = require('vscode') as typeof import('vscode');
12+
13+
import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants';
14+
import * as envExtApiInternal from '../../../client/envExt/api.internal';
15+
import { getProjectId } from '../../../client/testing/testController/common/projectUtils';
16+
17+
function createStubTestController(): TestController {
18+
const disposable = { dispose: () => undefined };
19+
20+
const controller = ({
21+
items: {
22+
forEach: sinon.stub(),
23+
get: sinon.stub(),
24+
add: sinon.stub(),
25+
replace: sinon.stub(),
26+
delete: sinon.stub(),
27+
size: 0,
28+
[Symbol.iterator]: sinon.stub(),
29+
},
30+
createRunProfile: sinon.stub().returns(disposable),
31+
createTestItem: sinon.stub(),
32+
dispose: sinon.stub(),
33+
resolveHandler: undefined,
34+
refreshHandler: undefined,
35+
} as unknown) as TestController;
36+
37+
return controller;
38+
}
39+
40+
function ensureVscodeTestsNamespace(): void {
41+
const vscodeAny = vscodeApi as any;
42+
if (!vscodeAny.tests) {
43+
vscodeAny.tests = {};
44+
}
45+
if (!vscodeAny.tests.createTestController) {
46+
vscodeAny.tests.createTestController = () => createStubTestController();
47+
}
48+
}
49+
50+
// NOTE:
51+
// `PythonTestController` calls `vscode.tests.createTestController(...)` in its constructor.
52+
// In unit tests, `vscode` is a mocked module (see `src/test/vscode-mock.ts`) and it does not
53+
// provide the `tests` namespace by default. If we import the controller normally, the module
54+
// will be evaluated before this file runs (ES imports are hoisted), and construction will
55+
// crash with `tests`/`createTestController` being undefined.
56+
//
57+
// To keep this test isolated (without changing production code), we:
58+
// 1) Patch the mocked vscode export to provide `tests.createTestController`.
59+
// 2) Require the controller module *after* patching so the constructor can run safely.
60+
ensureVscodeTestsNamespace();
61+
62+
// Dynamically require AFTER the vscode.tests namespace exists.
63+
// eslint-disable-next-line @typescript-eslint/no-var-requires
64+
const { PythonTestController } = require('../../../client/testing/testController/controller');
65+
66+
suite('PythonTestController', () => {
67+
let sandbox: sinon.SinonSandbox;
68+
69+
setup(() => {
70+
sandbox = sinon.createSandbox();
71+
});
72+
73+
teardown(() => {
74+
sandbox.restore();
75+
});
76+
77+
function createController(options?: { unittestEnabled?: boolean; interpreter?: any }): any {
78+
const unittestEnabled = options?.unittestEnabled ?? false;
79+
const interpreter =
80+
options?.interpreter ??
81+
({
82+
displayName: 'Python 3.11',
83+
path: '/usr/bin/python3',
84+
version: { raw: '3.11.8' },
85+
sysPrefix: '/usr',
86+
} as any);
87+
88+
const workspaceService = ({ workspaceFolders: [] } as unknown) as any;
89+
const configSettings = ({
90+
getSettings: sandbox.stub().returns({
91+
testing: {
92+
unittestEnabled,
93+
autoTestDiscoverOnSaveEnabled: false,
94+
},
95+
}),
96+
} as unknown) as any;
97+
98+
const pytest = ({} as unknown) as any;
99+
const unittest = ({} as unknown) as any;
100+
const disposables: any[] = [];
101+
const interpreterService = ({
102+
getActiveInterpreter: sandbox.stub().resolves(interpreter),
103+
} as unknown) as any;
104+
105+
const commandManager = ({
106+
registerCommand: sandbox.stub().returns({ dispose: () => undefined }),
107+
} as unknown) as any;
108+
const pythonExecFactory = ({} as unknown) as any;
109+
const debugLauncher = ({} as unknown) as any;
110+
const envVarsService = ({} as unknown) as any;
111+
112+
return new PythonTestController(
113+
workspaceService,
114+
configSettings,
115+
pytest,
116+
unittest,
117+
disposables,
118+
interpreterService,
119+
commandManager,
120+
pythonExecFactory,
121+
debugLauncher,
122+
envVarsService,
123+
);
124+
}
125+
126+
suite('getTestProvider', () => {
127+
test('returns unittest when enabled', () => {
128+
const controller = createController({ unittestEnabled: true });
129+
const workspaceUri: Uri = vscodeApi.Uri.file('/workspace');
130+
131+
const provider = (controller as any).getTestProvider(workspaceUri);
132+
133+
assert.strictEqual(provider, UNITTEST_PROVIDER);
134+
});
135+
136+
test('returns pytest when unittest not enabled', () => {
137+
const controller = createController({ unittestEnabled: false });
138+
const workspaceUri: Uri = vscodeApi.Uri.file('/workspace');
139+
140+
const provider = (controller as any).getTestProvider(workspaceUri);
141+
142+
assert.strictEqual(provider, PYTEST_PROVIDER);
143+
});
144+
});
145+
146+
suite('createDefaultProject', () => {
147+
test('creates a single default project using active interpreter', async () => {
148+
const workspaceUri: Uri = vscodeApi.Uri.file('/workspace/myws');
149+
const interpreter = {
150+
displayName: 'My Python',
151+
path: '/opt/py/bin/python',
152+
version: { raw: '3.12.1' },
153+
sysPrefix: '/opt/py',
154+
};
155+
156+
const controller = createController({ unittestEnabled: false, interpreter });
157+
158+
const fakeDiscoveryAdapter = { kind: 'discovery' };
159+
const fakeExecutionAdapter = { kind: 'execution' };
160+
sandbox
161+
.stub(controller as any, 'createTestAdapters')
162+
.returns({ discoveryAdapter: fakeDiscoveryAdapter, executionAdapter: fakeExecutionAdapter });
163+
164+
const project = await (controller as any).createDefaultProject(workspaceUri);
165+
166+
assert.strictEqual(project.workspaceUri.toString(), workspaceUri.toString());
167+
assert.strictEqual(project.projectUri.toString(), workspaceUri.toString());
168+
assert.strictEqual(project.projectId, getProjectId(workspaceUri));
169+
assert.strictEqual(project.projectName, 'myws');
170+
171+
assert.strictEqual(project.testProvider, PYTEST_PROVIDER);
172+
assert.strictEqual(project.discoveryAdapter, fakeDiscoveryAdapter);
173+
assert.strictEqual(project.executionAdapter, fakeExecutionAdapter);
174+
175+
assert.strictEqual(project.pythonProject.uri.toString(), workspaceUri.toString());
176+
assert.strictEqual(project.pythonProject.name, 'myws');
177+
178+
assert.strictEqual(project.pythonEnvironment.displayName, 'My Python');
179+
assert.strictEqual(project.pythonEnvironment.version, '3.12.1');
180+
assert.strictEqual(project.pythonEnvironment.execInfo.run.executable, '/opt/py/bin/python');
181+
});
182+
});
183+
184+
suite('discoverWorkspaceProjects', () => {
185+
test('respects useEnvExtension() == false and falls back to single default project', async () => {
186+
const controller = createController();
187+
const workspaceUri: Uri = vscodeApi.Uri.file('/workspace/a');
188+
189+
const defaultProject = { projectId: 'default', projectUri: workspaceUri };
190+
const createDefaultProjectStub = sandbox
191+
.stub(controller as any, 'createDefaultProject')
192+
.resolves(defaultProject as any);
193+
194+
const useEnvExtensionStub = sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false);
195+
const getEnvExtApiStub = sandbox.stub(envExtApiInternal, 'getEnvExtApi');
196+
197+
const projects = await (controller as any).discoverWorkspaceProjects(workspaceUri);
198+
199+
assert.strictEqual(useEnvExtensionStub.called, true);
200+
assert.strictEqual(getEnvExtApiStub.notCalled, true);
201+
assert.strictEqual(createDefaultProjectStub.calledOnceWithExactly(workspaceUri), true);
202+
assert.deepStrictEqual(projects, [defaultProject]);
203+
});
204+
205+
test('filters Python projects to workspace and creates adapters for each', async () => {
206+
const controller = createController();
207+
const workspaceUri: Uri = vscodeApi.Uri.file('/workspace/root');
208+
209+
const pythonProjects = [
210+
{ name: 'p1', uri: vscodeApi.Uri.file('/workspace/root/p1') },
211+
{ name: 'p2', uri: vscodeApi.Uri.file('/workspace/root/nested/p2') },
212+
{ name: 'other', uri: vscodeApi.Uri.file('/other/root/p3') },
213+
];
214+
215+
sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true);
216+
sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({
217+
getPythonProjects: () => pythonProjects,
218+
} as any);
219+
220+
const createdAdapters = [
221+
{ projectId: 'p1', projectUri: pythonProjects[0].uri },
222+
{ projectId: 'p2', projectUri: pythonProjects[1].uri },
223+
];
224+
225+
const createProjectAdapterStub = sandbox
226+
.stub(controller as any, 'createProjectAdapter')
227+
.onFirstCall()
228+
.resolves(createdAdapters[0] as any)
229+
.onSecondCall()
230+
.resolves(createdAdapters[1] as any);
231+
232+
const createDefaultProjectStub = sandbox.stub(controller as any, 'createDefaultProject');
233+
234+
const projects = await (controller as any).discoverWorkspaceProjects(workspaceUri);
235+
236+
// Should only create adapters for the 2 projects in the workspace.
237+
assert.strictEqual(createProjectAdapterStub.callCount, 2);
238+
assert.strictEqual(createProjectAdapterStub.firstCall.args[0].uri.fsPath, '/workspace/root/p1');
239+
assert.strictEqual(createProjectAdapterStub.secondCall.args[0].uri.fsPath, '/workspace/root/nested/p2');
240+
241+
assert.strictEqual(createDefaultProjectStub.notCalled, true);
242+
assert.deepStrictEqual(projects, createdAdapters);
243+
});
244+
245+
test('falls back to default project when no projects are in the workspace', async () => {
246+
const controller = createController();
247+
const workspaceUri: Uri = vscodeApi.Uri.file('/workspace/root');
248+
249+
sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true);
250+
sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({
251+
getPythonProjects: () => [{ name: 'other', uri: vscodeApi.Uri.file('/other/root/p3') }],
252+
} as any);
253+
254+
const defaultProject = { projectId: 'default', projectUri: workspaceUri };
255+
const createDefaultProjectStub = sandbox
256+
.stub(controller as any, 'createDefaultProject')
257+
.resolves(defaultProject as any);
258+
259+
const createProjectAdapterStub = sandbox.stub(controller as any, 'createProjectAdapter');
260+
261+
const projects = await (controller as any).discoverWorkspaceProjects(workspaceUri);
262+
263+
assert.strictEqual(createProjectAdapterStub.notCalled, true);
264+
assert.strictEqual(createDefaultProjectStub.calledOnceWithExactly(workspaceUri), true);
265+
assert.deepStrictEqual(projects, [defaultProject]);
266+
});
267+
});
268+
});

0 commit comments

Comments
 (0)