Skip to content

Commit 6a01851

Browse files
authored
Add smoke tests for functional checks and registration verification (#1225)
fixes #1214
1 parent dab7bc9 commit 6a01851

File tree

4 files changed

+682
-27
lines changed

4 files changed

+682
-27
lines changed

src/extension.ts

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -515,34 +515,44 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
515515
* Below are all the contributed features using the APIs.
516516
*/
517517
setImmediate(async () => {
518-
// This is the finder that is used by all the built in environment managers
519-
const nativeFinder: NativePythonFinder = await createNativePythonFinder(outputChannel, api, context);
520-
context.subscriptions.push(nativeFinder);
521-
const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel);
522-
sysPythonManager.resolve(sysMgr);
523-
await Promise.all([
524-
registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel, sysMgr),
525-
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
526-
registerPyenvFeatures(nativeFinder, context.subscriptions, projectManager),
527-
registerPipenvFeatures(nativeFinder, context.subscriptions, projectManager),
528-
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
529-
shellStartupVarsMgr.initialize(),
530-
]);
531-
532-
await applyInitialEnvironmentSelection(envManagers, projectManager, nativeFinder, api);
533-
534-
// Register manager-agnostic terminal watcher for package-modifying commands
535-
registerTerminalPackageWatcher(api, terminalActivation, outputChannel, context.subscriptions);
536-
537-
// Register listener for interpreter settings changes for interpreter re-selection
538-
context.subscriptions.push(
539-
registerInterpreterSettingsChangeListener(envManagers, projectManager, nativeFinder, api),
540-
);
518+
try {
519+
// This is the finder that is used by all the built in environment managers
520+
const nativeFinder: NativePythonFinder = await createNativePythonFinder(outputChannel, api, context);
521+
context.subscriptions.push(nativeFinder);
522+
const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel);
523+
sysPythonManager.resolve(sysMgr);
524+
await Promise.all([
525+
registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel, sysMgr),
526+
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
527+
registerPyenvFeatures(nativeFinder, context.subscriptions, projectManager),
528+
registerPipenvFeatures(nativeFinder, context.subscriptions, projectManager),
529+
registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager),
530+
shellStartupVarsMgr.initialize(),
531+
]);
532+
533+
await applyInitialEnvironmentSelection(envManagers, projectManager, nativeFinder, api);
534+
535+
// Register manager-agnostic terminal watcher for package-modifying commands
536+
registerTerminalPackageWatcher(api, terminalActivation, outputChannel, context.subscriptions);
537+
538+
// Register listener for interpreter settings changes for interpreter re-selection
539+
context.subscriptions.push(
540+
registerInterpreterSettingsChangeListener(envManagers, projectManager, nativeFinder, api),
541+
);
541542

542-
sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime);
543-
await terminalManager.initialize(api);
544-
sendManagerSelectionTelemetry(projectManager);
545-
await sendProjectStructureTelemetry(projectManager, envManagers);
543+
sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime);
544+
await terminalManager.initialize(api);
545+
sendManagerSelectionTelemetry(projectManager);
546+
await sendProjectStructureTelemetry(projectManager, envManagers);
547+
} catch (error) {
548+
traceError('Failed to initialize environment managers:', error);
549+
// Show a user-friendly error message
550+
window.showErrorMessage(
551+
l10n.t(
552+
'Python Environments: Failed to initialize environment managers. Some features may not work correctly. Check the Output panel for details.',
553+
),
554+
);
555+
}
546556
});
547557

