diff --git a/lib/modules/versioning/distro.spec.ts b/lib/modules/versioning/distro.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0dd67db08eab6aa48c943daa80019b0759fa900 --- /dev/null +++ b/lib/modules/versioning/distro.spec.ts @@ -0,0 +1,146 @@ +import { DistroInfo } from './distro'; + +describe('modules/versioning/distro', () => { + const di = new DistroInfo('data/ubuntu-distro-info.json'); + + it.each` + version | expected + ${'jammy'} | ${true} + ${'impish'} | ${true} + ${'hirsute'} | ${true} + ${'groovy'} | ${true} + ${'focal'} | ${true} + ${'eoan'} | ${true} + ${'Wily Werewolf'} | ${false} + ${'asdf'} | ${false} + ${'Yakkety'} | ${false} + `('isCodename("$version") === $expected', ({ version, expected }) => { + expect(di.isCodename(version)).toBe(expected); + }); + + it.each` + version | expected + ${'jammy'} | ${'22.04'} + ${'impish'} | ${'21.10'} + ${'hirsute'} | ${'21.04'} + ${'groovy'} | ${'20.10'} + ${'focal'} | ${'20.04'} + ${'eoan'} | ${'19.10'} + ${'asd'} | ${'asd'} + ${'16.06'} | ${'16.06'} + `( + 'getVersionByCodename("$version") === $expected', + ({ version, expected }) => { + expect(di.getVersionByCodename(version)).toBe(expected); + } + ); + + it.each` + version | expected + ${'22.04'} | ${'jammy'} + ${'21.10'} | ${'impish'} + ${'21.04'} | ${'hirsute'} + ${'20.10'} | ${'groovy'} + ${'20.04'} | ${'focal'} + ${'19.10'} | ${'eoan'} + ${'asd'} | ${'asd'} + ${'16.06'} | ${'16.06'} + `( + 'getCodenameByVersion("$version") === $expected', + ({ version, expected }) => { + expect(di.getCodenameByVersion(version)).toBe(expected); + } + ); + + it.each` + version | expected + ${'jammy'} | ${true} + ${'impish'} | ${true} + ${'hirsute'} | ${true} + ${'groovy'} | ${true} + ${'focal'} | ${true} + ${'Wily Werewolf'} | ${false} + ${'22.04'} | ${true} + ${'21.10'} | ${true} + ${'21.04'} | ${true} + ${'20.10'} | ${true} + ${'Wily Werewolf'} | ${false} + ${'asdf'} | ${false} + ${'Jellyfish'} | ${false} + `('exists("$version") === $expected', ({ version, expected }) => { + expect(di.exists(version)).toBe(expected); + }); + + it.each` + version | expected + ${'focal'} | ${false} + ${'groovy'} | ${true} + ${'hirsute'} | ${true} + ${'impish'} | ${false} + ${'jammy'} | ${false} + ${'20.04'} | ${false} + ${'20.10'} | ${true} + ${'21.04'} | ${true} + ${'21.10'} | ${false} + ${'22.04'} | ${false} + `('isEolLts("$version") === $expected', ({ version, expected }) => { + expect(di.isEolLts(version)).toBe(expected); + }); + + it('retrieves most recent release schedule with version', () => { + expect(di.getNLatest(0)).toEqual({ + codename: 'Jammy Jellyfish', + created: '2021-10-14', + eol: '2027-04-21', + eol_esm: '2032-04-21', + eol_server: '2027-04-21', + release: '2022-04-21', + series: 'jammy', + version: '22.04', + }); + }); + + it('sends an out of bound argument', () => { + expect(di.getNLatest(-1)).toBeNull(); + }); + + it('sends a float as an argument', () => { + expect(di.getNLatest(0.1)).toEqual({ + codename: 'Jammy Jellyfish', + created: '2021-10-14', + eol: '2027-04-21', + eol_esm: '2032-04-21', + eol_server: '2027-04-21', + release: '2022-04-21', + series: 'jammy', + version: '22.04', + }); + }); + + it('retrieves before most recent release schedule with version', () => { + expect(di.getNLatest(1)).toEqual({ + codename: 'Impish Indri', + series: 'impish', + created: '2021-04-22', + release: '2021-10-14', + eol: '2022-07-14', + version: '21.10', + }); + }); + + it('retrieves focal release schedule', () => { + expect(di.getSchedule('20.04')).toEqual({ + codename: 'Focal Fossa', + created: '2019-10-17', + eol: '2025-04-23', + eol_esm: '2030-04-23', + eol_server: '2025-04-23', + release: '2020-04-23', + series: 'focal', + }); + }); + + it('retrieves non-existent release schedule', () => { + expect(di.getSchedule('20.06')).toBeNull(); + }); +}); diff --git a/lib/modules/versioning/distro.ts b/lib/modules/versioning/distro.ts index 075c2adea6b2307235aadc4f6ffae03cf0cb37de..bb250a7b728a965ef9a8817bb9a63b04ca9f06dd 100644 --- a/lib/modules/versioning/distro.ts +++ b/lib/modules/versioning/distro.ts @@ -1,3 +1,4 @@ +import { DateTime } from 'luxon'; import dataFiles, { DataFile } from '../../data-files.generated'; interface DistroSchedule { @@ -12,7 +13,7 @@ interface DistroSchedule { eol_elts?: string; } -type DistroDataFile = 'data/ubuntu-distro-info.json'; +export type DistroDataFile = 'data/ubuntu-distro-info.json'; export type DistroInfoRecord = Record<string, DistroSchedule>; @@ -23,6 +24,9 @@ export class DistroInfo { string, DistroInfoRecordWithVersion >(); + + private readonly _sortedInfo: DistroInfoRecordWithVersion[] = []; + private readonly _distroInfo: DistroInfoRecord; constructor(distroJsonKey: DistroDataFile) { @@ -35,12 +39,45 @@ export class DistroInfo { const schedule = this._distroInfo[version]; this._codenameToVersion.set(schedule.series, { version, ...schedule }); } + + const arr = Object.keys(this._distroInfo).sort( + (a, b) => parseFloat(a) - parseFloat(b) + ); + + for (const v of arr) { + const obj = { version: v, ...this._distroInfo[v.toString()] }; + if (!obj.eol) { + // istanbul ignore next + continue; + } + this._sortedInfo.push(obj); + } } + /** + * Check if input is a valid release codename + * @param input A codename + * @returns true if input is a codename, false otherwise + */ public isCodename(input: string): boolean { return this._codenameToVersion.has(input); } + /** + * Checks if given input string is a valid release version + * @param input A codename/semVer + * @returns true if release exists, false otherwise + */ + public exists(input: string): boolean { + const ver = this.getVersionByCodename(input); + return !!this._distroInfo[ver]; + } + + /** + * Get semVer representation of a given codename + * @param input A codename + * @returns A semVer if exists, otherwise input string is returned + */ public getVersionByCodename(input: string): string { const schedule = this._codenameToVersion.get(input); if (schedule) { @@ -49,6 +86,11 @@ export class DistroInfo { return input; } + /** + * Get codename representation of a given semVer + * @param input A semVer + * @returns A codename if exists, otherwise input string is returned + */ public getCodenameByVersion(input: string): string { const di = this._distroInfo[input]; if (di) { @@ -58,7 +100,58 @@ export class DistroInfo { return input; } - public getSchedule(input: string): DistroSchedule { - return this._distroInfo[input]; + /** + * Get schedule of a given release + * @param input A codename/semVer + * @returns A schedule if available, otherwise undefined + */ + public getSchedule(input: string): DistroSchedule | null { + const ver = this.getVersionByCodename(input); + return this._distroInfo[ver] ?? null; + } + + /** + * Check if a given release has passed its EOL + * @param input A codename/semVer + * @returns false if still supported, true otherwise + */ + public isEolLts(input: string): boolean { + const ver = this.getVersionByCodename(input); + const schedule = this.getSchedule(ver); + const endLts = schedule?.eol ?? null; + let end = schedule?.eol_lts ?? null; + + // ubuntu: does not have eol_lts + // debian: only "Stable" has no eol_lts, old and oldold has both + if (!end) { + end = endLts; + } + + if (end) { + const now = DateTime.now(); + const eol = DateTime.fromISO(end); + return eol < now; + } + + // istanbul ignore next + return true; + } + + /** + * Get distro info for the release that has N other newer releases. + * Example: n=0 corresponds to the latest available release, n=1 the release before, etc. + * In Debian terms: N = 0 -> stable, N = 1 -> oldstable, N = 2 -> oldoldstalbe + * @param n + * @returns Distro info of the Nth latest release + */ + public getNLatest(n: number): DistroInfoRecordWithVersion | null { + const len = this._sortedInfo.length - 1; + const i = len - Math.floor(n); + + if (len >= i && i >= 0) { + return this._sortedInfo[i]; + } + + return null; } } diff --git a/lib/modules/versioning/ubuntu/index.spec.ts b/lib/modules/versioning/ubuntu/index.spec.ts index 1d286adfabc33bffeba6d73aa31adebe707adcd3..065d1bda9d143e53c9178169d8360fc90fcf049d 100644 --- a/lib/modules/versioning/ubuntu/index.spec.ts +++ b/lib/modules/versioning/ubuntu/index.spec.ts @@ -79,7 +79,7 @@ describe('modules/versioning/ubuntu/index', () => { ${'impish'} | ${true} ${'jammy'} | ${true} `('isValid("$version") === $expected', ({ version, expected }) => { - expect(!!ubuntu.isValid(version)).toBe(expected); + expect(ubuntu.isValid(version)).toBe(expected); }); test.each` @@ -94,8 +94,7 @@ describe('modules/versioning/ubuntu/index', () => { `( 'isCompatible("$version") === $expected', ({ version, range, expected }) => { - const res = ubuntu.isCompatible(version, range); - expect(!!res).toBe(expected); + expect(ubuntu.isCompatible(version, range)).toBe(expected); } ); @@ -107,7 +106,7 @@ describe('modules/versioning/ubuntu/index', () => { ${'20.04'} | ${true} ${'>=20.04'} | ${false} `('isSingleVersion("$version") === $expected', ({ version, expected }) => { - expect(!!ubuntu.isSingleVersion(version)).toBe(expected); + expect(ubuntu.isSingleVersion(version)).toBe(expected); }); test.each` @@ -198,8 +197,7 @@ describe('modules/versioning/ubuntu/index', () => { ${'impish'} | ${false} ${'jammy'} | ${false} `('isStable("$version") === $expected', ({ version, expected }) => { - const res = !!ubuntu.isStable(version); - expect(res).toBe(expected); + expect(ubuntu.isStable(version)).toBe(expected); }); test.each` @@ -252,7 +250,7 @@ describe('modules/versioning/ubuntu/index', () => { ${'impish-'} | ${false} ${'JAMMY'} | ${false} `('isVersion("$version") === $expected', ({ version, expected }) => { - expect(!!ubuntu.isVersion(version)).toBe(expected); + expect(ubuntu.isVersion(version)).toBe(expected); }); test.each`