Skip to content

Commit 6b91d5d

Browse files
committed
chore: add execProcess wrapper and refactor poetryUtils (Fixes #1239)
1 parent f86d6ab commit 6b91d5d

File tree

3 files changed

+88
-11
lines changed

3 files changed

+88
-11
lines changed

src/common/childProcess.apis.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,35 @@
11
import * as cp from 'child_process';
2+
import { promisify } from 'util';
3+
4+
const cpExec = promisify(cp.exec);
5+
6+
/**
7+
* Result of execProcess - contains stdout and stderr as strings.
8+
*/
9+
export interface ExecResult {
10+
stdout: string;
11+
stderr: string;
12+
}
13+
14+
/**
15+
* Executes a command and returns the result as a promise.
16+
* This function abstracts cp.exec to make it easier to mock in tests.
17+
*
18+
* @param command The command to execute (can include arguments).
19+
* @param options Optional execution options.
20+
* @returns A promise that resolves with { stdout, stderr } strings.
21+
*/
22+
export async function execProcess(command: string, options?: cp.ExecOptions): Promise<ExecResult> {
23+
const env = {
24+
PYTHONUTF8: '1',
25+
...(options?.env ?? process.env),
26+
};
27+
const result = await cpExec(command, { ...options, env });
28+
return {
29+
stdout: result.stdout ?? '',
30+
stderr: result.stderr ?? '',
31+
};
32+
}
233

334
/**
435
* Spawns a new process using the specified command and arguments.

src/managers/poetry/poetryUtils.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as path from 'path';
33
import { Uri } from 'vscode';
44
import which from 'which';
55
import { EnvironmentManager, PythonEnvironment, PythonEnvironmentApi, PythonEnvironmentInfo } from '../../api';
6+
import { execProcess } from '../../common/childProcess.apis';
67
import { ENVS_EXTENSION_ID } from '../../common/constants';
78
import { traceError, traceInfo } from '../../common/logging';
89
import { getWorkspacePersistentState } from '../../common/persistentState';
@@ -190,7 +191,7 @@ export async function getPoetryVirtualenvsPath(poetryExe?: string): Promise<stri
190191
const poetry = poetryExe || (await getPoetry());
191192
if (poetry) {
192193
try {
193-
const { stdout } = await exec(`"${poetry}" config virtualenvs.path`);
194+
const { stdout } = await execProcess(`"${poetry}" config virtualenvs.path`);
194195
if (stdout) {
195196
const venvPath = stdout.trim();
196197
// Poetry might return the path with placeholders like {cache-dir}
@@ -225,20 +226,14 @@ export async function getPoetryVirtualenvsPath(poetryExe?: string): Promise<stri
225226
return undefined;
226227
}
227228

228-
// These are now exported for use in main.ts or environment manager logic
229-
import * as cp from 'child_process';
230-
import { promisify } from 'util';
231-
232-
const exec = promisify(cp.exec);
233-
234229
export async function getPoetryVersion(poetry: string): Promise<string | undefined> {
235230
try {
236-
const { stdout } = await exec(`"${poetry}" --version`);
231+
const { stdout } = await execProcess(`"${poetry}" --version`);
237232
// Handle both formats:
238233
// Old: "Poetry version 1.5.1"
239234
// New: "Poetry (version 2.1.3)"
240235
traceInfo(`Poetry version output: ${stdout.trim()}`);
241-
const match = stdout.match(/Poetry (?:version|[\(\s]+version[\s\)]+)([0-9]+\.[0-9]+\.[0-9]+)/i);
236+
const match = stdout.match(/Poetry (?:version |[\(\s]+version[\s\)]+)([0-9]+\.[0-9]+\.[0-9]+)/i);
242237
return match ? match[1] : undefined;
243238
} catch {
244239
return undefined;

src/test/managers/poetry/poetryUtils.unit.test.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import assert from 'node:assert';
22
import * as sinon from 'sinon';
3-
import { isPoetryVirtualenvsInProject, nativeToPythonEnv } from '../../../managers/poetry/poetryUtils';
4-
import * as utils from '../../../managers/common/utils';
53
import { EnvironmentManager, PythonEnvironment, PythonEnvironmentApi, PythonEnvironmentInfo } from '../../../api';
4+
import * as childProcessApis from '../../../common/childProcess.apis';
65
import { NativeEnvInfo } from '../../../managers/common/nativePythonFinder';
6+
import * as utils from '../../../managers/common/utils';
7+
import {
8+
getPoetryVersion,
9+
isPoetryVirtualenvsInProject,
10+
nativeToPythonEnv,
11+
} from '../../../managers/poetry/poetryUtils';
712

813
suite('isPoetryVirtualenvsInProject', () => {
914
test('should return false when env var is not set', () => {
@@ -157,3 +162,49 @@ suite('nativeToPythonEnv - POETRY_VIRTUALENVS_IN_PROJECT integration', () => {
157162
assert.strictEqual(capturedInfo!.group, undefined, 'Non-global path should not be global');
158163
});
159164
});
165+
166+
suite('getPoetryVersion - childProcess.apis mocking pattern', () => {
167+
let execProcessStub: sinon.SinonStub;
168+
169+
setup(() => {
170+
execProcessStub = sinon.stub(childProcessApis, 'execProcess');
171+
});
172+
173+
teardown(() => {
174+
sinon.restore();
175+
});
176+
177+
test('should parse Poetry 1.x version format', async () => {
178+
execProcessStub.resolves({ stdout: 'Poetry version 1.5.1\n', stderr: '' });
179+
180+
const version = await getPoetryVersion('/usr/bin/poetry');
181+
182+
assert.strictEqual(version, '1.5.1');
183+
assert.ok(execProcessStub.calledOnce);
184+
assert.ok(execProcessStub.calledWith('"/usr/bin/poetry" --version'));
185+
});
186+
187+
test('should parse Poetry 2.x version format', async () => {
188+
execProcessStub.resolves({ stdout: 'Poetry (version 2.1.3)\n', stderr: '' });
189+
190+
const version = await getPoetryVersion('/usr/bin/poetry');
191+
192+
assert.strictEqual(version, '2.1.3');
193+
});
194+
195+
test('should return undefined when command fails', async () => {
196+
execProcessStub.rejects(new Error('Command not found'));
197+
198+
const version = await getPoetryVersion('/nonexistent/poetry');
199+
200+
assert.strictEqual(version, undefined);
201+
});
202+
203+
test('should return undefined when output does not match expected format', async () => {
204+
execProcessStub.resolves({ stdout: 'unexpected output', stderr: '' });
205+
206+
const version = await getPoetryVersion('/usr/bin/poetry');
207+
208+
assert.strictEqual(version, undefined);
209+
});
210+
});

0 commit comments

Comments
 (0)