From d8790704289b0bd633ae57bd130019dc41587c0b Mon Sep 17 00:00:00 2001 From: botandbang <337765928+botandbang@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:47:04 -0400 Subject: [PATCH 1/5] fix(core): return empty files for a release without any A release in index.json with no files key made core throw, since files.available fell back to undefined and the links loop called Object.keys on it. It now falls back to an empty array. --- core/index.js | 2 +- core/test/files.js | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 core/test/files.js diff --git a/core/index.js b/core/index.js index 34e0881..ba5ee87 100644 --- a/core/index.js +++ b/core/index.js @@ -80,7 +80,7 @@ async function core(options) { // TODO: parse versions[version].files and convert them to URLs that can be directly accessed data[name].releases[`v${versionSemver.version}`].files.available = - versions[version].files ?? undefined; + versions[version].files ?? []; const availableShorthand = data[name].releases[`v${versionSemver.version}`].files.available; // since we're going to be writing this a lot for assignments, it's nice to have shorthand for readability diff --git a/core/test/files.js b/core/test/files.js new file mode 100644 index 0000000..69d9894 --- /dev/null +++ b/core/test/files.js @@ -0,0 +1,22 @@ +const assert = require('node:assert'); +const { describe, it } = require('node:test'); +const nodevu = require('../index'); + +describe('files for a release that index.json lists without any', async () => { + it('should return no available files and no links', async () => { + const data = await nodevu({ + fetch: async (url) => + Response.json( + url.endsWith('schedule.json') + ? {} + : [{ version: 'v28.0.0', date: '2028-01-01', lts: false }], + ), + now: '2028-01-01', + }); + + assert.deepStrictEqual(data.v28.releases['v28.0.0'].files, { + available: [], + links: {}, + }); + }); +}); From a3f1b484b8ead1f30e5f43d6ff68361e223af4f8 Mon Sep 17 00:00:00 2001 From: botandbang <337765928+botandbang@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:47:19 -0400 Subject: [PATCH 2/5] fix(parsefiles): list both source tarballs The src case assigned files[0] twice, so the .tar.gz name was overwritten and only the .tar.xz was returned. It now lists both, like headers. --- parsefiles/index.js | 2 +- parsefiles/test/data/expected.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/parsefiles/index.js b/parsefiles/index.js index fccffb5..a95ad92 100644 --- a/parsefiles/index.js +++ b/parsefiles/index.js @@ -52,7 +52,7 @@ function parseFilename(file, version) { } else if (info.files[0] === 'src') { // source filenames. info.files[0] = `node-v${version}.tar.gz`; - info.files[0] = `node-v${version}.tar.xz`; + info.files.push(`node-v${version}.tar.xz`); } else if ( info.files[0].includes('linux-') || info.files[0] === 'headers' || diff --git a/parsefiles/test/data/expected.json b/parsefiles/test/data/expected.json index b4766dd..28b9358 100644 --- a/parsefiles/test/data/expected.json +++ b/parsefiles/test/data/expected.json @@ -72,7 +72,7 @@ }, { "id": "src", - "files": ["node-v17.7.0.tar.xz"], + "files": ["node-v17.7.0.tar.gz", "node-v17.7.0.tar.xz"], "type": "source" }, { From d4b969d954d7abb8df9ca9eac563f99c68813001 Mon Sep 17 00:00:00 2001 From: botandbang <337765928+botandbang@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:47:20 -0400 Subject: [PATCH 3/5] fix(core): write now.json as a plain ISO string updateStaticNow stringified the DateTime and write() stringified it again, so every reader had to JSON.parse it a second time. The script now hands write() the DateTime, the snapshot is re-encoded, and the extra JSON.parse calls are gone. --- core/test/combos.js | 4 +--- core/test/data/static/now.json | 2 +- core/test/dynamic-values.js | 4 +--- core/util/dev/updateStaticNow.js | 6 ++---- ranges/test/emptyFields.test.js | 2 +- 5 files changed, 6 insertions(+), 12 deletions(-) diff --git a/core/test/combos.js b/core/test/combos.js index b51653c..d3aa96a 100644 --- a/core/test/combos.js +++ b/core/test/combos.js @@ -26,9 +26,7 @@ function check(data) { assert.deepStrictEqual(data.v12.security.oldest, '12.8.1'); } -const staticNow = require('./data/static/now.json'); - -const now = JSON.parse(staticNow); +const now = require('./data/static/now.json'); describe('check to make sure that combining options works as expected', async () => { beforeEach(beforeEachTemplate); diff --git a/core/test/data/static/now.json b/core/test/data/static/now.json index a9247f9..5ea1020 100644 --- a/core/test/data/static/now.json +++ b/core/test/data/static/now.json @@ -1 +1 @@ -"\"2025-10-30T08:51:04.858-07:00\"" +"2025-10-30T08:51:04.858-07:00" diff --git a/core/test/dynamic-values.js b/core/test/dynamic-values.js index f4875c9..aba3639 100644 --- a/core/test/dynamic-values.js +++ b/core/test/dynamic-values.js @@ -4,9 +4,7 @@ const { describe, it, beforeEach } = require('node:test'); const beforeEachTemplate = require('../util/dev/beforeEachTemplate'); -const staticNow = require('./data/static/now.json'); - -const now = JSON.parse(staticNow); +const now = require('./data/static/now.json'); // this is a set of tests that only test against static data effectively frozen at a point in time. // these tests should *probably* be updated sometimes. see the npm scripts for update scripts. diff --git a/core/util/dev/updateStaticNow.js b/core/util/dev/updateStaticNow.js index 3851311..c8cb6b9 100644 --- a/core/util/dev/updateStaticNow.js +++ b/core/util/dev/updateStaticNow.js @@ -2,10 +2,8 @@ const { DateTime } = require('luxon'); const write = require('./write'); async function updateStaticDateTime() { - await write( - './test/data/static/now.json', - JSON.stringify(DateTime.now(), null, 2), - ); + // write serializes the DateTime to its ISO string + await write('./test/data/static/now.json', DateTime.now()); } updateStaticDateTime(); diff --git a/ranges/test/emptyFields.test.js b/ranges/test/emptyFields.test.js index 285079d..6c19dc5 100644 --- a/ranges/test/emptyFields.test.js +++ b/ranges/test/emptyFields.test.js @@ -4,7 +4,7 @@ const { at } = require('./fixtures'); const { describe, it } = require('node:test'); // core's snapshot time, when the Current line hasn't had a security release yet -const snapshotNow = JSON.parse(require('../../core/test/data/static/now.json')); +const snapshotNow = require('../../core/test/data/static/now.json'); describe('fields without a value should be undefined, not "undefined"', async () => { it('should leave out the security fields of a Current line without security releases', async () => { From ff61fdc848bb529c021774a755c3195c0b9d87b7 Mon Sep 17 00:00:00 2001 From: botandbang <337765928+botandbang@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:47:22 -0400 Subject: [PATCH 4/5] fix(static): declare @nodevu/core as a dev dependency The build scripts require @nodevu/core but static never listed it, so it only resolved when every workspace was installed. npm install -w static now links it. --- static/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/static/package.json b/static/package.json index ac0860d..1e53adc 100644 --- a/static/package.json +++ b/static/package.json @@ -38,6 +38,7 @@ }, "homepage": "https://github.com/cutenode/nodevu#readme", "devDependencies": { + "@nodevu/core": "^1.1.0", "c8": "^10.1.2" } } From a445f39a7c821d8ac3067b9d5a60123c0485e55a Mon Sep 17 00:00:00 2001 From: "tierney cyren (bot version)" Date: Wed, 21 Oct 2026 08:45:36 -0400 Subject: [PATCH 5/5] test: cover every package at 100% and enforce it in CI (#108) Co-authored-by: botandbang <337765928+botandbang@users.noreply.github.com> --- .c8rc.json | 9 ++ .github/workflows/tests-core.yml | 4 +- .github/workflows/tests-fetchindex.yml | 4 +- .github/workflows/tests-newest.yml | 4 +- .github/workflows/tests-oldest.yml | 4 +- .github/workflows/tests-opt.yml | 4 +- .github/workflows/tests-parsefiles.yml | 4 +- .github/workflows/tests-ranges.yml | 4 +- .github/workflows/tests-static.yml | 4 +- .github/workflows/tests-translate.yml | 4 +- core/index.js | 22 ++--- core/package.json | 1 + core/test/dev-fixtures.js | 84 +++++++++++++++++++ core/test/dev-scripts.js | 80 ++++++++++++++++++ core/test/dev-write.js | 22 +++++ core/test/null-values.js | 59 +++++++++++++ core/test/phases.js | 69 +++++++++++++++ package.json | 2 +- parsefiles/test/test.js | 32 +++++-- static/test/test.build.js | 112 +++++++++++++++++++++++++ static/test/test.write.js | 39 +++++++++ translate/test/examples.test.js | 25 ++++++ 22 files changed, 550 insertions(+), 42 deletions(-) create mode 100644 .c8rc.json create mode 100644 core/test/dev-fixtures.js create mode 100644 core/test/dev-scripts.js create mode 100644 core/test/dev-write.js create mode 100644 core/test/null-values.js create mode 100644 core/test/phases.js create mode 100644 static/test/test.build.js create mode 100644 static/test/test.write.js create mode 100644 translate/test/examples.test.js diff --git a/.c8rc.json b/.c8rc.json new file mode 100644 index 0000000..0768307 --- /dev/null +++ b/.c8rc.json @@ -0,0 +1,9 @@ +{ + "all": true, + "exclude": ["coverage/**", "test/**"], + "check-coverage": true, + "branches": 100, + "functions": 100, + "lines": 100, + "statements": 100 +} diff --git a/.github/workflows/tests-core.yml b/.github/workflows/tests-core.yml index f07d61c..28537b0 100644 --- a/.github/workflows/tests-core.yml +++ b/.github/workflows/tests-core.yml @@ -27,5 +27,5 @@ jobs: run: npm install -g npm - name: Run npm install -w core run: npm install -w core - - name: Run npm test -w core - run: npm test -w core + - name: Run npm run coverage -w core + run: npm run coverage -w core diff --git a/.github/workflows/tests-fetchindex.yml b/.github/workflows/tests-fetchindex.yml index 271a2ed..3fae39a 100644 --- a/.github/workflows/tests-fetchindex.yml +++ b/.github/workflows/tests-fetchindex.yml @@ -27,5 +27,5 @@ jobs: run: npm install -g npm - name: Run npm install -w fetchindex run: npm install -w fetchindex - - name: Run npm test -w fetchindex - run: npm test -w fetchindex + - name: Run npm run coverage -w fetchindex + run: npm run coverage -w fetchindex diff --git a/.github/workflows/tests-newest.yml b/.github/workflows/tests-newest.yml index 5614d3a..36f0257 100644 --- a/.github/workflows/tests-newest.yml +++ b/.github/workflows/tests-newest.yml @@ -28,5 +28,5 @@ jobs: run: npm install -g npm - name: Run npm install -w newest run: npm install -w newest - - name: Run npm test -w newest - run: npm test -w newest + - name: Run npm run coverage -w newest + run: npm run coverage -w newest diff --git a/.github/workflows/tests-oldest.yml b/.github/workflows/tests-oldest.yml index c1bffa2..7395877 100644 --- a/.github/workflows/tests-oldest.yml +++ b/.github/workflows/tests-oldest.yml @@ -28,5 +28,5 @@ jobs: run: npm install -g npm - name: Run npm install -w oldest run: npm install -w oldest - - name: Run npm test -w oldest - run: npm test -w oldest + - name: Run npm run coverage -w oldest + run: npm run coverage -w oldest diff --git a/.github/workflows/tests-opt.yml b/.github/workflows/tests-opt.yml index a8d69d0..3d8949f 100644 --- a/.github/workflows/tests-opt.yml +++ b/.github/workflows/tests-opt.yml @@ -27,5 +27,5 @@ jobs: run: npm install -g npm - name: Run npm install -w opt run: npm install -w opt - - name: Run npm test -w opt - run: npm test -w opt + - name: Run npm run coverage -w opt + run: npm run coverage -w opt diff --git a/.github/workflows/tests-parsefiles.yml b/.github/workflows/tests-parsefiles.yml index 83e842c..b668c62 100644 --- a/.github/workflows/tests-parsefiles.yml +++ b/.github/workflows/tests-parsefiles.yml @@ -27,5 +27,5 @@ jobs: run: npm install -g npm - name: Run npm install -w parsefiles run: npm install -w parsefiles - - name: Run npm test -w parsefiles - run: npm test -w parsefiles + - name: Run npm run coverage -w parsefiles + run: npm run coverage -w parsefiles diff --git a/.github/workflows/tests-ranges.yml b/.github/workflows/tests-ranges.yml index 7c3396e..bb43f5d 100644 --- a/.github/workflows/tests-ranges.yml +++ b/.github/workflows/tests-ranges.yml @@ -29,5 +29,5 @@ jobs: run: npm install -g npm - name: Run npm install -w ranges run: npm install -w ranges - - name: Run npm test -w ranges - run: npm test -w ranges + - name: Run npm run coverage -w ranges + run: npm run coverage -w ranges diff --git a/.github/workflows/tests-static.yml b/.github/workflows/tests-static.yml index b3162fc..33c9fc0 100644 --- a/.github/workflows/tests-static.yml +++ b/.github/workflows/tests-static.yml @@ -27,5 +27,5 @@ jobs: run: npm install -g npm - name: Run npm install -w static run: npm install -w static - - name: Run npm test -w static - run: npm test -w static + - name: Run npm run coverage -w static + run: npm run coverage -w static diff --git a/.github/workflows/tests-translate.yml b/.github/workflows/tests-translate.yml index d8470d7..1522acd 100644 --- a/.github/workflows/tests-translate.yml +++ b/.github/workflows/tests-translate.yml @@ -30,5 +30,5 @@ jobs: run: npm install -g npm - name: Run npm install -w translate run: npm install -w translate - - name: Run npm test -w translate - run: npm test -w translate + - name: Run npm run coverage -w translate + run: npm run coverage -w translate diff --git a/core/index.js b/core/index.js index ba5ee87..298a378 100644 --- a/core/index.js +++ b/core/index.js @@ -209,24 +209,18 @@ async function core(options) { } function determineCurrentReleasePhase(now, dates = {}) { - // we set set this up to enable custom `now` passing, since passing an ISO - // const usableNow = DateTime.fromISO(now) - // here we figure out if the dates for each release line passed is in the past or future // `true` is in the past // `false` is in the future + // `undefined` is a missing date const isoified = { - alpha: - isInPast(DateTime.fromISO(dates.alpha).diff(now).toMillis()) ?? undefined, - start: - isInPast(DateTime.fromISO(dates.start).diff(now).toMillis()) ?? undefined, - lts: - isInPast(DateTime.fromISO(dates.lts).diff(now).toMillis()) ?? undefined, - maintenance: - isInPast(DateTime.fromISO(dates.maintenance).diff(now).toMillis()) ?? - undefined, - end: - isInPast(DateTime.fromISO(dates.end).diff(now).toMillis()) ?? undefined, + alpha: isInPast(DateTime.fromISO(dates.alpha).diff(now).toMillis()), + start: isInPast(DateTime.fromISO(dates.start).diff(now).toMillis()), + lts: isInPast(DateTime.fromISO(dates.lts).diff(now).toMillis()), + maintenance: isInPast( + DateTime.fromISO(dates.maintenance).diff(now).toMillis(), + ), + end: isInPast(DateTime.fromISO(dates.end).diff(now).toMillis()), }; // set up our result to return diff --git a/core/package.json b/core/package.json index 89e6389..8c1d702 100644 --- a/core/package.json +++ b/core/package.json @@ -28,6 +28,7 @@ }, "devDependencies": { "c8": "^10.1.2", + "fast-check": "^4.10.2", "undici": "^6.20.1" }, "repository": { diff --git a/core/test/dev-fixtures.js b/core/test/dev-fixtures.js new file mode 100644 index 0000000..ca9020f --- /dev/null +++ b/core/test/dev-fixtures.js @@ -0,0 +1,84 @@ +const assert = require('node:assert'); +const { describe, it } = require('node:test'); +const fc = require('fast-check'); +const { at } = require('../util/dev/fixtures'); +const schedule = require('./data/static/schedule.json'); + +describe('at', async () => { + it('should pass now through as an option', () => { + // Arrange + const now = '2025-09-01'; + + // Act + const options = at(now); + + // Assert + assert.strictEqual(options.now, now); + }); + + it("should serve core's schedule.json snapshot by default", async () => { + // Arrange + const { fetch } = at('2025-09-01'); + + // Act + const response = await fetch( + 'https://raw.githubusercontent.com/nodejs/Release/master/schedule.json', + ); + + // Assert + assert.deepStrictEqual(await response.json(), schedule); + }); + + it('should only serve the releases that were out by now', async () => { + // Arrange + const index = [ + { version: 'v24.8.0', date: '2025-09-10' }, + { version: 'v24.7.0', date: '2025-08-27' }, + ]; + + const { fetch } = at('2025-09-01', { index, schedule: {} }); + + // Act + const response = await fetch('https://nodejs.org/dist/index.json'); + + // Assert + assert.deepStrictEqual(await response.json(), [ + { version: 'v24.7.0', date: '2025-08-27' }, + ]); + }); + + it('should throw for any other URL', async () => { + // Arrange + const { fetch } = at('2025-09-01'); + + // Act + const response = fetch('https://nodejs.org/dist/v24.7.0/SHASUMS256.txt'); + + // Assert + await assert.rejects(response, /No fixture data/); + }); + + it('should never serve a release from after now', async () => { + await fc.assert( + fc.asyncProperty( + fc.date({ + min: new Date('2009-01-01'), + max: new Date('2100-01-01'), + noInvalidDate: true, + }), + async (date) => { + // Arrange + const now = date.toISOString().slice(0, 10); + const { fetch } = at(now); + + // Act + const response = await fetch('https://nodejs.org/dist/index.json'); + + // Assert + const releases = await response.json(); + assert.ok(releases.every((release) => release.date <= now)); + }, + ), + ); + }); +}); diff --git a/core/test/dev-scripts.js b/core/test/dev-scripts.js new file mode 100644 index 0000000..5c0147f --- /dev/null +++ b/core/test/dev-scripts.js @@ -0,0 +1,80 @@ +const assert = require('node:assert'); +const { execFile } = require('node:child_process'); +const { mkdir, mkdtemp, readFile } = require('node:fs/promises'); +const { tmpdir } = require('node:os'); +const { join } = require('node:path'); +const { describe, it } = require('node:test'); +const { promisify } = require('node:util'); +const { DateTime } = require('luxon'); +const index = require('./data/static/index.json'); +const schedule = require('./data/static/schedule.json'); + +describe('test:update:static', async () => { + it('should save the index.json and schedule.json it downloads', async () => { + // Arrange + const cwd = await snapshotDirectory(); + + // Act + await runScript( + 'updateStaticData.js', + `require(${JSON.stringify(require.resolve('../util/dev/beforeEachTemplate'))})();`, + cwd, + ); + + // Assert + assert.deepStrictEqual( + { + index: await readSnapshot(cwd, 'index.json'), + schedule: await readSnapshot(cwd, 'schedule.json'), + }, + { index, schedule }, + ); + }); +}); + +describe('test:update:now', async () => { + it('should save the current time as an ISO string', async () => { + // Arrange + const cwd = await snapshotDirectory(); + const now = Date.parse('2025-10-30T15:51:04.858Z'); + + // Act + await runScript( + 'updateStaticNow.js', + `require(${JSON.stringify(require.resolve('luxon'))}).Settings.now = () => ${now};`, + cwd, + ); + + // Assert + const saved = await readSnapshot(cwd, 'now.json'); + assert.strictEqual(DateTime.fromISO(saved).toMillis(), now); + }); +}); + +// Helpers + +// a directory laid out like core, for the scripts to write their snapshots into +async function snapshotDirectory() { + const cwd = await mkdtemp(join(tmpdir(), 'nodevu-')); + await mkdir(join(cwd, 'test/data/static'), { recursive: true }); + + return cwd; +} + +async function readSnapshot(cwd, file) { + return JSON.parse( + await readFile(join(cwd, 'test/data/static', file), 'utf8'), + ); +} + +// runs a util/dev script the way its npm script does, after `setup` stubs +// whatever it reaches outside the process for +function runScript(script, setup, cwd) { + const path = require.resolve(`../util/dev/${script}`); + + return promisify(execFile)( + process.execPath, + ['-e', `${setup} require(${JSON.stringify(path)});`], + { cwd }, + ); +} diff --git a/core/test/dev-write.js b/core/test/dev-write.js new file mode 100644 index 0000000..7d39329 --- /dev/null +++ b/core/test/dev-write.js @@ -0,0 +1,22 @@ +const assert = require('node:assert'); +const { mkdtemp, readFile } = require('node:fs/promises'); +const { tmpdir } = require('node:os'); +const { join } = require('node:path'); +const { describe, it } = require('node:test'); +const write = require('../util/dev/write'); + +describe('write', async () => { + it('should save data as tab-indented JSON', async () => { + // Arrange + const path = join(await mkdtemp(join(tmpdir(), 'nodevu-')), 'data.json'); + + // Act + await write(path, { v22: { codename: 'Jod' } }); + + // Assert + assert.strictEqual( + await readFile(path, 'utf8'), + '{\n\t"v22": {\n\t\t"codename": "Jod"\n\t}\n}', + ); + }); +}); diff --git a/core/test/null-values.js b/core/test/null-values.js new file mode 100644 index 0000000..1f138fb --- /dev/null +++ b/core/test/null-values.js @@ -0,0 +1,59 @@ +const assert = require('node:assert'); +const { describe, it } = require('node:test'); +const nodevu = require('../index'); + +const now = '2028-01-01'; + +function run(schedule, index) { + return nodevu({ + fetch: async (url) => + Response.json(url.endsWith('schedule.json') ? schedule : index), + now, + }); +} + +// JSON can't hold undefined, so a value index.json or schedule.json doesn't +// know yet could come through as null. core reports those as undefined, the +// same as a missing key. +describe('null values in index.json and schedule.json', async () => { + it('should report a null release date as undefined', async () => { + // Arrange + const index = [ + { version: 'v28.0.0', date: null, files: ['src'], lts: false }, + ]; + + // Act + const data = await run({}, index); + + // Assert + assert.strictEqual(data.v28.releases['v28.0.0'].releaseDate, undefined); + }); + + it('should report a null start date as undefined', async () => { + // Arrange + const schedule = { v28: { start: null, end: '2031-04-30' } }; + + // Act + const data = await run(schedule, [release('v28.0.0')]); + + // Assert + assert.strictEqual(data.v28.support.phases.dates.start, undefined); + }); + + it('should report a null end date as undefined', async () => { + // Arrange + const schedule = { v28: { start: '2027-04-20', end: null } }; + + // Act + const data = await run(schedule, [release('v28.0.0')]); + + // Assert + assert.strictEqual(data.v28.support.phases.dates.end, undefined); + }); +}); + +// Helpers + +function release(version) { + return { version, date: '2027-04-20', files: ['src'], lts: false }; +} diff --git a/core/test/phases.js b/core/test/phases.js new file mode 100644 index 0000000..bd8a739 --- /dev/null +++ b/core/test/phases.js @@ -0,0 +1,69 @@ +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 phases = ['alpha', 'start', 'lts', 'maintenance', 'end']; + +describe('support.phases.current', async () => { + it('should always be the newest phase that has begun by now', async () => { + await fc.assert( + fc.asyncProperty(fc.gen(), async (g) => { + // Arrange + const phaseDates = [ + DateTime.fromJSDate( + g(fc.date, { + min: new Date('1970-01-01'), + max: new Date('2200-01-01'), + noInvalidDate: true, + }), + ).startOf('day'), + ]; + + for (let i = 1; i < phases.length; i++) { + phaseDates.push(phaseDates[i - 1].plus({ days: g(daysBetween) })); + } + + const phase = g(fc.nat, { max: phases.length - 1 }); + + const nextPhase = phaseDates[phase + 1]; + + const daysInPhase = nextPhase + ? nextPhase.diff(phaseDates[phase], 'days').days + : g(daysBetween); + + const now = phaseDates[phase].plus({ + days: g(fc.nat, { max: daysInPhase - 1 }), + }); + + const schedule = { + v30: Object.fromEntries( + phases.map((name, i) => [name, phaseDates[i].toISODate()]), + ), + }; + + // Act + const data = await nodevu({ + fetch: async (url) => + Response.json( + url.endsWith('schedule.json') + ? schedule + : [{ version: 'v30.0.0', files: ['src'], lts: false }], + ), + now: now.toISODate(), + }); + + // Assert + assert.strictEqual(data.v30.support.phases.current, phases[phase]); + }), + ); + }); +}); + +// Helpers + +// phases are at least a day apart, and bounded so every date stays valid +function daysBetween() { + return fc.integer({ min: 1, max: 5000 }); +} diff --git a/package.json b/package.json index c4c9e4a..c963d51 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "lint": "oxlint && oxfmt --check", "lint:write": "oxlint --fix && oxfmt", - "coverage": "npm run coverage --ws", + "coverage": "npm run coverage --ws --if-present", "test": "npm run test --ws", "updates": "npm run updates --ws" }, diff --git a/parsefiles/test/test.js b/parsefiles/test/test.js index 1b839ad..f96fb67 100644 --- a/parsefiles/test/test.js +++ b/parsefiles/test/test.js @@ -28,16 +28,30 @@ describe('check if sunos support is working as expected', () => { assert.deepEqual(result.id, id); assert.deepEqual(result.type, 'sunos'); }); + + it('should return the correct sunos x86 version string', () => { + const id = 'sunos-x86'; + const result = parseFiles(id, mockverson); + assert.deepEqual(result.architecture, 'x86'); + assert.deepEqual(result.files, [ + `node-v${mockverson}-sunos-x86.tar.gz`, + `node-v${mockverson}-sunos-x86.tar.xz`, + ]); + assert.deepEqual(result.id, id); + assert.deepEqual(result.type, 'sunos'); + }); }); describe('check if the armv6l support is working as expected', () => { - const id = 'linux-armv6l'; - const result = parseFiles(id, mockverson); - assert.deepEqual(result.architecture, 'armv6l'); - assert.deepEqual(result.files, [ - `node-v${mockverson}-linux-armv6l.tar.gz`, - `node-v${mockverson}-linux-armv6l.tar.xz`, - ]); - assert.deepEqual(result.id, id); - assert.deepEqual(result.type, 'linux'); + it('should return the correct armv6l version string', () => { + const id = 'linux-armv6l'; + const result = parseFiles(id, mockverson); + assert.deepEqual(result.architecture, 'armv6l'); + assert.deepEqual(result.files, [ + `node-v${mockverson}-linux-armv6l.tar.gz`, + `node-v${mockverson}-linux-armv6l.tar.xz`, + ]); + assert.deepEqual(result.id, id); + assert.deepEqual(result.type, 'linux'); + }); }); diff --git a/static/test/test.build.js b/static/test/test.build.js new file mode 100644 index 0000000..aad25ab --- /dev/null +++ b/static/test/test.build.js @@ -0,0 +1,112 @@ +const assert = require('node:assert'); +const { execFile } = require('node:child_process'); +const { mkdir, mkdtemp, readFile } = require('node:fs/promises'); +const { tmpdir } = require('node:os'); +const { dirname, join } = require('node:path'); +const { describe, it } = require('node:test'); +const { promisify } = require('node:util'); +const nodevu = require('@nodevu/core'); +const fixtures = require('../../core/util/dev/fixtures'); +const now = require('../../core/test/data/static/now.json'); + +// a full diff of every release takes node minutes to build, so failures say +// this instead +const mismatch = 'the build output should match what @nodevu/core returns'; + +describe('build:default', async () => { + it('should save everything @nodevu/core returns', async () => { + // Arrange + const expected = await coreData(); + + // Act + const written = await runBuild('default'); + + // Assert + assert.deepStrictEqual(written, toJSON(expected), mismatch); + }); +}); + +describe('build:releases', async () => { + it("should save each release line's releases", async () => { + // Arrange + const expected = pick(await coreData(), 'releases'); + + // Act + const written = await runBuild('releases'); + + // Assert + assert.deepStrictEqual(written, toJSON(expected), mismatch); + }); +}); + +describe('build:security', async () => { + it("should save each release line's security data", async () => { + // Arrange + const expected = pick(await coreData(), 'security'); + + // Act + const written = await runBuild('security'); + + // Assert + assert.deepStrictEqual(written, toJSON(expected), mismatch); + }); +}); + +describe('build:support', async () => { + it("should save each release line's support data", async () => { + // Arrange + const expected = pick(await coreData(), 'support'); + + // Act + const written = await runBuild('support'); + + // Assert + assert.deepStrictEqual(written, toJSON(expected), mismatch); + }); +}); + +// Helpers + +// what @nodevu/core returns for its snapshot data at the time it was taken +function coreData() { + return nodevu(fixtures.at(now)); +} + +function pick(data, key) { + return Object.fromEntries( + Object.entries(data).map(([line, value]) => [line, value[key]]), + ); +} + +// the build output is JSON, which drops undefined values +function toJSON(data) { + return JSON.parse(JSON.stringify(data)); +} + +// runs a build script the way `npm run build -w static` does, from a copy of +// static/, with core's snapshot data served instead of nodejs.org and the +// clock stopped at the time the snapshot was taken. returns what it wrote. +async function runBuild(name) { + const cwd = join(await mkdtemp(join(tmpdir(), 'nodevu-')), 'static'); + await mkdir(join(cwd, 'data'), { recursive: true }); + + const luxon = require.resolve('luxon', { + paths: [dirname(require.resolve('@nodevu/core'))], + }); + + const setup = [ + `require(${JSON.stringify(luxon)}).Settings.now = () => ${Date.parse(now)};`, + `globalThis.fetch = require(${JSON.stringify(require.resolve('../../core/util/dev/fixtures'))}).at(${JSON.stringify(now)}).fetch;`, + ].join(' '); + + await promisify(execFile)( + process.execPath, + [ + '-e', + `${setup} require(${JSON.stringify(require.resolve(`../build/${name}`))});`, + ], + { cwd }, + ); + + return JSON.parse(await readFile(join(cwd, 'data', `${name}.json`), 'utf8')); +} diff --git a/static/test/test.write.js b/static/test/test.write.js new file mode 100644 index 0000000..df8ad05 --- /dev/null +++ b/static/test/test.write.js @@ -0,0 +1,39 @@ +const assert = require('node:assert'); +const { mkdir, mkdtemp, readFile } = require('node:fs/promises'); +const { tmpdir } = require('node:os'); +const { join } = require('node:path'); +const { describe, it } = require('node:test'); +const write = require('../util/dev/write'); + +describe('write', async () => { + it('should save data as JSON indented with two spaces', async () => { + // Arrange + const path = join(await mkdtemp(join(tmpdir(), 'nodevu-')), 'data.json'); + + // Act + await write(path, { v22: { codename: 'Jod' } }); + + // Assert + assert.strictEqual( + await readFile(path, 'utf8'), + '{\n "v22": {\n "codename": "Jod"\n }\n}', + ); + }); + + // the build scripts pass paths like ./static/data, so running them from + // inside static/ would otherwise point at static/static/data + it('should collapse a doubled static/static/ in the path', async () => { + // Arrange + const root = await mkdtemp(join(tmpdir(), 'nodevu-')); + await mkdir(join(root, 'static')); + + // Act + await write(join(root, 'static/static/data.json'), []); + + // Assert + assert.strictEqual( + await readFile(join(root, 'static/data.json'), 'utf8'), + '[]', + ); + }); +}); diff --git a/translate/test/examples.test.js b/translate/test/examples.test.js new file mode 100644 index 0000000..0938e27 --- /dev/null +++ b/translate/test/examples.test.js @@ -0,0 +1,25 @@ +const assert = require('node:assert'); +const { execFile } = require('node:child_process'); +const { describe, it } = require('node:test'); +const { promisify } = require('node:util'); + +describe('examples/default.js', async () => { + it('should print the ranges for each Package Support term', async () => { + // Arrange + // serve core's snapshot data instead of fetching from nodejs.org + const setup = `globalThis.fetch = require(${JSON.stringify(require.resolve('../../ranges/test/fixtures'))}).at().fetch;`; + const example = JSON.stringify(require.resolve('../examples/default')); + + // Act + const { stdout } = await promisify(execFile)(process.execPath, [ + '-e', + `${setup} require(${example});`, + ]); + + // Assert + assert.match( + stdout, + /current: \{[\s\S]*lts_latest: \{[\s\S]*lts: \{[\s\S]*supported: \{[\s\S]*all: \{/, + ); + }); +});