11import assert from 'assert' ;
2+ import * as path from 'path' ;
3+ import * as sinon from 'sinon' ;
24import { Uri } from 'vscode' ;
5+ import * as windowApis from '../../../common/window.apis' ;
36import {
47 PyprojectToml ,
58 shouldProceedAfterPyprojectValidation ,
@@ -9,66 +12,103 @@ import {
912
1013suite ( 'pipUtils - validatePyproject' , ( ) => {
1114 suite ( 'shouldProceedAfterPyprojectValidation' , ( ) => {
15+ const invalidRoot = path . join ( process . cwd ( ) , 'test-data' , 'invalid-project' ) ;
16+ const otherRoot = path . join ( process . cwd ( ) , 'test-data' , 'other-project' ) ;
1217 const mockValidationError : ValidationError = {
1318 message : 'Invalid package name "my package" in pyproject.toml.' ,
14- fileUri : Uri . file ( '/test/ path/ pyproject.toml') ,
19+ fileUri : Uri . file ( path . join ( invalidRoot , ' pyproject.toml') ) ,
1520 } ;
21+ let showErrorMessage : sinon . SinonStub ;
22+ let showTextDocument : sinon . SinonStub ;
1623
17- test ( 'should return true when no validation error exists' , async ( ) => {
18- // Arrange: no validation error
19- const validationError = undefined ;
20- const install = [ '-e' , '/test/path' ] ;
24+ setup ( ( ) => {
25+ showErrorMessage = sinon . stub ( windowApis , 'showErrorMessage' ) ;
26+ showTextDocument = sinon . stub ( windowApis , 'showTextDocument' ) . resolves ( { } as never ) ;
27+ } ) ;
2128
22- // Act
23- const result = await shouldProceedAfterPyprojectValidation ( validationError , install ) ;
29+ teardown ( ( ) => sinon . restore ( ) ) ;
2430
25- // Assert
26- assert . strictEqual ( result , true , 'Should proceed when no validation error' ) ;
31+ test ( 'returns true when no validation error exists' , async ( ) => {
32+ assert . strictEqual ( await shouldProceedAfterPyprojectValidation ( [ ] ) , true ) ;
33+ assert . ok ( showErrorMessage . notCalled ) ;
2734 } ) ;
2835
29- test ( 'should return true when install array is empty' , async ( ) => {
30- // Arrange: validation error exists but no packages selected
31- const install : string [ ] = [ ] ;
36+ test ( 'does not block when no pyproject.toml or a different file was selected' , async ( ) => {
37+ assert . strictEqual ( await shouldProceedAfterPyprojectValidation ( [ mockValidationError ] ) , true ) ;
38+ assert . strictEqual (
39+ await shouldProceedAfterPyprojectValidation ( [ mockValidationError ] , {
40+ selectedPyprojectTomls : [ Uri . file ( path . join ( otherRoot , 'pyproject.toml' ) ) ] ,
41+ } ) ,
42+ true ,
43+ ) ;
44+ assert . ok ( showErrorMessage . notCalled ) ;
45+ } ) ;
3246
33- // Act
34- const result = await shouldProceedAfterPyprojectValidation ( mockValidationError , install ) ;
47+ test ( 'does not confuse a bracketed sibling directory with extras from the invalid project' , async ( ) => {
48+ const bracketedSibling = Uri . file ( `${ invalidRoot } [docs]` + path . sep + 'pyproject.toml' ) ;
49+
50+ const result = await shouldProceedAfterPyprojectValidation ( [ mockValidationError ] , {
51+ selectedPyprojectTomls : [ bracketedSibling ] ,
52+ } ) ;
3553
36- // Assert
37- assert . strictEqual ( result , true , 'Should proceed when no packages selected' ) ;
54+ assert . strictEqual ( result , true ) ;
55+ assert . ok ( showErrorMessage . notCalled ) ;
3856 } ) ;
3957
40- test ( 'should return true when only requirements.txt packages selected (no -e flag)' , async ( ) => {
41- // Arrange: validation error exists but only requirements.txt packages selected
42- const install = [ '-r' , '/test/requirements.txt' ] ;
58+ test ( 'continues when Continue Anyway is selected for the matching pyproject' , async ( ) => {
59+ showErrorMessage . callsFake ( async ( ...args : unknown [ ] ) => args [ 2 ] as never ) ;
4360
44- // Act
45- const result = await shouldProceedAfterPyprojectValidation ( mockValidationError , install ) ;
61+ const result = await shouldProceedAfterPyprojectValidation ( [ mockValidationError ] , {
62+ selectedPyprojectTomls : [ mockValidationError . fileUri ] ,
63+ } ) ;
4664
47- // Assert
48- assert . strictEqual ( result , true , 'Should proceed when no TOML packages selected' ) ;
65+ assert . strictEqual ( result , true ) ;
66+ assert . ok ( showErrorMessage . calledOnce ) ;
67+ assert . ok ( showTextDocument . notCalled ) ;
4968 } ) ;
5069
51- test ( 'should return true when only PyPI packages selected (no flags at all)' , async ( ) => {
52- // Arrange: only PyPI package names, no flags
53- const install = [ 'numpy' , 'pandas' , 'requests' ] ;
70+ test ( 'opens the matching pyproject and stops creation when Open is selected' , async ( ) => {
71+ showErrorMessage . callsFake ( async ( ...args : unknown [ ] ) => args [ 1 ] as never ) ;
5472
55- // Act
56- const result = await shouldProceedAfterPyprojectValidation ( mockValidationError , install ) ;
73+ const result = await shouldProceedAfterPyprojectValidation ( [ mockValidationError ] , {
74+ selectedPyprojectTomls : [ mockValidationError . fileUri ] ,
75+ } ) ;
5776
58- // Assert
59- assert . strictEqual ( result , true , 'Should proceed when only PyPI packages selected' ) ;
77+ assert . strictEqual ( result , false ) ;
78+ assert . ok ( showTextDocument . calledOnceWithExactly ( mockValidationError . fileUri ) ) ;
6079 } ) ;
6180
62- test ( 'should not trigger on -e flag at end of array without following argument' , async ( ) => {
63- // Arrange: -e flag is last item (malformed, but should not crash)
64- const install = [ 'numpy' , '-e' ] ;
65- // This is edge case - -e at end means no path follows, so index + 1 < arr.length is false
81+ test ( 'stops creation without opening the file when Cancel is selected' , async ( ) => {
82+ showErrorMessage . callsFake ( async ( ...args : unknown [ ] ) => args [ 3 ] as never ) ;
6683
67- // Act
68- const result = await shouldProceedAfterPyprojectValidation ( mockValidationError , install ) ;
84+ const result = await shouldProceedAfterPyprojectValidation ( [ mockValidationError ] , {
85+ selectedPyprojectTomls : [ mockValidationError . fileUri ] ,
86+ } ) ;
6987
70- // Assert
71- assert . strictEqual ( result , true , 'Should not crash on malformed -e flag at end' ) ;
88+ assert . strictEqual ( result , false ) ;
89+ assert . ok ( showTextDocument . notCalled ) ;
90+ } ) ;
91+
92+ test ( 'stops creation when the validation dialog is dismissed' , async ( ) => {
93+ showErrorMessage . resolves ( undefined ) ;
94+
95+ const result = await shouldProceedAfterPyprojectValidation ( [ mockValidationError ] , {
96+ selectedPyprojectTomls : [ mockValidationError . fileUri ] ,
97+ } ) ;
98+
99+ assert . strictEqual ( result , false ) ;
100+ assert . ok ( showTextDocument . notCalled ) ;
101+ } ) ;
102+
103+ test ( 'prompts for an invalid-file-only Quick Create selection' , async ( ) => {
104+ showErrorMessage . callsFake ( async ( ...args : unknown [ ] ) => args [ 3 ] as never ) ;
105+
106+ const result = await shouldProceedAfterPyprojectValidation ( [ mockValidationError ] , {
107+ allPyprojectTomlsSelected : true ,
108+ } ) ;
109+
110+ assert . strictEqual ( result , false ) ;
111+ assert . ok ( showErrorMessage . calledOnce ) ;
72112 } ) ;
73113 } ) ;
74114
@@ -196,6 +236,65 @@ suite('pipUtils - validatePyproject', () => {
196236 } ) ;
197237 } ) ;
198238
239+ suite ( 'validatePyprojectToml - Dependency Values' , ( ) => {
240+ test ( 'accepts dependency arrays containing strings' , ( ) => {
241+ verifyValidationError (
242+ {
243+ project : {
244+ name : 'test' ,
245+ dependencies : [ 'requests>=2' , 'httpx' ] ,
246+ 'optional-dependencies' : { test : [ 'pytest' ] } ,
247+ } ,
248+ 'build-system' : { requires : [ 'setuptools' ] } ,
249+ } ,
250+ undefined ,
251+ ) ;
252+ } ) ;
253+
254+ test ( 'rejects project dependencies that are not an array of strings' , ( ) => {
255+ const toml = {
256+ project : { name : 'test' , dependencies : 'requests' } ,
257+ } as PyprojectToml ;
258+ assert . match ( validatePyprojectToml ( toml ) ?? '' , / d e p e n d e n c i e s .* a r r a y o f s t r i n g s / i) ;
259+ } ) ;
260+
261+ test ( 'rejects non-string entries in project dependencies' , ( ) => {
262+ const toml = {
263+ project : { name : 'test' , dependencies : [ 'requests' , 42 ] } ,
264+ } as PyprojectToml ;
265+ assert . match ( validatePyprojectToml ( toml ) ?? '' , / d e p e n d e n c i e s .* a r r a y o f s t r i n g s / i) ;
266+ } ) ;
267+
268+ test ( 'rejects optional dependencies that are not a table' , ( ) => {
269+ const toml = {
270+ project : { name : 'test' , 'optional-dependencies' : [ 'pytest' ] } ,
271+ } as PyprojectToml ;
272+ assert . match ( validatePyprojectToml ( toml ) ?? '' , / o p t i o n a l - d e p e n d e n c i e s .* t a b l e / i) ;
273+ } ) ;
274+
275+ test ( 'rejects invalid optional dependency group names' , ( ) => {
276+ const toml = {
277+ project : { name : 'test' , 'optional-dependencies' : { 'bad extra' : [ 'pytest' ] } } ,
278+ } as PyprojectToml ;
279+ assert . match ( validatePyprojectToml ( toml ) ?? '' , / i n v a l i d o p t i o n a l d e p e n d e n c y g r o u p n a m e / i) ;
280+ } ) ;
281+
282+ test ( 'rejects optional dependency groups that are not arrays of strings' , ( ) => {
283+ const toml = {
284+ project : { name : 'test' , 'optional-dependencies' : { test : 'pytest' } } ,
285+ } as PyprojectToml ;
286+ assert . match ( validatePyprojectToml ( toml ) ?? '' , / g r o u p .* a r r a y o f s t r i n g s / i) ;
287+ } ) ;
288+
289+ test ( 'rejects build-system requirements that are not arrays of strings' , ( ) => {
290+ const toml = {
291+ project : { name : 'test' } ,
292+ 'build-system' : { requires : 'setuptools' } ,
293+ } as PyprojectToml ;
294+ assert . match ( validatePyprojectToml ( toml ) ?? '' , / r e q u i r e s .* a r r a y o f s t r i n g s / i) ;
295+ } ) ;
296+ } ) ;
297+
199298 suite ( 'validatePyprojectToml - Required Fields (PEP 621)' , ( ) => {
200299 test ( 'should accept valid project with name' , ( ) => {
201300 const toml : PyprojectToml = {
0 commit comments