Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/managers/builtin/commands/uninstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export class PipUninstallCommand extends UninstallCommand {

/**
* UV uninstall command.
* Parsed command: `uv pip uninstall -y --python <path> <package>`
* Parsed command: `uv pip uninstall --python <path> <package>`
* Note: `uv pip uninstall` is non-interactive by default and has no `-y`/`--yes` option.
* Official documentation: https://docs.astral.sh/uv/pip/
*/
export class UvUninstallCommand extends UninstallCommand {
Expand Down
137 changes: 137 additions & 0 deletions src/test/managers/builtin/commands.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import assert from 'assert';
import * as sinon from 'sinon';
import { LogOutputChannel } from 'vscode';
import * as workspaceApis from '../../../common/workspace.apis';
import { PipAvailableVersionsCommand, UvAvailableVersionsCommand } from '../../../managers/builtin/commands/availableVersions';
import { PipInstallCommand, UvInstallCommand } from '../../../managers/builtin/commands/install';
import { PipListCommand, UvListCommand } from '../../../managers/builtin/commands/list';
import { PipListDirectNamesCommand, UvListDirectNamesCommand } from '../../../managers/builtin/commands/listDirectNames';
import { PipUninstallCommand, UvUninstallCommand } from '../../../managers/builtin/commands/uninstall';
import { PipVersionCommand, UvVersionCommand } from '../../../managers/builtin/commands/version';
import * as helpers from '../../../managers/builtin/helpers';
import { createMockLogOutputChannel } from '../../mocks/helper';

suite('Pip and UV commands', () => {
let mockLog: LogOutputChannel;
let runPythonStub: sinon.SinonStub;
let runUvStub: sinon.SinonStub;

setup(() => {
mockLog = createMockLogOutputChannel();
sinon.stub(workspaceApis, 'getConfiguration').returns({
get: () => undefined,
} as unknown as ReturnType<typeof workspaceApis.getConfiguration>);
runPythonStub = sinon.stub(helpers, 'runPython').resolves('');
runUvStub = sinon.stub(helpers, 'runUV').resolves('');
});

teardown(() => {
sinon.restore();
});

// Mutation commands have no output to parse; unit coverage is a smoke test that
// execute() resolves. Their concrete command strings are exercised by integration tests.
suite('mutation commands execute without error', () => {
Comment on lines +33 to +34

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This integration-coverage claim is not currently true. packageManagement.integration.test.ts only selects venv environments for install/uninstall, so it never exercises the Conda or Poetry command classes; that path also dispatches to Pip or UV dynamically, so it does not deterministically cover either concrete command string. Because these unit tests do not inspect the runner invocation either, subcommand, flag, interpreter, environment-prefix, and cwd regressions remain uncovered. Please assert the runner arguments in these suites (including the analogous Conda/Poetry tests), or remove this claim until deterministic integration coverage exists.

test('PipInstallCommand', async () => {
const command = new PipInstallCommand({ pythonExecutable: 'python', log: mockLog });
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
});

test('UvInstallCommand', async () => {
const command = new UvInstallCommand({ pythonExecutable: 'python', log: mockLog });
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
});

test('PipUninstallCommand', async () => {
const command = new PipUninstallCommand({ pythonExecutable: 'python', log: mockLog });
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
});

test('UvUninstallCommand', async () => {
const command = new UvUninstallCommand({ pythonExecutable: 'python', log: mockLog });
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
});
});

test('PipAvailableVersionsCommand parses versions and filters prereleases', async () => {
runPythonStub.resolves(JSON.stringify({ versions: ['2.0.0rc1', '1.0.0'] }));
const command = new PipAvailableVersionsCommand({ pythonExecutable: 'python', log: mockLog });

const result = await command.execute({
packageName: 'package',
pythonVersion: '3.13.1',
includePrerelease: false,
});

assert.deepStrictEqual(result.map((v) => v.public), ['1.0.0']);
});

test('UvAvailableVersionsCommand parses versions from embedded JSON', async () => {
runUvStub.resolves(`Some preamble\n${JSON.stringify({ versions: ['1.0.0'] })}`);
const command = new UvAvailableVersionsCommand({ pythonExecutable: 'python', log: mockLog });

const result = await command.execute({ packageName: 'package', pythonVersion: '3.13.1' });

assert.deepStrictEqual(result.map((v) => v.public), ['1.0.0']);
});

test('PipListCommand parses packages and drops entries missing a version', async () => {
runPythonStub.resolves(JSON.stringify([{ name: 'package', version: '1.0.0' }, { name: 'broken' }]));
const command = new PipListCommand({ pythonExecutable: 'python', log: mockLog });

const result = await command.execute();

assert.deepStrictEqual(
result.map((p) => ({ name: p.name, version: p.version })),
[{ name: 'package', version: '1.0.0' }],
);
});

test('UvListCommand parses packages from JSON', async () => {
runUvStub.resolves(JSON.stringify([{ name: 'package', version: '1.0.0' }]));
const command = new UvListCommand({ pythonExecutable: 'python', log: mockLog });

const result = await command.execute();

assert.deepStrictEqual(
result.map((p) => ({ name: p.name, version: p.version })),
[{ name: 'package', version: '1.0.0' }],
);
});

test('PipListDirectNamesCommand returns normalized names', async () => {
runPythonStub.resolves(JSON.stringify([{ name: 'Flask_Thing', version: '1.0.0' }]));
const command = new PipListDirectNamesCommand({ pythonExecutable: 'python', log: mockLog });

const result = await command.execute();

assert.deepStrictEqual([...result], ['flask-thing']);
});

test('UvListDirectNamesCommand keeps top-level packages and skips indented dependencies', async () => {
runUvStub.resolves(['Flask_Thing 1.0.0', '├── dependency 2.0.0', '└── another-dep 3.0.0'].join('\n'));
const command = new UvListDirectNamesCommand({ pythonExecutable: 'python', log: mockLog });

const result = await command.execute();

assert.deepStrictEqual([...result], ['flask-thing']);
});

test('PipVersionCommand parses the version from pip --version output', async () => {
runPythonStub.resolves('pip 24.0 from /site-packages/pip (python 3.13)');
const command = new PipVersionCommand({ pythonExecutable: 'python', log: mockLog });

const result = await command.execute();

assert.strictEqual(result?.public, '24.0');
});

test('UvVersionCommand parses the version from uv --version output', async () => {
runUvStub.resolves('uv 0.4.20 (abcdef 2024-01-01)');
const command = new UvVersionCommand({ pythonExecutable: 'python', log: mockLog });

const result = await command.execute();

assert.strictEqual(result?.public, '0.4.20');
});
});
84 changes: 84 additions & 0 deletions src/test/managers/conda/commands.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import assert from 'assert';
import * as sinon from 'sinon';
import { LogOutputChannel } from 'vscode';
import * as workspaceApis from '../../../common/workspace.apis';
import { CondaAvailableVersionsCommand } from '../../../managers/conda/commands/availableVersions';
import { CondaInstallCommand } from '../../../managers/conda/commands/install';
import { CondaListCommand } from '../../../managers/conda/commands/list';
import { CondaUninstallCommand } from '../../../managers/conda/commands/uninstall';
import { CondaVersionCommand } from '../../../managers/conda/commands/version';
import * as condaUtils from '../../../managers/conda/condaUtils';
import { createMockLogOutputChannel } from '../../mocks/helper';

suite('Conda commands', () => {
let mockLog: LogOutputChannel;
let runCondaStub: sinon.SinonStub;

setup(() => {
mockLog = createMockLogOutputChannel();
sinon.stub(workspaceApis, 'getConfiguration').returns({
get: () => undefined,
} as unknown as ReturnType<typeof workspaceApis.getConfiguration>);
runCondaStub = sinon.stub(condaUtils, 'runCondaExecutable').resolves('');
});

teardown(() => {
sinon.restore();
});

// Mutation commands have no output to parse; unit coverage is a smoke test that
// execute() resolves. Their concrete command strings are exercised by integration tests.
suite('mutation commands execute without error', () => {
test('CondaInstallCommand', async () => {
const command = new CondaInstallCommand({
pythonExecutable: 'conda',
condaEnvironmentPath: 'environment',
log: mockLog,
});
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
});

test('CondaUninstallCommand', async () => {
const command = new CondaUninstallCommand({
pythonExecutable: 'conda',
condaEnvironmentPath: 'environment',
log: mockLog,
});
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
});
});

test('CondaAvailableVersionsCommand parses versions keyed by package name', async () => {
runCondaStub.resolves(JSON.stringify({ package: [{ version: '1.0.0' }, { version: '2.0.0' }] }));
const command = new CondaAvailableVersionsCommand({ pythonExecutable: 'conda', log: mockLog });

const result = await command.execute({ packageName: 'package', pythonVersion: '' });

assert.deepStrictEqual(result.map((v) => v.public), ['1.0.0', '2.0.0']);
});

test('CondaListCommand parses packages and drops entries missing a version', async () => {
runCondaStub.resolves(JSON.stringify([{ name: 'package', version: '1.0.0' }, { name: 'broken' }]));
const command = new CondaListCommand({
pythonExecutable: 'conda',
condaEnvironmentPath: 'environment',
log: mockLog,
});

const result = await command.execute();

assert.deepStrictEqual(
result.map((p) => ({ name: p.name, version: p.version })),
[{ name: 'package', version: '1.0.0' }],
);
});

test('CondaVersionCommand parses the version from conda --version output', async () => {
runCondaStub.resolves('conda 24.1.2');
const command = new CondaVersionCommand({ pythonExecutable: 'conda', log: mockLog });

const result = await command.execute();

assert.strictEqual(result?.public, '24.1.2');
});
});
73 changes: 73 additions & 0 deletions src/test/managers/poetry/commands.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import assert from 'assert';
import * as sinon from 'sinon';
import { LogOutputChannel } from 'vscode';
import * as workspaceApis from '../../../common/workspace.apis';
import { PoetryAddCommand } from '../../../managers/poetry/commands/add';
import { PoetryRemoveCommand } from '../../../managers/poetry/commands/remove';
import * as poetryRunner from '../../../managers/poetry/commands/runPoetry';
import { PoetryShowCommand } from '../../../managers/poetry/commands/show';
import { PoetryShowTopLevelCommand } from '../../../managers/poetry/commands/showTopLevel';
import { PoetryVersionCommand } from '../../../managers/poetry/commands/version';
import * as poetryUtils from '../../../managers/poetry/poetryUtils';
import { createMockLogOutputChannel } from '../../mocks/helper';

suite('Poetry commands', () => {
let mockLog: LogOutputChannel;
let runPoetryStub: sinon.SinonStub;

setup(() => {
mockLog = createMockLogOutputChannel();
sinon.stub(workspaceApis, 'getConfiguration').returns({
get: () => undefined,
} as unknown as ReturnType<typeof workspaceApis.getConfiguration>);
runPoetryStub = sinon.stub(poetryRunner, 'runPoetry').resolves('');
sinon.stub(poetryUtils, 'getPoetryVersion').resolves('1.8.2');
});

teardown(() => {
sinon.restore();
});

// Mutation commands have no output to parse; unit coverage is a smoke test that
// execute() resolves. Their concrete command strings are exercised by integration tests.
suite('mutation commands execute without error', () => {
test('PoetryAddCommand', async () => {
const command = new PoetryAddCommand({ pythonExecutable: 'poetry', cwd: 'project', log: mockLog });
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
});

test('PoetryRemoveCommand', async () => {
const command = new PoetryRemoveCommand({ pythonExecutable: 'poetry', cwd: 'project', log: mockLog });
await assert.doesNotReject(() => command.execute({ packages: [{ packageName: 'package' }] }));
});
});

test('PoetryShowCommand parses name, version and description', async () => {
runPoetryStub.resolves(['requests 2.31.0 Python HTTP for Humans.', ''].join('\n'));
const command = new PoetryShowCommand({ pythonExecutable: 'poetry', cwd: 'project', log: mockLog });

const result = await command.execute();

assert.deepStrictEqual(
result.map((p) => ({ name: p.name, version: p.version, description: p.description })),
[{ name: 'requests', version: '2.31.0', description: '2.31.0 - Python HTTP for Humans.' }],
);
});

test('PoetryShowTopLevelCommand returns normalized names', async () => {
runPoetryStub.resolves(['Flask_Thing', 'requests'].join('\n'));
const command = new PoetryShowTopLevelCommand({ pythonExecutable: 'poetry', cwd: 'project', log: mockLog });

const result = await command.execute();

assert.deepStrictEqual([...result], ['flask-thing', 'requests']);
});

test('PoetryVersionCommand parses the poetry version', async () => {
const command = new PoetryVersionCommand({ pythonExecutable: 'poetry', log: mockLog });

const result = await command.execute();

assert.strictEqual(result?.public, '1.8.2');
});
});
Loading