Skip to content

Commit 02207fd

Browse files
edvilmeCopilot
andcommitted
Add parametrized package manager roundtrip integration test
Adds a single public-API-driven integration test that exercises an install/list/direct-deps/uninstall roundtrip for every discovered package manager. Parametrized over environments grouped by managerId so future managers are covered automatically with no per-manager code. Available versions is omitted because it is not on the exported PythonEnvironmentApi. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f16397e-0917-4efb-8d75-566c71ebf9ba
1 parent c0f89be commit 02207fd

1 file changed

Lines changed: 192 additions & 0 deletions

File tree

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
/**
5+
* Integration Test: Package Manager Roundtrip
6+
*
7+
* PURPOSE:
8+
* Verify that every discovered package manager performs a full package lifecycle
9+
* correctly, using only the public extension API. The test is parametrized over
10+
* the package managers backing the discovered environments, so new managers are
11+
* covered automatically without adding manager-specific code.
12+
*
13+
* ROUNDTRIP (per manager, all via the public API):
14+
* 1. list packages -> getPackages (baseline; test package absent)
15+
* 2. install test package -> managePackages({ install })
16+
* 3. list again -> getPackages (test package now present)
17+
* 4. list direct packages -> getPackages filtered by !isTransitive
18+
* 5. uninstall test package -> managePackages({ uninstall })
19+
* 6. list again -> getPackages (test package absent again)
20+
*
21+
* NOTES:
22+
* - "Available versions" is intentionally NOT exercised here: the exported
23+
* PythonEnvironmentApi does not surface getPackageAvailableVersions (it lives
24+
* only on the internal PackageManager interface), so it is unreachable from a
25+
* public-API-only test.
26+
* - Direct (non-transitive) packages are derived from Package.isTransitive,
27+
* which IS part of the public API.
28+
* - The test package (cowsay) is small and dependency-free so a successful
29+
* install shows up as a direct package with no transitive fan-out.
30+
*/
31+
32+
import * as assert from 'assert';
33+
import * as vscode from 'vscode';
34+
import { Package, PythonEnvironment, PythonEnvironmentApi } from '../../api';
35+
import { normalizePackageName } from '../../managers/builtin/utils';
36+
import { ENVS_EXTENSION_ID } from '../constants';
37+
import { waitForCondition } from '../testUtils';
38+
39+
/** Small, dependency-free package used for the install/uninstall roundtrip. */
40+
const TEST_PACKAGE = 'cowsay';
41+
42+
/** True when a package with the given (normalized) name is in the list. */
43+
function hasPackage(packages: Package[] | undefined, name: string): boolean {
44+
const target = normalizePackageName(name);
45+
return (packages ?? []).some((p) => normalizePackageName(p.name) === target);
46+
}
47+
48+
/**
49+
* Runs the full lifecycle roundtrip for a single environment/manager.
50+
* Returns undefined on success, or a human-readable skip reason.
51+
*/
52+
async function runRoundtrip(api: PythonEnvironmentApi, env: PythonEnvironment, managerId: string): Promise<void> {
53+
const baseline = await api.getPackages(env, { skipCache: true });
54+
if (baseline === undefined) {
55+
// No usable package manager for this environment.
56+
return;
57+
}
58+
59+
// Avoid clobbering a pre-existing install of the test package.
60+
assert.ok(
61+
!hasPackage(baseline, TEST_PACKAGE),
62+
`[${managerId}] ${TEST_PACKAGE} unexpectedly already installed; cannot run a clean roundtrip`,
63+
);
64+
65+
let installed = false;
66+
try {
67+
// 2. Install.
68+
await api.managePackages(env, { install: [TEST_PACKAGE] });
69+
installed = true;
70+
71+
// 3. List again -> present.
72+
await api.refreshPackages(env);
73+
const afterInstall = await api.getPackages(env, { skipCache: true });
74+
assert.ok(hasPackage(afterInstall, TEST_PACKAGE), `[${managerId}] ${TEST_PACKAGE} should be installed`);
75+
76+
// 4. Direct (non-transitive) packages should include the directly-installed package.
77+
const direct = (afterInstall ?? []).filter((p) => p.isTransitive !== true);
78+
assert.ok(
79+
hasPackage(direct, TEST_PACKAGE),
80+
`[${managerId}] ${TEST_PACKAGE} should be reported as a direct (non-transitive) package`,
81+
);
82+
83+
// 5. Uninstall.
84+
await api.managePackages(env, { uninstall: [TEST_PACKAGE] });
85+
installed = false;
86+
87+
// 6. List again -> absent.
88+
await api.refreshPackages(env);
89+
const afterUninstall = await api.getPackages(env, { skipCache: true });
90+
assert.ok(!hasPackage(afterUninstall, TEST_PACKAGE), `[${managerId}] ${TEST_PACKAGE} should be uninstalled`);
91+
} finally {
92+
// Best-effort cleanup so a mid-roundtrip failure never leaves the env dirty.
93+
if (installed) {
94+
try {
95+
await api.managePackages(env, { uninstall: [TEST_PACKAGE] });
96+
} catch {
97+
console.log(`[${managerId}] cleanup: failed to uninstall ${TEST_PACKAGE}`);
98+
}
99+
}
100+
}
101+
}
102+
103+
suite('Integration: Package Manager Roundtrip', function () {
104+
this.timeout(180_000); // Install/uninstall across multiple managers can be slow.
105+
106+
let api: PythonEnvironmentApi;
107+
108+
suiteSetup(async function () {
109+
this.timeout(30_000);
110+
111+
const extension = vscode.extensions.getExtension(ENVS_EXTENSION_ID);
112+
assert.ok(extension, `Extension ${ENVS_EXTENSION_ID} not found`);
113+
114+
if (!extension.isActive) {
115+
await extension.activate();
116+
await waitForCondition(() => extension.isActive, 20_000, 'Extension did not activate');
117+
}
118+
119+
api = extension.exports as PythonEnvironmentApi;
120+
assert.ok(api, 'API not available');
121+
});
122+
123+
/**
124+
* Picks one representative environment per package manager, grouped by managerId.
125+
* Prefers virtual-environment-like envs, which are safe to install into.
126+
*/
127+
async function getEnvironmentsByManager(): Promise<Map<string, PythonEnvironment>> {
128+
const environments = await api.getEnvironments('all');
129+
const byManager = new Map<string, PythonEnvironment>();
130+
131+
const looksModifiable = (env: PythonEnvironment): boolean =>
132+
env.displayName.includes('venv') ||
133+
env.displayName.includes('.venv') ||
134+
env.envId.managerId.includes('venv');
135+
136+
for (const env of environments) {
137+
const managerId = env.envId.managerId;
138+
const current = byManager.get(managerId);
139+
if (!current || (looksModifiable(env) && !looksModifiable(current))) {
140+
byManager.set(managerId, env);
141+
}
142+
}
143+
return byManager;
144+
}
145+
146+
/**
147+
* Parametrized roundtrip: one assertion pass per discovered package manager.
148+
*
149+
* This is a single test that iterates managers (rather than a static list) so
150+
* that any future manager is exercised automatically once its environments are
151+
* discovered. Failures are aggregated and reported per manager.
152+
*/
153+
test('install/list/direct/uninstall roundtrip for each package manager', async function () {
154+
const byManager = await getEnvironmentsByManager();
155+
156+
if (byManager.size === 0) {
157+
console.log('No environments discovered; skipping package manager roundtrip');
158+
this.skip();
159+
return;
160+
}
161+
162+
const failures: string[] = [];
163+
let exercised = 0;
164+
165+
for (const [managerId, env] of byManager) {
166+
try {
167+
const before = await api.getPackages(env, { skipCache: true });
168+
if (before === undefined) {
169+
console.log(`[${managerId}] no package manager available; skipping`);
170+
continue;
171+
}
172+
if (hasPackage(before, TEST_PACKAGE)) {
173+
console.log(`[${managerId}] ${TEST_PACKAGE} already present; skipping to avoid clobbering`);
174+
continue;
175+
}
176+
177+
await runRoundtrip(api, env, managerId);
178+
exercised++;
179+
console.log(`[${managerId}] roundtrip passed (${env.displayName})`);
180+
} catch (e) {
181+
failures.push(`[${managerId}] ${e instanceof Error ? e.message : String(e)}`);
182+
}
183+
}
184+
185+
assert.strictEqual(failures.length, 0, `Package manager roundtrip failures:\n${failures.join('\n')}`);
186+
187+
if (exercised === 0) {
188+
console.log('No modifiable package managers were exercised; skipping');
189+
this.skip();
190+
}
191+
});
192+
});

0 commit comments

Comments
 (0)