Skip to content

Commit 8137950

Browse files
edvilmeCopilotCopilot
authored
test: expand package manager integration coverage (#1717)
Follow-up to #1704. Adds shared conformance coverage ensuring Pip, Conda, and Poetry do not prompt or show error UI during headless package operations, while failures still reject. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3fd1a810-6840-4ac9-ac33-c8a9fda4bfc4 Copilot-Session: edcaaa35-9a42-4351-98fa-55bcf8f1968e
1 parent a891360 commit 8137950

18 files changed

Lines changed: 1195 additions & 250 deletions

‎.github/instructions/testing-workflow.instructions.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ This guide covers the full testing lifecycle:
1616
4. **🛠️ Fixing Problems** - Resolve compilation and runtime issues
1717
5. **✅ Validation** - Ensure coverage and resilience
1818

19+
## Learnings
20+
21+
- Pip commands that return JSON must pass `--disable-pip-version-check`; the process helper combines stderr with stdout, so update notices can otherwise make valid JSON unparseable (1).
22+
1923
### When to Use This Guide
2024

2125
**User Requests Testing:**

‎.github/workflows/pr-check.yml‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,9 @@ jobs:
252252
name: Integration Tests
253253
runs-on: ${{ matrix.os }}
254254
needs: [smoke-tests]
255+
env:
256+
CONDA_PLUGINS_AUTO_ACCEPT_TOS: 'true'
257+
VSC_PYTHON_PACKAGE_NETWORK_TEST: '1'
255258
strategy:
256259
fail-fast: false
257260
matrix:
@@ -325,6 +328,12 @@ jobs:
325328
- name: Compile Tests
326329
run: npm run compile-tests
327330

331+
- name: Set up Conda
332+
uses: conda-incubator/setup-miniconda@fc2d68f6413eb2d87b895e92f8584b5b94a10167 # v3
333+
with:
334+
activate-environment: ''
335+
auto-activate: false
336+
328337
- name: Run Integration Tests (Linux)
329338
if: runner.os == 'Linux'
330339
uses: GabrielBB/xvfb-action@86d97bde4a65fe9b290c0b3fb92c2c4ed0e5302d # v1.6
@@ -335,14 +344,6 @@ jobs:
335344
if: runner.os != 'Linux'
336345
run: npm run integration-test
337346

338-
- name: Run Package Manager Network Integration Tests
339-
if: runner.os == 'Linux' && matrix.python-version == '3.12'
340-
uses: GabrielBB/xvfb-action@86d97bde4a65fe9b290c0b3fb92c2c4ed0e5302d # v1.6
341-
env:
342-
VSC_PYTHON_PACKAGE_NETWORK_TEST: '1'
343-
with:
344-
run: npm run integration-test -- --grep "Package Manager"
345-
346347
integration-tests-multiroot:
347348
name: Integration Tests (Multi-Root)
348349
runs-on: ${{ matrix.os }}

‎.github/workflows/push-check.yml‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,9 @@ jobs:
253253
name: Integration Tests
254254
runs-on: ${{ matrix.os }}
255255
needs: [smoke-tests]
256+
env:
257+
CONDA_PLUGINS_AUTO_ACCEPT_TOS: 'true'
258+
VSC_PYTHON_PACKAGE_NETWORK_TEST: '1'
256259
strategy:
257260
fail-fast: false
258261
matrix:
@@ -326,6 +329,12 @@ jobs:
326329
- name: Compile Tests
327330
run: npm run compile-tests
328331

332+
- name: Set up Conda
333+
uses: conda-incubator/setup-miniconda@fc2d68f6413eb2d87b895e92f8584b5b94a10167 # v3
334+
with:
335+
activate-environment: ''
336+
auto-activate: false
337+
329338
- name: Run Integration Tests (Linux)
330339
if: runner.os == 'Linux'
331340
uses: GabrielBB/xvfb-action@86d97bde4a65fe9b290c0b3fb92c2c4ed0e5302d # v1.6
@@ -335,11 +344,3 @@ jobs:
335344
- name: Run Integration Tests (non-Linux)
336345
if: runner.os != 'Linux'
337346
run: npm run integration-test
338-
339-
- name: Run Package Manager Network Integration Tests
340-
if: runner.os == 'Linux' && matrix.python-version == '3.12'
341-
uses: GabrielBB/xvfb-action@86d97bde4a65fe9b290c0b3fb92c2c4ed0e5302d # v1.6
342-
env:
343-
VSC_PYTHON_PACKAGE_NETWORK_TEST: '1'
344-
with:
345-
run: npm run integration-test -- --grep "Package Manager"

‎src/extension.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,16 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
276276
commands.registerCommand('python-envs.test.getPackageManagerIds', () =>
277277
envManagers.packageManagers.map((manager) => manager.id),
278278
),
279+
commands.registerCommand(
280+
'python-envs.test.resolveEnvironmentWithManager',
281+
async (managerId: string, environmentUri: Uri) => {
282+
const manager = envManagers.getEnvironmentManager(managerId);
283+
if (!manager) {
284+
throw new Error(`Environment manager not found: ${managerId}`);
285+
}
286+
return manager.resolve(environmentUri);
287+
},
288+
),
279289
commands.registerCommand(
280290
'python-envs.test.getDirectPackageNames',
281291
async (environment: PythonEnvironment) => {

‎src/managers/builtin/pipPackageManager.ts‎

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import type { Pep440Version } from '@renovatebot/pep440';
22
import { compare, explain as parse, rcompare } from '@renovatebot/pep440';
33
import {
44
CancellationError,
5+
CancellationToken,
56
Disposable,
67
Event,
78
EventEmitter,
89
LogOutputChannel,
910
MarkdownString,
1011
ProgressLocation,
1112
ThemeIcon,
12-
window,
1313
} from 'vscode';
1414
import {
1515
DidChangePackagesEventArgs,
@@ -22,6 +22,7 @@ import {
2222
PythonEnvironment,
2323
PythonEnvironmentApi,
2424
} from '../../api';
25+
import { showErrorMessage, withProgress } from '../../common/window.apis';
2526
import { updatePackagesAndNotify } from '../common/packageChanges';
2627
import { runPython, runUV, shouldUseUv } from './helpers';
2728
import { getWorkspacePackagesToInstall } from './pipUtils';
@@ -75,45 +76,52 @@ export class PipPackageManager implements PackageManager, Disposable {
7576
install: toInstall,
7677
uninstall: toUninstall,
7778
};
78-
await window.withProgress(
79+
const execute = async (token?: CancellationToken): Promise<void> => {
80+
try {
81+
await managePackages(environment, manageOptions, this, token);
82+
await updatePackagesAndNotify(
83+
this,
84+
environment,
85+
this.packages.get(environment.envId.id),
86+
(changes) => {
87+
this._onDidChangePackages.fire({ environment, manager: this, changes });
88+
},
89+
() => this.fetchPackages(environment, !manageOptions.runHeadless),
90+
);
91+
} catch (e) {
92+
if (e instanceof CancellationError) {
93+
throw e;
94+
}
95+
this.log.error('Error managing packages', e);
96+
if (!manageOptions.runHeadless) {
97+
setImmediate(async () => {
98+
const result = await showErrorMessage('Error managing packages', 'View Output');
99+
if (result === 'View Output') {
100+
this.log.show();
101+
}
102+
});
103+
}
104+
throw e;
105+
}
106+
};
107+
108+
if (manageOptions.runHeadless) {
109+
await execute();
110+
return;
111+
}
112+
113+
await withProgress(
79114
{
80115
location: ProgressLocation.Notification,
81116
title: 'Installing packages',
82117
cancellable: true,
83118
},
84-
async (_progress, token) => {
85-
try {
86-
await managePackages(environment, manageOptions, this, token);
87-
await updatePackagesAndNotify(
88-
this,
89-
environment,
90-
this.packages.get(environment.envId.id),
91-
(changes) => {
92-
this._onDidChangePackages.fire({ environment, manager: this, changes });
93-
},
94-
() => this.fetchPackages(environment, !manageOptions.runHeadless),
95-
);
96-
} catch (e) {
97-
if (e instanceof CancellationError) {
98-
throw e;
99-
}
100-
this.log.error('Error managing packages', e);
101-
if (!manageOptions.runHeadless) {
102-
setImmediate(async () => {
103-
const result = await window.showErrorMessage('Error managing packages', 'View Output');
104-
if (result === 'View Output') {
105-
this.log.show();
106-
}
107-
});
108-
}
109-
throw e;
110-
}
111-
},
119+
async (_progress, token) => execute(token),
112120
);
113121
}
114122

115123
async refresh(environment: PythonEnvironment): Promise<void> {
116-
await window.withProgress(
124+
await withProgress(
117125
{
118126
location: ProgressLocation.Window,
119127
title: 'Refreshing packages',

‎src/managers/builtin/utils.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ async function execPipList(environment: PythonEnvironment, log?: LogOutputChanne
200200
try {
201201
return await runPython(
202202
environment.execInfo.run.executable,
203-
['-m', 'pip', 'list', '--format=json', ...(args ?? [])],
203+
['-m', 'pip', 'list', '--format=json', '--disable-pip-version-check', ...(args ?? [])],
204204
undefined,
205205
log,
206206
undefined,

‎src/managers/conda/condaPackageManager.ts‎

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { explain as parse, rcompare } from '@renovatebot/pep440';
33
import * as path from 'path';
44
import {
55
CancellationError,
6+
CancellationToken,
67
Disposable,
78
Event,
89
EventEmitter,
@@ -72,37 +73,44 @@ export class CondaPackageManager implements PackageManager, Disposable {
7273
install: toInstall,
7374
uninstall: toUninstall,
7475
};
76+
const execute = async (token?: CancellationToken): Promise<void> => {
77+
try {
78+
await managePackages(environment, manageOptions, token, this.log);
79+
await updatePackagesAndNotify(
80+
this,
81+
environment,
82+
this.packages.get(environment.envId.id),
83+
(changes) => {
84+
this._onDidChangePackages.fire({ environment, manager: this, changes });
85+
},
86+
);
87+
} catch (e) {
88+
if (e instanceof CancellationError) {
89+
throw e;
90+
}
91+
92+
this.log.error('Error installing packages', e);
93+
if (!manageOptions.runHeadless) {
94+
setImmediate(async () => {
95+
await showErrorMessageWithLogs(CondaStrings.condaInstallError, this.log);
96+
});
97+
}
98+
throw e;
99+
}
100+
};
101+
102+
if (manageOptions.runHeadless) {
103+
await execute();
104+
return;
105+
}
106+
75107
await withProgress(
76108
{
77109
location: ProgressLocation.Notification,
78110
title: CondaStrings.condaInstallingPackages,
79111
cancellable: true,
80112
},
81-
async (_progress, token) => {
82-
try {
83-
await managePackages(environment, manageOptions, token, this.log);
84-
await updatePackagesAndNotify(
85-
this,
86-
environment,
87-
this.packages.get(environment.envId.id),
88-
(changes) => {
89-
this._onDidChangePackages.fire({ environment, manager: this, changes });
90-
},
91-
);
92-
} catch (e) {
93-
if (e instanceof CancellationError) {
94-
throw e;
95-
}
96-
97-
this.log.error('Error installing packages', e);
98-
if (!manageOptions.runHeadless) {
99-
setImmediate(async () => {
100-
await showErrorMessageWithLogs(CondaStrings.condaInstallError, this.log);
101-
});
102-
}
103-
throw e;
104-
}
105-
},
113+
async (_progress, token) => execute(token),
106114
);
107115
}
108116

@@ -178,16 +186,6 @@ export class CondaPackageManager implements PackageManager, Disposable {
178186
}
179187
}
180188

181-
/**
182-
* Lists available versions for a package via `conda search --json`, newest first.
183-
*
184-
* Conda always supports version lookup, so operational failures (command, network, or
185-
* malformed/unparseable output) propagate instead of being swallowed into `undefined`.
186-
*
187-
* @param _environment - Unused; conda resolves versions from its configured channels.
188-
* @param packageName - The package whose versions should be listed.
189-
* @returns A promise that resolves to an array of {@link Pep440Version} objects (newest first).
190-
*/
191189
async getPackageAvailableVersions(
192190
_environment: PythonEnvironment,
193191
packageName: string,
@@ -197,7 +195,6 @@ export class CondaPackageManager implements PackageManager, Disposable {
197195
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed[packageName])) {
198196
throw new Error(`Conda returned unexpected package version data for: ${packageName}`);
199197
}
200-
201198
const uniqueVersions = new Map<string, Pep440Version>();
202199
parsed[packageName]
203200
.filter((entry: { version?: string }) => !!entry.version?.trim())

‎src/managers/conda/condaUtils.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1253,7 +1253,7 @@ export async function deleteCondaEnvironment(environment: PythonEnvironment, log
12531253
export async function managePackages(
12541254
environment: PythonEnvironment,
12551255
options: PackageManagementOptions,
1256-
token: CancellationToken,
1256+
token: CancellationToken | undefined,
12571257
log: LogOutputChannel,
12581258
): Promise<void> {
12591259
if (options.uninstall && options.uninstall.length > 0) {

0 commit comments

Comments
 (0)