Skip to content

Commit 730c8e5

Browse files
committed
unit tests
1 parent d7f0687 commit 730c8e5

File tree

2 files changed

+407
-0
lines changed

2 files changed

+407
-0
lines changed
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
3+
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as chaiAsPromised from 'chai-as-promised';
7+
import * as path from 'path';
8+
import { assert, use as chaiUse } from 'chai';
9+
import * as sinon from 'sinon';
10+
import * as typemoq from 'typemoq';
11+
import { CancellationToken, ProgressOptions, Uri } from 'vscode';
12+
import { CreateEnvironmentProgress } from '../../../../client/pythonEnvironments/creation/types';
13+
import { UvCreationProvider } from '../../../../client/pythonEnvironments/creation/provider/uvCreationProvider';
14+
import * as wsSelect from '../../../../client/pythonEnvironments/creation/common/workspaceSelection';
15+
import * as windowApis from '../../../../client/common/vscodeApis/windowApis';
16+
import * as uvUtils from '../../../../client/pythonEnvironments/creation/provider/uvUtils';
17+
import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants';
18+
import * as rawProcessApis from '../../../../client/common/process/rawProcessApis';
19+
import { Output } from '../../../../client/common/process/types';
20+
import { createDeferred } from '../../../../client/common/utils/async';
21+
import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils';
22+
import { CreateEnv } from '../../../../client/common/utils/localize';
23+
import { UvUtils } from '../../../../client/pythonEnvironments/common/environmentManagers/uv';
24+
import * as venvUtils from '../../../../client/pythonEnvironments/creation/provider/venvUtils';
25+
import {
26+
CreateEnvironmentProvider,
27+
CreateEnvironmentResult,
28+
} from '../../../../client/pythonEnvironments/creation/proposed.createEnvApis';
29+
30+
chaiUse(chaiAsPromised.default);
31+
32+
suite('UV Creation provider tests', () => {
33+
let uvProvider: CreateEnvironmentProvider;
34+
let progressMock: typemoq.IMock<CreateEnvironmentProgress>;
35+
let getUvUtilsStub: sinon.SinonStub;
36+
let pickPythonVersionStub: sinon.SinonStub;
37+
let pickWorkspaceFolderStub: sinon.SinonStub;
38+
let execObservableStub: sinon.SinonStub;
39+
let withProgressStub: sinon.SinonStub;
40+
let showErrorMessageWithLogsStub: sinon.SinonStub;
41+
let pickExistingVenvActionStub: sinon.SinonStub;
42+
let getVenvExecutableStub: sinon.SinonStub;
43+
44+
setup(() => {
45+
pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder');
46+
getUvUtilsStub = sinon.stub(UvUtils, 'getUvUtils');
47+
pickPythonVersionStub = sinon.stub(uvUtils, 'pickPythonVersion');
48+
execObservableStub = sinon.stub(rawProcessApis, 'execObservable');
49+
withProgressStub = sinon.stub(windowApis, 'withProgress');
50+
51+
showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showPositronErrorMessageWithLogs');
52+
showErrorMessageWithLogsStub.resolves();
53+
54+
pickExistingVenvActionStub = sinon.stub(venvUtils, 'pickExistingVenvAction');
55+
pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Create);
56+
57+
getVenvExecutableStub = sinon.stub(commonUtils, 'getVenvExecutable');
58+
59+
progressMock = typemoq.Mock.ofType<CreateEnvironmentProgress>();
60+
uvProvider = new UvCreationProvider();
61+
});
62+
63+
teardown(() => {
64+
sinon.restore();
65+
});
66+
67+
test('No uv installed', async () => {
68+
getUvUtilsStub.resolves(undefined);
69+
70+
assert.isUndefined(await uvProvider.createEnvironment());
71+
});
72+
73+
test('No workspace selected', async () => {
74+
getUvUtilsStub.resolves({});
75+
pickWorkspaceFolderStub.resolves(undefined);
76+
77+
await assert.isRejected(uvProvider.createEnvironment());
78+
});
79+
80+
test('No python version picked selected', async () => {
81+
getUvUtilsStub.resolves({});
82+
pickWorkspaceFolderStub.resolves({
83+
uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')),
84+
name: 'workspace1',
85+
index: 0,
86+
});
87+
pickPythonVersionStub.resolves(undefined);
88+
89+
await assert.isRejected(uvProvider.createEnvironment());
90+
assert.isTrue(pickExistingVenvActionStub.calledOnce);
91+
});
92+
93+
test('Create uv environment', async () => {
94+
getUvUtilsStub.resolves({});
95+
const workspace1 = {
96+
uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')),
97+
name: 'workspace1',
98+
index: 0,
99+
};
100+
pickWorkspaceFolderStub.resolves(workspace1);
101+
pickPythonVersionStub.resolves('3.12');
102+
103+
const deferred = createDeferred();
104+
let _next: undefined | ((value: Output<string>) => void);
105+
let _complete: undefined | (() => void);
106+
execObservableStub.callsFake(() => {
107+
deferred.resolve();
108+
return {
109+
proc: {
110+
exitCode: 0,
111+
},
112+
out: {
113+
subscribe: (
114+
next?: (value: Output<string>) => void,
115+
_error?: (error: unknown) => void,
116+
complete?: () => void,
117+
) => {
118+
_next = next;
119+
_complete = complete;
120+
},
121+
},
122+
dispose: () => undefined,
123+
};
124+
});
125+
126+
progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once());
127+
128+
withProgressStub.callsFake(
129+
(
130+
_options: ProgressOptions,
131+
task: (
132+
progress: CreateEnvironmentProgress,
133+
token?: CancellationToken,
134+
) => Thenable<CreateEnvironmentResult>,
135+
) => task(progressMock.object),
136+
);
137+
138+
const promise = uvProvider.createEnvironment();
139+
await deferred.promise;
140+
assert.isDefined(_next);
141+
assert.isDefined(_complete);
142+
143+
_next!({ out: 'Created virtual environment', source: 'stdout' });
144+
_complete!();
145+
146+
const expectedPath = path.join(
147+
workspace1.uri.fsPath,
148+
process.platform === 'win32' ? '\\.venv\\Scripts\\python.exe' : '/.venv/bin/python',
149+
);
150+
const result = await promise;
151+
assert.deepStrictEqual(result, {
152+
path: expectedPath,
153+
workspaceFolder: workspace1,
154+
});
155+
assert.isTrue(showErrorMessageWithLogsStub.notCalled);
156+
assert.isTrue(pickExistingVenvActionStub.calledOnce);
157+
});
158+
159+
test('Create uv environment failed', async () => {
160+
getUvUtilsStub.resolves({});
161+
pickWorkspaceFolderStub.resolves({
162+
uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')),
163+
name: 'workspace1',
164+
index: 0,
165+
});
166+
pickPythonVersionStub.resolves('3.12');
167+
168+
const deferred = createDeferred();
169+
let _error: undefined | ((error: unknown) => void);
170+
let _complete: undefined | (() => void);
171+
execObservableStub.callsFake(() => {
172+
deferred.resolve();
173+
return {
174+
proc: undefined,
175+
out: {
176+
subscribe: (
177+
_next?: (value: Output<string>) => void,
178+
error?: (error: unknown) => void,
179+
complete?: () => void,
180+
) => {
181+
_error = error;
182+
_complete = complete;
183+
},
184+
},
185+
dispose: () => undefined,
186+
};
187+
});
188+
189+
progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once());
190+
191+
withProgressStub.callsFake(
192+
(
193+
_options: ProgressOptions,
194+
task: (
195+
progress: CreateEnvironmentProgress,
196+
token?: CancellationToken,
197+
) => Thenable<CreateEnvironmentResult>,
198+
) => task(progressMock.object),
199+
);
200+
201+
const promise = uvProvider.createEnvironment();
202+
await deferred.promise;
203+
assert.isDefined(_error);
204+
_error!('bad arguments');
205+
_complete!();
206+
const result = await promise;
207+
assert.ok(result?.error);
208+
assert.isTrue(showErrorMessageWithLogsStub.calledOnce);
209+
assert.isTrue(pickExistingVenvActionStub.calledOnce);
210+
});
211+
212+
test('Create uv environment failed (non-zero exit code)', async () => {
213+
getUvUtilsStub.resolves({});
214+
const workspace1 = {
215+
uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')),
216+
name: 'workspace1',
217+
index: 0,
218+
};
219+
pickWorkspaceFolderStub.resolves(workspace1);
220+
pickPythonVersionStub.resolves('3.12');
221+
222+
const deferred = createDeferred();
223+
let _next: undefined | ((value: Output<string>) => void);
224+
let _complete: undefined | (() => void);
225+
execObservableStub.callsFake(() => {
226+
deferred.resolve();
227+
return {
228+
proc: {
229+
exitCode: 1,
230+
},
231+
out: {
232+
subscribe: (
233+
next?: (value: Output<string>) => void,
234+
_error?: (error: unknown) => void,
235+
complete?: () => void,
236+
) => {
237+
_next = next;
238+
_complete = complete;
239+
},
240+
},
241+
dispose: () => undefined,
242+
};
243+
});
244+
245+
progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once());
246+
247+
withProgressStub.callsFake(
248+
(
249+
_options: ProgressOptions,
250+
task: (
251+
progress: CreateEnvironmentProgress,
252+
token?: CancellationToken,
253+
) => Thenable<CreateEnvironmentResult>,
254+
) => task(progressMock.object),
255+
);
256+
257+
const promise = uvProvider.createEnvironment();
258+
await deferred.promise;
259+
assert.isDefined(_next);
260+
assert.isDefined(_complete);
261+
262+
_next!({ out: 'Failed to create virtual environment', source: 'stdout' });
263+
_complete!();
264+
const result = await promise;
265+
assert.ok(result?.error);
266+
assert.isTrue(showErrorMessageWithLogsStub.calledOnce);
267+
assert.isTrue(pickExistingVenvActionStub.calledOnce);
268+
});
269+
270+
test('Use existing uv environment', async () => {
271+
getUvUtilsStub.resolves({});
272+
const workspace1 = {
273+
uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')),
274+
name: 'workspace1',
275+
index: 0,
276+
};
277+
pickWorkspaceFolderStub.resolves(workspace1);
278+
pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.UseExisting);
279+
getVenvExecutableStub.returns('/path/to/existing/venv/bin/python');
280+
281+
const result = await uvProvider.createEnvironment();
282+
assert.isTrue(showErrorMessageWithLogsStub.notCalled);
283+
assert.isTrue(pickPythonVersionStub.notCalled);
284+
assert.isTrue(execObservableStub.notCalled);
285+
assert.isTrue(withProgressStub.notCalled);
286+
287+
assert.deepStrictEqual(result, { path: '/path/to/existing/venv/bin/python', workspaceFolder: workspace1 });
288+
});
289+
290+
test('Create uv environment with options and pre-selected python version', async () => {
291+
getUvUtilsStub.resolves({});
292+
const newProjectWorkspace = {
293+
uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'newProjectWorkspace')),
294+
name: 'newProjectWorkspace',
295+
index: 0,
296+
};
297+
pickWorkspaceFolderStub.resolves(newProjectWorkspace);
298+
pickPythonVersionStub.resolves('3.12');
299+
300+
const deferred = createDeferred();
301+
let _next: undefined | ((value: Output<string>) => void);
302+
let _complete: undefined | (() => void);
303+
execObservableStub.callsFake(() => {
304+
deferred.resolve();
305+
return {
306+
proc: {
307+
exitCode: 0,
308+
},
309+
out: {
310+
subscribe: (
311+
next?: (value: Output<string>) => void,
312+
_error?: (error: unknown) => void,
313+
complete?: () => void,
314+
) => {
315+
_next = next;
316+
_complete = complete;
317+
},
318+
},
319+
dispose: () => undefined,
320+
};
321+
});
322+
323+
progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once());
324+
325+
withProgressStub.callsFake(
326+
(
327+
_options: ProgressOptions,
328+
task: (
329+
progress: CreateEnvironmentProgress,
330+
token?: CancellationToken,
331+
) => Thenable<CreateEnvironmentResult>,
332+
) => task(progressMock.object),
333+
);
334+
335+
// Options for createEnvironment
336+
const options = {
337+
workspaceFolder: newProjectWorkspace,
338+
uvPythonVersion: '3.12',
339+
};
340+
341+
const promise = uvProvider.createEnvironment(options);
342+
await deferred.promise;
343+
assert.isDefined(_next);
344+
assert.isDefined(_complete);
345+
346+
_next!({ out: 'Created virtual environment', source: 'stdout' });
347+
_complete!();
348+
349+
const expectedPath = path.join(
350+
newProjectWorkspace.uri.fsPath,
351+
process.platform === 'win32' ? '\\.venv\\Scripts\\python.exe' : '/.venv/bin/python',
352+
);
353+
assert.deepStrictEqual(await promise, {
354+
path: expectedPath,
355+
workspaceFolder: newProjectWorkspace,
356+
});
357+
assert.isTrue(showErrorMessageWithLogsStub.notCalled);
358+
assert.isTrue(pickExistingVenvActionStub.calledOnce);
359+
});
360+
});

0 commit comments

Comments
 (0)