|
| 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