548558
sendTelemetryEvent(EventNames.EXTENSION_ACTIVATION_DURATION, start.elapsedTime);
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
/**
5+
* Smoke Test: Functional Checks
6+
*
7+
* PURPOSE:
8+
* Verify that core extension features actually work, not just that they're registered.
9+
* These tests require Python to be installed and may have side effects.
10+
*
11+
* WHAT THIS TESTS:
12+
* 1. Environment discovery returns results
13+
* 2. Projects API works correctly
14+
* 3. Environment variables API works
15+
* 4. Settings are not polluted on activation
16+
*/
17+
18+
import * as assert from 'assert';
19+
import * as vscode from 'vscode';
20+
import { PythonEnvironmentApi } from '../../api';
21+
import { ENVS_EXTENSION_ID, MAX_EXTENSION_ACTIVATION_TIME } from '../constants';
22+
import { waitForApiReady, waitForCondition } from '../testUtils';
23+
24+
suite('Smoke: Functional Checks', function () {
25+
this.timeout(MAX_EXTENSION_ACTIVATION_TIME);
26+
27+
let api: PythonEnvironmentApi;
28+
let managersReady = false;
29+
30+
suiteSetup(async function () {
31+
const extension = vscode.extensions.getExtension<PythonEnvironmentApi>(ENVS_EXTENSION_ID);
32+
assert.ok(extension, `Extension ${ENVS_EXTENSION_ID} not found`);
33+
34+
if (!extension.isActive) {
35+
await extension.activate();
36+
await waitForCondition(() => extension.isActive, 30_000, 'Extension did not activate');
37+
}
38+
39+
api = extension.exports;
40+
assert.ok(api, 'API not exported');
41+
42+
// Wait for environment managers to register (happens async in setImmediate)
43+
// This may fail in CI if the pet binary is not available
44+
const result = await waitForApiReady(api, 45_000);
45+
managersReady = result.ready;
46+
if (!result.ready) {
47+
console.log(`[WARN] Managers not ready: ${result.error}`);
48+
console.log('[WARN] Tests requiring managers will be skipped');
49+
}
50+
});
51+
52+
// =========================================================================
53+
// ENVIRONMENT DISCOVERY - Core feature must work
54+
// =========================================================================
55+
56+
test('getEnvironments returns an array', async function () {
57+
// Skip if managers aren't ready (e.g., pet binary not available in CI)
58+
if (!managersReady) {
59+
this.skip();
60+
return;
61+
}
62+
63+
// This test verifies discovery machinery works
64+
// Even if no Python is installed, it should return an empty array, not throw
65+
66+
const environments = await api.getEnvironments('all');
67+
68+
assert.ok(Array.isArray(environments), 'getEnvironments("all") should return an array');
69+
});
70+
71+
test('getEnvironments finds Python installations when available', async function () {
72+
// Skip if managers aren't ready (e.g., pet binary not available in CI)
73+
if (!managersReady) {
74+
this.skip();
75+
return;
76+
}
77+
78+
// Skip this test if no Python is expected (CI without Python)
79+
if (process.env.SKIP_PYTHON_TESTS) {
80+
this.skip();
81+
return;
82+
}
83+
84+
const environments = await api.getEnvironments('all');
85+
86+
// On a typical dev machine, we expect at least one Python
87+
// This test may need to be conditional based on CI environment
88+
if (environments.length === 0) {
89+
console.log('[WARN] No Python environments found - is Python installed?');
90+
// Don't fail - just warn. CI may not have Python.
91+
return;
92+
}
93+
94+
// Verify environment structure
95+
const env = environments[0];
96+
assert.ok(env.envId, 'Environment should have envId');
97+
assert.ok(env.envId.id, 'envId.id should be defined');
98+
assert.ok(env.envId.managerId, 'envId.managerId should be defined');
99+
assert.ok(env.name, 'Environment should have a name');
100+
assert.ok(env.version, 'Environment should have a version');
101+
assert.ok(env.environmentPath, 'Environment should have environmentPath');
102+
});
103+
104+
test('getEnvironments with scope "global" returns global interpreters', async function () {
105+
// Skip if managers aren't ready (e.g., pet binary not available in CI)
106+
if (!managersReady) {
107+
this.skip();
108+
return;
109+
}
110+
111+
const globalEnvs = await api.getEnvironments('global');
112+
113+
assert.ok(Array.isArray(globalEnvs), 'getEnvironments("global") should return an array');
114+
115+
// Global environments are system Python installations
116+
// They should be a subset of 'all' environments
117+
const allEnvs = await api.getEnvironments('all');
118+
assert.ok(globalEnvs.length <= allEnvs.length, 'Global environments should be a subset of all environments');
119+
});
120+
121+
test('refreshEnvironments completes without error', async function () {
122+
// Skip if managers aren't ready (e.g., pet binary not available in CI)
123+
if (!managersReady) {
124+
this.skip();
125+
return;
126+
}
127+
128+
// This should not throw
129+
await api.refreshEnvironments(undefined);
130+
131+
// Verify we can still get environments after refresh
132+
const environments = await api.getEnvironments('all');
133+
assert.ok(Array.isArray(environments), 'Should be able to get environments after refresh');
134+
});
135+
136+
// =========================================================================
137+
// PROJECTS - Core project management features
138+
// =========================================================================
139+
140+
test('getPythonProjects returns workspace folders by default', function () {
141+
const projects = api.getPythonProjects();
142+
143+
assert.ok(Array.isArray(projects), 'getPythonProjects should return an array');
144+
145+
// By default, workspace folders are treated as projects
146+
const workspaceFolders = vscode.workspace.workspaceFolders;
147+
if (workspaceFolders && workspaceFolders.length > 0) {
148+
assert.ok(projects.length > 0, 'With workspace folders open, there should be at least one project');
149+
150+
// Verify project structure
151+
const project = projects[0];
152+
assert.ok(project.name, 'Project should have a name');
153+
assert.ok(project.uri, 'Project should have a uri');
154+
}
155+
});
156+
157+
test('getPythonProject returns undefined for non-existent path', function () {
158+
const fakeUri = vscode.Uri.file('/this/path/does/not/exist/anywhere');
159+
const project = api.getPythonProject(fakeUri);
160+
161+
// Should return undefined, not throw
162+
assert.strictEqual(project, undefined, 'getPythonProject should return undefined for non-existent path');
163+
});
164+
165+
// =========================================================================
166+
// ENVIRONMENT SELECTION - Get/Set environment
167+
// =========================================================================
168+
169+
test('getEnvironment returns undefined or a valid environment', async function () {
170+
// Skip if managers aren't ready (e.g., pet binary not available in CI)
171+
if (!managersReady) {
172+
this.skip();
173+
return;
174+
}
175+
176+
// With no explicit selection, may return undefined or auto-selected env
177+
const env = await api.getEnvironment(undefined);
178+
179+
if (env !== undefined) {
180+
// If an environment is returned, verify its structure
181+
assert.ok(env.envId, 'Returned environment should have envId');
182+
assert.ok(env.name, 'Returned environment should have name');
183+
}
184+
// undefined is also valid - no environment selected
185+
});
186+
187+
// =========================================================================
188+
// ENVIRONMENT VARIABLES - .env file support
189+
// =========================================================================
190+
191+
test('getEnvironmentVariables returns an object', async function () {
192+
const envVars = await api.getEnvironmentVariables(undefined);
193+
194+
assert.ok(envVars !== null, 'getEnvironmentVariables should not return null');
195+
assert.ok(typeof envVars === 'object', 'getEnvironmentVariables should return an object');
196+
197+
// Should at least contain PATH or similar system variables
198+
// (merged from process.env by default)
199+
const hasKeys = Object.keys(envVars).length > 0;
200+
assert.ok(hasKeys, 'Environment variables object should have some entries');
201+
});
202+
203+
test('getEnvironmentVariables with workspace uri works', async function () {
204+
const workspaceFolders = vscode.workspace.workspaceFolders;
205+
206+
if (!workspaceFolders || workspaceFolders.length === 0) {
207+
this.skip();
208+
return;
209+
}
210+
211+
const workspaceUri = workspaceFolders[0].uri;
212+
const envVars = await api.getEnvironmentVariables(workspaceUri);
213+
214+
assert.ok(envVars !== null, 'getEnvironmentVariables with workspace uri should not return null');
215+
assert.ok(typeof envVars === 'object', 'Should return an object');
216+
});
217+
218+
// =========================================================================
219+
// RESOLVE ENVIRONMENT - Detailed environment info
220+
// =========================================================================
221+
222+
test('resolveEnvironment handles invalid path gracefully', async function () {
223+
// Skip if managers aren't ready (e.g., pet binary not available in CI)
224+
if (!managersReady) {
225+
this.skip();
226+
return;
227+
}
228+
229+
const fakeUri = vscode.Uri.file('/this/is/not/a/python/installation');
230+
231+
// Should return undefined, not throw
232+
const resolved = await api.resolveEnvironment(fakeUri);
233+
assert.strictEqual(resolved, undefined, 'resolveEnvironment should return undefined for invalid path');
234+
});
235+
236+
test('resolveEnvironment returns full details for valid environment', async function () {
237+
// Skip if managers aren't ready (e.g., pet binary not available in CI)
238+
if (!managersReady) {
239+
this.skip();
240+
return;
241+
}
242+
243+
const environments = await api.getEnvironments('all');
244+
245+
if (environments.length === 0) {
246+
this.skip();
247+
return;
248+
}
249+
250+
// Try to resolve the first environment's path
251+
const env = environments[0];
252+
const resolved = await api.resolveEnvironment(env.environmentPath);
253+
254+
if (resolved) {
255+
// Verify resolved environment has execution info
256+
assert.ok(resolved.execInfo, 'Resolved environment should have execInfo');
257+
assert.ok(resolved.execInfo.run, 'execInfo should have run configuration');
258+
assert.ok(resolved.execInfo.run.executable, 'run should have executable path');
259+
}
260+
});
261+
262+
// =========================================================================
263+
// PACKAGES - Package listing (read-only)
264+
// =========================================================================
265+
266+
test('getPackages returns array or undefined for valid environment', async function () {
267+
// Skip if managers aren't ready (e.g., pet binary not available in CI)
268+
if (!managersReady) {
269+
this.skip();
270+
return;
271+
}
272+
273+
const environments = await api.getEnvironments('all');
274+
275+
if (environments.length === 0) {
276+
this.skip();
277+
return;
278+
}
279+
280+
const env = environments[0];
281+
const packages = await api.getPackages(env);
282+
283+
// Should return array or undefined, not throw
284+
assert.ok(packages === undefined || Array.isArray(packages), 'getPackages should return undefined or an array');
285+
286+
// If packages exist, verify structure
287+
if (packages && packages.length > 0) {
288+
const pkg = packages[0];
289+
assert.ok(pkg.pkgId, 'Package should have pkgId');
290+
assert.ok(pkg.name, 'Package should have name');
291+
}
292+
});
293+
});

0 commit comments

Comments
 (0)