Skip to content

Commit 32f9f3a

Browse files
committed
add more tests
1 parent f56998b commit 32f9f3a

2 files changed

Lines changed: 226 additions & 8 deletions

File tree

src/managers/builtin/pipUtils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ export function validatePyprojectToml(toml: tomljs.JsonMap): string | undefined
2626
}
2727

2828
// 2. Validate version format (PEP 440)
29-
if (toml.project && (toml.project as tomljs.JsonMap).version) {
29+
if (toml.project && 'version' in (toml.project as tomljs.JsonMap)) {
3030
const version = (toml.project as tomljs.JsonMap).version as string;
31+
if (version.length === 0) {
32+
return l10n.t('Version cannot be empty in pyproject.toml.');
33+
}
3134
// PEP 440 version regex. Versions must follow PEP 440 format (e.g., "1.0.0", "2.1a3").
3235
// See https://peps.python.org/pep-0440/
3336
// This regex is adapted from the official python 'packaging' library:

src/test/managers/builtin/helpers.validatePyproject.unit.test.ts

Lines changed: 222 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,6 @@ suite('pipUtils - validatePyproject', () => {
176176
verifyValidationError(toml, 'Invalid package name "mypackage_" in pyproject.toml.');
177177
});
178178

179-
test('should reject empty package name', () => {
180-
const toml: tomljs.JsonMap = {
181-
project: { name: '' } as tomljs.JsonMap,
182-
};
183-
verifyValidationError(toml, 'Invalid package name "" in pyproject.toml.');
184-
});
185-
186179
test('should reject package name with special characters', () => {
187180
const toml: tomljs.JsonMap = {
188181
project: { name: 'my@package' } as tomljs.JsonMap,
@@ -255,4 +248,226 @@ suite('pipUtils - validatePyproject', () => {
255248
verifyValidationError(toml, undefined);
256249
});
257250
});
251+
252+
suite('validatePyprojectToml - Version Validation (PEP 440)', () => {
253+
interface VersionTestCase {
254+
version: string;
255+
expectedError: string | undefined;
256+
description: string;
257+
}
258+
259+
function createVersionToml(version: string): tomljs.JsonMap {
260+
return {
261+
project: { name: 'test', version } as tomljs.JsonMap,
262+
};
263+
}
264+
265+
const versionTestCases: VersionTestCase[] = [
266+
// Basic release versions
267+
{ version: '1.0', expectedError: undefined, description: 'simple version 1.0' },
268+
{ version: '1.0.0', expectedError: undefined, description: 'version with three parts 1.0.0' },
269+
{ version: '1.2.3.4.5', expectedError: undefined, description: 'version with many parts 1.2.3.4.5' },
270+
{ version: '1.0.01', expectedError: undefined, description: 'version with leading zeros 1.0.01' },
271+
{ version: '0', expectedError: undefined, description: 'single digit version 0' },
272+
{ version: '2024.1.15', expectedError: undefined, description: 'large version numbers 2024.1.15' },
273+
274+
// Epoch versions
275+
{ version: '1!1.0', expectedError: undefined, description: 'epoch version 1!1.0' },
276+
{ version: '2!0.0.1', expectedError: undefined, description: 'epoch version 2!0.0.1' },
277+
{ version: '100!1.0.0', expectedError: undefined, description: 'large epoch 100!1.0.0' },
278+
279+
// Pre-release versions - Alpha
280+
{ version: '1.0a1', expectedError: undefined, description: 'alpha version 1.0a1' },
281+
{ version: '1.0.a1', expectedError: undefined, description: 'alpha with dot separator 1.0.a1' },
282+
{ version: '1.0-a1', expectedError: undefined, description: 'alpha with hyphen separator 1.0-a1' },
283+
{ version: '1.0_a1', expectedError: undefined, description: 'alpha with underscore 1.0_a1' },
284+
{ version: '1.0a', expectedError: undefined, description: 'alpha without number 1.0a' },
285+
{ version: '1.0alpha1', expectedError: undefined, description: 'long form alpha 1.0alpha1' },
286+
{ version: '1.0.alpha.1', expectedError: undefined, description: 'alpha with separators 1.0.alpha.1' },
287+
{ version: '1.0a999', expectedError: undefined, description: 'alpha with large number 1.0a999' },
288+
289+
// Pre-release versions - Beta
290+
{ version: '1.0b1', expectedError: undefined, description: 'beta version 1.0b1' },
291+
{ version: '1.0beta1', expectedError: undefined, description: 'long form beta 1.0beta1' },
292+
{ version: '1.0.beta.2', expectedError: undefined, description: 'beta with separators 1.0.beta.2' },
293+
{ version: '1.0b', expectedError: undefined, description: 'beta without number 1.0b' },
294+
295+
// Pre-release versions - RC
296+
{ version: '1.0rc1', expectedError: undefined, description: 'rc version 1.0rc1' },
297+
{ version: '1.0c1', expectedError: undefined, description: 'c version 1.0c1' },
298+
{ version: '1.0.rc.3', expectedError: undefined, description: 'rc with separators 1.0.rc.3' },
299+
{ version: '1.0rc', expectedError: undefined, description: 'rc without number 1.0rc' },
300+
301+
// Pre-release versions - Other
302+
{ version: '1.0preview1', expectedError: undefined, description: 'preview version 1.0preview1' },
303+
{ version: '1.0pre1', expectedError: undefined, description: 'pre version 1.0pre1' },
304+
{
305+
version: '1.0-preview-2',
306+
expectedError: undefined,
307+
description: 'preview with separators 1.0-preview-2',
308+
},
309+
310+
// Post-release versions
311+
{ version: '1.0.post1', expectedError: undefined, description: 'post version 1.0.post1' },
312+
{ version: '1.0post1', expectedError: undefined, description: 'post without dot 1.0post1' },
313+
{ version: '1.0-post1', expectedError: undefined, description: 'post with hyphen 1.0-post1' },
314+
{ version: '1.0_post1', expectedError: undefined, description: 'post with underscore 1.0_post1' },
315+
{ version: '1.0.post', expectedError: undefined, description: 'post without number 1.0.post' },
316+
{ version: '1.0-1', expectedError: undefined, description: 'implicit post version 1.0-1' },
317+
{ version: '1.0-5', expectedError: undefined, description: 'implicit post version 1.0-5' },
318+
{ version: '1.0rev1', expectedError: undefined, description: 'rev version 1.0rev1' },
319+
{ version: '1.0r1', expectedError: undefined, description: 'r version 1.0r1' },
320+
{ version: '1.0.rev.2', expectedError: undefined, description: 'rev with separators 1.0.rev.2' },
321+
{ version: '1.0.post999', expectedError: undefined, description: 'post with large number 1.0.post999' },
322+
323+
// Dev versions
324+
{ version: '1.0.dev1', expectedError: undefined, description: 'dev version 1.0.dev1' },
325+
{ version: '1.0dev1', expectedError: undefined, description: 'dev without dot 1.0dev1' },
326+
{ version: '1.0-dev1', expectedError: undefined, description: 'dev with hyphen 1.0-dev1' },
327+
{ version: '1.0_dev1', expectedError: undefined, description: 'dev with underscore 1.0_dev1' },
328+
{ version: '1.0.dev', expectedError: undefined, description: 'dev without number 1.0.dev' },
329+
{ version: '1.0.dev999', expectedError: undefined, description: 'dev with large number 1.0.dev999' },
330+
331+
// Local versions
332+
{ version: '1.0+abc', expectedError: undefined, description: 'local version 1.0+abc' },
333+
{ version: '1.0+abc.def', expectedError: undefined, description: 'local with dots 1.0+abc.def' },
334+
{ version: '1.0+abc-def', expectedError: undefined, description: 'local with hyphens 1.0+abc-def' },
335+
{ version: '1.0+abc_def', expectedError: undefined, description: 'local with underscores 1.0+abc_def' },
336+
{ version: '1.0+abc.5', expectedError: undefined, description: 'local with numbers 1.0+abc.5' },
337+
{
338+
version: '1.0+abc.def-ghi_jkl',
339+
expectedError: undefined,
340+
description: 'local with mixed separators 1.0+abc.def-ghi_jkl',
341+
},
342+
{ version: '1.0+001', expectedError: undefined, description: 'numeric local version 1.0+001' },
343+
{ version: '1.0+g1234567', expectedError: undefined, description: 'git hash-like local 1.0+g1234567' },
344+
345+
// Combined versions
346+
{ version: '1.0a1.post1', expectedError: undefined, description: 'pre + post 1.0a1.post1' },
347+
{
348+
version: '1.0a1.post1.dev2',
349+
expectedError: undefined,
350+
description: 'pre + post + dev 1.0a1.post1.dev2',
351+
},
352+
{ version: '1.0.post1.dev2', expectedError: undefined, description: 'post + dev 1.0.post1.dev2' },
353+
{ version: '1.0a1.dev1', expectedError: undefined, description: 'pre + dev 1.0a1.dev1' },
354+
{ version: '1.0a1+local', expectedError: undefined, description: 'pre + local 1.0a1+local' },
355+
{ version: '1.0.post1+local', expectedError: undefined, description: 'post + local 1.0.post1+local' },
356+
{ version: '1.0.dev1+local', expectedError: undefined, description: 'dev + local 1.0.dev1+local' },
357+
{
358+
version: '1!1.0a1.post1.dev2+abc',
359+
expectedError: undefined,
360+
description: 'epoch + all components 1!1.0a1.post1.dev2+abc',
361+
},
362+
{
363+
version: '2!1.2.3rc4.post5.dev6+local.version',
364+
expectedError: undefined,
365+
description: 'full complex version 2!1.2.3rc4.post5.dev6+local.version',
366+
},
367+
{ version: '1.0rc1-1', expectedError: undefined, description: 'rc + implicit post 1.0rc1-1' },
368+
369+
// Version with v prefix
370+
{ version: 'v1.0', expectedError: undefined, description: 'version with v prefix v1.0' },
371+
{ version: 'v1.0.0', expectedError: undefined, description: 'version with v prefix v1.0.0' },
372+
{ version: 'v1.0a1', expectedError: undefined, description: 'v prefix with pre-release v1.0a1' },
373+
{ version: 'v1.0-1', expectedError: undefined, description: 'v prefix with implicit post v1.0-1' },
374+
{
375+
version: 'v1!2.0rc1.post2.dev3+local',
376+
expectedError: undefined,
377+
description: 'v prefix with all components v1!2.0rc1.post2.dev3+local',
378+
},
379+
380+
// Case insensitivity
381+
{ version: '1.0A1', expectedError: undefined, description: 'uppercase alpha 1.0A1' },
382+
{ version: '1.0ALPHA1', expectedError: undefined, description: 'uppercase ALPHA 1.0ALPHA1' },
383+
{ version: '1.0Alpha1', expectedError: undefined, description: 'mixed case Alpha 1.0Alpha1' },
384+
{ version: '1.0POST1', expectedError: undefined, description: 'uppercase POST 1.0POST1' },
385+
{ version: '1.0DEV1', expectedError: undefined, description: 'uppercase DEV 1.0DEV1' },
386+
{ version: '1.0RC1', expectedError: undefined, description: 'uppercase RC 1.0RC1' },
387+
{ version: 'V1.0', expectedError: undefined, description: 'uppercase V prefix V1.0' },
388+
{
389+
version: '1.0Alpha1.POST2.Dev3',
390+
expectedError: undefined,
391+
description: 'mixed case components 1.0Alpha1.POST2.Dev3',
392+
},
393+
394+
// Invalid versions
395+
{
396+
version: '.1.0',
397+
expectedError: 'Invalid version ".1.0" in pyproject.toml.',
398+
description: 'starting with dot .1.0',
399+
},
400+
{
401+
version: '1.0.',
402+
expectedError: 'Invalid version "1.0." in pyproject.toml.',
403+
description: 'ending with dot 1.0.',
404+
},
405+
{
406+
version: 'abc',
407+
expectedError: 'Invalid version "abc" in pyproject.toml.',
408+
description: 'completely invalid abc',
409+
},
410+
{ version: '', expectedError: 'Version cannot be empty in pyproject.toml.', description: 'empty version' },
411+
{
412+
version: '1..0',
413+
expectedError: 'Invalid version "1..0" in pyproject.toml.',
414+
description: 'double dots 1..0',
415+
},
416+
{
417+
version: '1.0 rc1',
418+
expectedError: 'Invalid version "1.0 rc1" in pyproject.toml.',
419+
description: 'spaces 1.0 rc1',
420+
},
421+
{
422+
version: '1.0gamma1',
423+
expectedError: 'Invalid version "1.0gamma1" in pyproject.toml.',
424+
description: 'invalid pre-release keyword 1.0gamma1',
425+
},
426+
{
427+
version: '1 1.0',
428+
expectedError: 'Invalid version "1 1.0" in pyproject.toml.',
429+
description: 'epoch without exclamation 1 1.0',
430+
},
431+
{
432+
version: '1.0local',
433+
expectedError: 'Invalid version "1.0local" in pyproject.toml.',
434+
description: 'local without plus 1.0local',
435+
},
436+
{
437+
version: '1.0+abc+def',
438+
expectedError: 'Invalid version "1.0+abc+def" in pyproject.toml.',
439+
description: 'multiple local markers 1.0+abc+def',
440+
},
441+
{
442+
version: '1!',
443+
expectedError: 'Invalid version "1!" in pyproject.toml.',
444+
description: 'only epoch 1!',
445+
},
446+
{
447+
version: '1.0--a1',
448+
expectedError: 'Invalid version "1.0--a1" in pyproject.toml.',
449+
description: 'invalid separator combinations 1.0--a1',
450+
},
451+
];
452+
453+
versionTestCases.forEach(({ version, expectedError, description }) => {
454+
test(`should ${expectedError ? 'reject' : 'accept'} ${description}`, () => {
455+
const toml = createVersionToml(version);
456+
verifyValidationError(toml, expectedError);
457+
});
458+
});
459+
460+
// Edge cases
461+
test('should accept when no version field exists', () => {
462+
const toml: tomljs.JsonMap = {
463+
project: { name: 'test' } as tomljs.JsonMap,
464+
};
465+
verifyValidationError(toml, undefined);
466+
});
467+
468+
test('should accept when no project section exists', () => {
469+
const toml: tomljs.JsonMap = {};
470+
verifyValidationError(toml, undefined);
471+
});
472+
});
258473
});

0 commit comments

Comments
 (0)