diff --git a/.c8rc.json b/.c8rc.json new file mode 100644 index 00000000..07683076 --- /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 f07d61cf..28537b06 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 271a2ed9..3fae39ae 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 5614d3a9..36f02573 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 c1bffa26..73958770 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 a8d69d08..3d8949f7 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 83e842c5..b668c624 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 7c3396eb..bb43f5da 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 b3162fce..33c9fc04 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 d8470d76..1522acd6 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 34e08813..298a3785 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 @@ -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 89e63894..8c1d7022 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/combos.js b/core/test/combos.js index b51653c4..d3aa96ac 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 a9247f9e..5ea1020c 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/dev-fixtures.js b/core/test/dev-fixtures.js new file mode 100644 index 00000000..ca9020f9 --- /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 00000000..5c0147f4 --- /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 00000000..7d393296 --- /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/dynamic-values.js b/core/test/dynamic-values.js index f4875c96..aba36398 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/test/files.js b/core/test/files.js new file mode 100644 index 00000000..69d98949 --- /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: {}, + }); + }); +}); diff --git a/core/test/null-values.js b/core/test/null-values.js new file mode 100644 index 00000000..1f138fb9 --- /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 00000000..bd8a7391 --- /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/core/util/dev/updateStaticNow.js b/core/util/dev/updateStaticNow.js index 38513110..c8cb6b9b 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/package.json b/package.json index c4c9e4a4..c963d51e 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/index.js b/parsefiles/index.js index fccffb5d..a95ad922 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 b4766dde..28b9358d 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" }, { diff --git a/parsefiles/test/test.js b/parsefiles/test/test.js index 1b839ad0..f96fb67b 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/ranges/test/emptyFields.test.js b/ranges/test/emptyFields.test.js index 285079dd..6c19dc57 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 () => { diff --git a/static/package.json b/static/package.json index ac0860d0..1e53adcb 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" } } diff --git a/static/test/test.build.js b/static/test/test.build.js new file mode 100644 index 00000000..aad25abf --- /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 00000000..df8ad05c --- /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 00000000..0938e273 --- /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: \{/, + ); + }); +});