Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
184 changes: 184 additions & 0 deletions core/test/generated-index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
const assert = require('node:assert');
const { describe, it } = require('node:test');
const fc = require('fast-check');
const semver = require('semver');
const nodevu = require('../index');
const { at } = require('@nodevu/fixtures');
const snapshot = require('@nodevu/fixtures/data/index.json');

// every generated release is out by then
const now = '2027-01-01';

describe('core on a generated index.json', async () => {
it('should keep every release as the index lists it, in any order', async () => {
await fc.assert(
fc.asyncProperty(index(), async (entries) => {
// Arrange
const options = at(now, { index: entries, schedule: {} });

// Act
const data = await nodevu(options);

// Assert
const keys = Object.values(data).flatMap(({ releases }) =>
Object.keys(releases),
);

assert.strictEqual(keys.length, entries.length);

for (const entry of entries) {
const { line, key } = place(entry.version);
const release = data[line].releases[key];

const linked = Object.values(release.files.links).flatMap((links) =>
links.map(({ id }) => id),
);

assert.strictEqual(release.semver.line, line);
assert.strictEqual(release.releaseDate, entry.date);
assert.strictEqual(release.lts.isLts, Boolean(entry.lts));
assert.strictEqual(release.security.isSecurity, entry.security);
assert.strictEqual(
release.alpha.isAlpha,
entry.version.includes('-alpha.'),
);
assert.deepStrictEqual(release.files.available, entry.files ?? []);
assert.deepStrictEqual(
linked.sort(),
[...(entry.files ?? [])].sort(),
);
}

for (const [line, { releases, security }] of Object.entries(data)) {
const flagged = Object.values(releases)
.filter((release) => release.security.isSecurity)
.map((release) => release.semver.raw);

assert.deepStrictEqual(
[...security.all].sort(),
flagged.sort(),
line,
);
}
}),
);
});

it('should order security and LTS releases by version on a newest-first index', async () => {
await fc.assert(
fc.asyncProperty(index(), async (shuffled) => {
// Arrange
// index.json lists releases newest first, and core relies on it
const entries = [...shuffled].sort((a, b) =>
semver.rcompare(place(a.version).raw, place(b.version).raw),
);

const lines = new Set(
entries.map((entry) => place(entry.version).line),
);

const schedule = Object.fromEntries(
[...lines].map((line) => [line, { start: '2000-01-01' }]),
);

// Act
const data = await nodevu(at(now, { index: entries, schedule }));

// Assert
for (const [line, { releases, security, support }] of Object.entries(
data,
)) {
const all = Object.values(releases).map(({ semver }) => semver.raw);

const lts = Object.values(releases)
.filter((release) => release.lts.isLts)
.map((release) => release.semver.raw)
.sort(semver.rcompare);

assert.deepStrictEqual(
security.all,
[...security.all].sort(semver.rcompare),
line,
);
assert.strictEqual(security.newest, security.all[0], line);
assert.strictEqual(security.oldest, security.all.at(-1), line);
assert.strictEqual(support?.lts?.oldest, lts.at(-1), line);

// newest is only set when the line's newest release is LTS, which
// is always true of index.json, since a line stays LTS once it is
const isNewestLts = releases[`v${all[0]}`].lts.isLts;
assert.strictEqual(
support?.lts?.newest,
isNewestLts ? lts[0] : undefined,
line,
);
}
}),
);
});
});

// Helpers

/**
* Where core files a version: its release line and its key in that line.
*
* @param {string} version
*/
function place(version) {
const parsed = semver.coerce(version, { includePrerelease: true });
assert.ok(parsed);

return {
raw: parsed.version,
key: `v${parsed.version}`,
line: parsed.major === 0 ? `v0.${parsed.minor}` : `v${parsed.major}`,
};
}

// releases unique by version, in any order, with odd lts values, unknown file
// ids and missing files
function index() {
const ids = [...new Set(snapshot.flatMap((release) => release.files))];

return fc.uniqueArray(
fc.record(
{
version: version(),
date: fc
.date({
min: new Date('2009-06-01'),
max: new Date('2026-12-31'),
noInvalidDate: true,
})
.map((date) => date.toISOString().slice(0, 10)),
lts: fc.oneof(fc.constant(false), fc.constant(true), fc.string()),
security: fc.boolean(),
files: fc.array(fc.oneof(fc.constantFrom(...ids), fc.string()), {
maxLength: 5,
}),
},
{ requiredKeys: ['version', 'date', 'lts', 'security'] },
),
{ selector: (entry) => place(entry.version).key, maxLength: 20 },
);
}

