Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(versioning): add PVP versioning scheme #32298

Merged
merged 16 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 2 additions & 0 deletions lib/modules/versioning/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import * as nuget from './nuget';
import * as pep440 from './pep440';
import * as perl from './perl';
import * as poetry from './poetry';
import * as pvp from './pvp';
import * as python from './python';
import * as redhat from './redhat';
import * as regex from './regex';
Expand Down Expand Up @@ -71,6 +72,7 @@ api.set(nuget.id, nuget.api);
api.set(pep440.id, pep440.api);
api.set(perl.id, perl.api);
api.set(poetry.id, poetry.api);
api.set(pvp.id, pvp.api);
api.set(python.id, python.api);
api.set(redhat.id, redhat.api);
api.set(regex.id, regex.api);
Expand Down
351 changes: 351 additions & 0 deletions lib/modules/versioning/pvp/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,351 @@
import pvp, { extractAllComponents, getComponents, parse } from '.';

describe('modules/versioning/pvp/index', () => {
describe('.isGreaterThan(version, other)', () => {
secustor marked this conversation as resolved.
Show resolved Hide resolved
it('should return true', () => {
expect(pvp.isGreaterThan('1.23.1', '1.9.6')).toBeTrue();
expect(pvp.isGreaterThan('4.0.0', '3.0.0')).toBeTrue();
expect(pvp.isGreaterThan('3.0.1', '3.0.0')).toBeTrue();
expect(pvp.isGreaterThan('4.10', '4.1')).toBeTrue();
expect(pvp.isGreaterThan('1.0.0', '1.0')).toBeTrue();
});

it('should return false', () => {
expect(pvp.isGreaterThan('2.0.2', '3.1.0')).toBeFalse(); // less
expect(pvp.isGreaterThan('3.0.0', '3.0.0')).toBeFalse(); // equal
expect(pvp.isGreaterThan('4.1', '4.10')).toBeFalse();
expect(pvp.isGreaterThan('1.0', '1.0.0')).toBeFalse();
});
});

describe('.parse(range)', () => {
it('should parse >=1.0 && <1.1', () => {
const parsed = parse('>=1.0 && <1.1');
expect(parsed).not.toBeNull();
expect(parsed!.lower).toBe('1.0');
expect(parsed!.upper).toBe('1.1');
});
});
ysangkok marked this conversation as resolved.
Show resolved Hide resolved

describe('.getMajor(version)', () => {
it('should extract second component as decimal digit', () => {
expect(pvp.getMajor('1.0.0')).toBe(1.0);
expect(pvp.getMajor('1.0.1')).toBe(1.0);
expect(pvp.getMajor('1.1.1')).toBe(1.1);
});
});

describe('.getMinor(version)', () => {
it('should extract minor as third component in version', () => {
expect(pvp.getMinor('1.0')).toBeNull();
expect(pvp.getMinor('1.0.0')).toBe(0);
expect(pvp.getMinor('1.0.1')).toBe(1);
expect(pvp.getMinor('1.1.2')).toBe(2);
});
});

describe('.getPatch(version)', () => {
it('should return null where there is no patch version', () => {
expect(pvp.getPatch('1.0.0')).toBeNull();
});

it('should extract all remaining components as decimal digits', () => {
expect(pvp.getPatch('1.0.0.5.1')).toBe(5.1);
expect(pvp.getPatch('1.0.1.6')).toBe(6);
expect(pvp.getPatch('1.1.2.7')).toBe(7);
});
});

describe('.matches(version, range)', () => {
it('should return true when version has same major', () => {
expect(pvp.matches('1.0.1', '>=1.0 && <1.1')).toBeTrue();
expect(pvp.matches('4.1', '>=4.0 && <4.10')).toBeTrue();
expect(pvp.matches('4.1', '>=4.1 && <4.10')).toBeTrue();
expect(pvp.matches('4.1.0', '>=4.1 && <4.10')).toBeTrue();
expect(pvp.matches('4.10', '>=4.1 && <4.10.0')).toBeTrue();
expect(pvp.matches('4.10', '>=4.0 && <4.10.1')).toBeTrue();
});

it('should return false when version has different major', () => {
expect(pvp.matches('1.0.0', '>=2.0 && <2.1')).toBeFalse();
expect(pvp.matches('4', '>=4.0 && <4.10')).toBeFalse();
expect(pvp.matches('4.10', '>=4.1 && <4.10')).toBeFalse();
});

it("should return false when range can't be parsed", () => {
expect(pvp.matches('4', 'gibberish')).toBeFalse();
});

it('should return false when given an invalid version', () => {
expect(pvp.matches('', '>=1.0 && <1.1')).toBeFalse();
});
});

describe('.getSatisfyingVersion(versions, range)', () => {
it('should return max satisfying version in range', () => {
expect(
pvp.getSatisfyingVersion(
['1.0.0', '1.0.4', '1.3.0', '2.0.0'],
'>=1.0 && <1.1',
),
).toBe('1.0.4');
});

it('should handle unsorted inputs', () => {
expect(
pvp.getSatisfyingVersion(
['2.0.0', '1.0.0', '1.0.4', '1.3.0'],
'>=1.0 && <1.1',
),
).toBe('1.0.4');
});

it('should return null when no versions match', () => {
expect(
pvp.getSatisfyingVersion(
['1.0.0', '1.0.4', '1.3.0', '2.0.0'],
'>=3.0 && <4.0',
),
).toBeNull();
});
});

describe('.minSatisfyingVersion(versions, range)', () => {
it('should return min satisfying version in range', () => {
expect(
pvp.minSatisfyingVersion(
['0.9', '1.0.0', '1.0.4', '1.3.0', '2.0.0'],
'>=1.0 && <1.1',
),
).toBe('1.0.0');
});
});

describe('.isLessThanRange(version, range)', () => {
it('should return true', () => {
expect(pvp.isLessThanRange?.('2.0.2', '>=3.0 && <3.1')).toBeTrue();
expect(pvp.isLessThanRange?.('3', '>=3.0 && <3.1')).toBeTrue();
});

it('should return false', () => {
expect(pvp.isLessThanRange?.('3', '>=3 && <3.1')).toBeFalse();
expect(pvp.isLessThanRange?.('3.0', '>=3.0 && <3.1')).toBeFalse();
expect(pvp.isLessThanRange?.('3.0.0', '>=3.0 && <3.1')).toBeFalse();
expect(pvp.isLessThanRange?.('4.0.0', '>=3.0 && <3.1')).toBeFalse();
expect(pvp.isLessThanRange?.('3.1.0', '>=3.0 && <3.1')).toBeFalse();
});

it("should return false when range can't be parsed", () => {
expect(pvp.isLessThanRange?.('3', 'gibberish')).toBeFalse();
});

it('should return true when version is invalid', () => {
expect(pvp.isLessThanRange?.('', '>=3.0 && <3.1')).toBeTrue();
});
});

describe('.extractAllComponents(version)', () => {
it('should return an empty array when there are no numbers', () => {
expect(extractAllComponents('')).toEqual([]);
});

it('should parse 3.0', () => {
expect(extractAllComponents('3.0')).toEqual([3, 0]);
});
});

describe('.isValid(version)', () => {
it('should reject zero components', () => {
expect(pvp.isValid('')).toBeFalse();
});

it('should accept four components', () => {
expect(pvp.isValid('1.0.0.0')).toBeTrue();
});

it('should accept 1.0 as valid', () => {
expect(pvp.isValid('1.0')).toBeTrue();
});

it('should accept >=1.0 && <1.1 as valid (range)', () => {
expect(pvp.isValid('>=1.0 && <1.1')).toBeTrue();
});
});

describe('.getNewValue(newValueConfig)', () => {
it('should bump the upper end of the range if necessary', () => {
expect(
pvp.getNewValue({
currentValue: '>=1.0 && <1.1',
newVersion: '1.1',
rangeStrategy: 'auto',
}),
).toBe('>=1.0 && <1.2');
});

it("shouldn't modify the range if not necessary", () => {
expect(
pvp.getNewValue({
currentValue: '>=1.2 && <1.3',
newVersion: '1.2.3',
rangeStrategy: 'auto',
}),
).toBeNull();
});

it('should return null when given unimplemented range strategies like update-lockfile', () => {
expect(
pvp.getNewValue({
currentValue: '>=1.0 && <1.1',
newVersion: '1.2.3',
rangeStrategy: 'update-lockfile',
}),
).toBeNull();
});

it("should return null when given range that can't be parsed", () => {
expect(
pvp.getNewValue({
currentValue: 'gibberish',
newVersion: '1.2.3',
rangeStrategy: 'auto',
}),
).toBeNull();
});

it('should return null when newVersion is below range', () => {
expect(
pvp.getNewValue({
currentValue: '>=1.0 && <1.1',
newVersion: '0.9',
rangeStrategy: 'auto',
}),
).toBeNull();
});

it('should return null when newVersion is empty', () => {
expect(
pvp.getNewValue({
currentValue: '>=1.0 && <1.1',
newVersion: '',
rangeStrategy: 'auto',
}),
).toBeNull();
});
});

describe('.getComponents(...)', () => {
it('"0" is valid major version', () => {
expect(getComponents('0')?.major).toEqual([0]);
});

it('returns null when no components could be extracted', () => {
expect(getComponents('')).toBeNull();
});
});

describe('.isSame(...)', () => {
it('should compare major components correctly', () => {
expect(pvp.isSame?.('major', '4.10', '4.1')).toBeFalse();
expect(pvp.isSame?.('major', '4.1.0', '5.1.0')).toBeFalse();
expect(pvp.isSame?.('major', '4.1', '5.1')).toBeFalse();
expect(pvp.isSame?.('major', '0', '1')).toBeFalse();
expect(pvp.isSame?.('major', '4.1', '4.1.0')).toBeTrue();
expect(pvp.isSame?.('major', '4.1.1', '4.1.2')).toBeTrue();
expect(pvp.isSame?.('major', '0', '0')).toBeTrue();
});

it('should compare minor components correctly', () => {
expect(pvp.isSame?.('minor', '4.1.0', '5.1.0')).toBeTrue();
expect(pvp.isSame?.('minor', '4.1', '4.1')).toBeTrue();
expect(pvp.isSame?.('minor', '4.1', '5.1')).toBeTrue();
expect(pvp.isSame?.('minor', '4.1.0', '4.1.1')).toBeFalse();
});

it('should compare patch components correctly', () => {
expect(pvp.isSame?.('patch', '1.0.0.0', '1.0.0.0')).toBeTrue();
expect(pvp.isSame?.('patch', '1.0.0.0', '2.0.0.0')).toBeTrue();
expect(pvp.isSame?.('patch', '1.0.0.0', '1.0.0.1')).toBeFalse();
});

it('should return false when the given a invalid version', () => {
expect(pvp.isSame?.('minor', '', '0')).toBeFalse();
});
});

describe('.isVersion(maybeRange)', () => {
it('should accept 1.0 as valid version', () => {
expect(pvp.isVersion('1.0')).toBeTrue();
});

it('should reject >=1.0 && <1.1 as it is a range, not a version', () => {
expect(pvp.isVersion('>=1.0 && <1.1')).toBeFalse();
});
});

describe('.equals(a, b)', () => {
it('should regard 1.01 and 1.1 as equal', () => {
expect(pvp.equals('1.01', '1.1')).toBeTrue();
});

it('should regard 1.01 and 1.0 are not equal', () => {
expect(pvp.equals('1.01', '1.0')).toBeFalse();
});
});

describe('.isSingleVersion(range)', () => {
it('should consider ==1.0 a single version', () => {
expect(pvp.isSingleVersion('==1.0')).toBeTrue();
});

it('should return false for ranges', () => {
expect(pvp.isSingleVersion('>=1.0 && <1.1')).toBeFalse();
});
});

describe('.subset(subRange, superRange)', () => {
it('1.1-1.2 is inside 1.0-2.0', () => {
expect(pvp.subset?.('>=1.0 && <1.1', '>=1.0 && <2.0')).toBeTrue();
});

it('1.0-2.0 is inside 1.0-2.0', () => {
expect(pvp.subset?.('>=1.0 && <2.0', '>=1.0 && <2.0')).toBeTrue();
});

it('1.0-2.1 outside 1.0-2.0', () => {
expect(pvp.subset?.('>=1.0 && <2.1', '>=1.0 && <2.0')).toBeFalse();
});

it('0.9-2.0 outside 1.0-2.0', () => {
expect(pvp.subset?.('>=0.9 && <2.1', '>=1.0 && <2.0')).toBeFalse();
});

it("returns undefined when passed a range that can't be parsed", () => {
expect(pvp.subset?.('gibberish', '')).toBeUndefined();
});
});

describe('.sortVersions()', () => {
it('should sort 1.0 and 1.1', () => {
expect(pvp.sortVersions('1.0', '1.1')).toBe(-1);
expect(pvp.sortVersions('1.1', '1.0')).toBe(1);
expect(pvp.sortVersions('1.0', '1.0')).toBe(0);
});
});

describe('.isStable()', () => {
it('should consider 0.0.0 stable', () => {
// in PVP, stability is not conveyed in the version number
// so we consider all versions stable
expect(pvp.isStable('0.0.0')).toBeTrue();
});
});

describe('.isCompatible()', () => {
it('should consider 0.0.0 compatible', () => {
// in PVP, there is no extra information besides the numbers
// so we consider all versions compatible
expect(pvp.isCompatible('0.0.0')).toBeTrue();
});
});
});
Loading