// a version as index.json writes it, including 0.x lines and Alpha releases
function version() {
return fc.oneof(
fc
.tuple(fc.nat({ max: 12 }), fc.nat({ max: 5 }))
.map(([minor, patch]) => `v0.${minor}.${patch}`),
fc
.tuple(
fc.integer({ min: 1, max: 30 }),
fc.nat({ max: 5 }),
fc.nat({ max: 5 }),
)
.map(([major, minor, patch]) => `v${major}.${minor}.${patch}`),
fc
.tuple(fc.integer({ min: 1, max: 30 }), fc.nat({ max: 5 }))
.map(([major, alpha]) => `v${major}.0.0-alpha.${alpha}.0.0`),
);
}
171 changes: 171 additions & 0 deletions core/test/generated-schedule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
const assert = require('node:assert');
const { describe, it } = require('node:test');
const fc = require('fast-check');
const { DateTime } = require('luxon');
const nodevu = require('../index');
const { at } = require('@nodevu/fixtures');
const snapshot = require('@nodevu/fixtures/data/schedule.json');

const phases = /** @type {const} */ ([
'alpha',
'start',
'lts',
'maintenance',
'end',
]);

const lines = ['v20', 'v21', 'v22'];

// one release per line
const index = lines.map((line) => ({
version: `${line}.0.0`,
date: '2000-01-01',
files: ['src'],
}));

describe('core on a generated schedule.json', async () => {
it('should be in the latest phase that has begun, whatever the schedule holds', async () => {
await fc.assert(
fc.asyncProperty(schedule(), date(), async (generated, now) => {
// Arrange
const options = at('2027-01-01', { index, schedule: generated });

// Act
const data = await nodevu({ ...options, now });

// Assert
for (const line of lines) {
const support = data[line].support;

if (generated[line]?.start === undefined) {
assert.strictEqual(support, undefined, line);
continue;
}

assert.ok(support, line);

const begun = phases.filter((phase) => {
const start = DateTime.fromISO(support.phases.dates[phase] ?? '');

return start.isValid && start <= DateTime.fromISO(now);
});

assert.deepStrictEqual(
support.phases.current,
begun.at(-1) ?? {},
`${line}: ${JSON.stringify(generated[line])} on ${now}`,
);
assert.strictEqual(support.codename, generated[line].codename);

for (const phase of phases) {
assert.strictEqual(
support.phases.dates[phase],
generated[line][phase],
line,
);
}
}
}),
);
});

it('should give the same phases for one moment written in any zone', async () => {
// the days a phase starts on, where a moment read in the wrong zone
// lands in the wrong phase
const starts = Object.values(snapshot)
.flatMap((line) => Object.values(line))
.filter(
(value) => /^\d{4}-\d{2}-\d{2}$/.test(value) && value <= '2026-10-01',
);

await fc.assert(
fc.asyncProperty(
fc.oneof(day(), fc.constantFrom(...starts)),
zone(),
async (now, zone) => {
// Arrange
const zoned = DateTime.fromISO(now).setZone(zone);
const options = at(now);

// Act
const results = await Promise.all(
[now, zoned, String(zoned.toISO())].map((written) =>
nodevu({ ...options, now: written }),
),
);

// Assert
const [expected, ...others] = results.map((data) =>
Object.fromEntries(
Object.entries(data).map(([line, { support }]) => [
line,
support?.phases.current,
]),
),
);

for (const other of others) {
assert.deepStrictEqual(other, expected);
}
},
),
);
});
});

// Helpers

// a day, as YYYY-MM-DD
function day() {
return fc
.date({
min: new Date('2009-06-01'),
max: new Date('2026-10-01'),
noInvalidDate: true,
})
.map((date) => date.toISOString().slice(0, 10));
}

// a day, or a moment with a time and an offset
function date() {
return fc.oneof(
day(),
fc
.tuple(day(), fc.integer({ min: 0, max: 86_399 }), zone())
.map(([on, seconds, zone]) =>
String(DateTime.fromISO(on, { zone }).plus({ seconds }).toISO()),
),
);
}

function zone() {
return fc.constantFrom(
'UTC',
'America/New_York',
'Asia/Kolkata',
'Pacific/Chatham',
'UTC+14',
'UTC-12',
);
}

// a schedule.json for each line, with phases missing, out of order, written
// with times and offsets, or unreadable
function schedule() {
const phaseDate = fc.oneof(date(), fc.string());

return fc.dictionary(
fc.constantFrom(...lines),
fc.record(
{
alpha: phaseDate,
start: phaseDate,
lts: phaseDate,
maintenance: phaseDate,
end: phaseDate,
codename: fc.string(),
},
{ requiredKeys: [] },
),
);
}
Loading
Loading