Skip to content

Commit

Permalink
chore: node:test and node:assert
Browse files Browse the repository at this point in the history
This is a proof of concept intended to explore what it would look like
to use `node:test` and `node:assert` for testing.  For the most part the
assertions themselves translated nicely.

As part of the refactor, large fixtures were moved out of some test
files and into `./test/fixtures`.

The parts that are probably not ready are:

 - Code coverage.  This flag is still experimental in Node.js.  There is
   also no way to set a coverage level requirement, nor is there
   coverage mapping.  This is the only repo left that the npm cli team
   manages which still uses coverage mapping.  Whether or not we want to
   keep coverage mapping is a discussion that could take place.
 - Timeout.  There is a test that assumes the timeout is not Infinity.
   This is Node.js's default, and can only apparently be changed from
   tests that are invoked from `run()`.
 - Test runner.  This still uses `tap` for running the tests.  Running
   the tests purely from node would require having a centralized runner,
   or refactoring all the tests to invoke `run()` and set a timeout. It
   would also require solving the coverage problem.
 - Snapshots.  There is no built-in snapshot functionality in the
   Node.js test suite. There was only one test that really could
   arguably benefit from it: the help output.  The rest really were
   better suited for explicit checks.

Conclusion: `node:test` is not feature complete, and we can not cut over
to it fully.  This PR may be ok to land as-is, since `node:test` returns
tap-compatible results.  Whether or not we want to land it like this is
a discussion to be had.  At the very least this demonstrates the amount
of work needed to translate tests from `tap` to `node:test`
  • Loading branch information
wraithgar committed Jan 5, 2024
1 parent 14d263f commit ea562e1
Show file tree
Hide file tree
Showing 67 changed files with 1,665 additions and 2,058 deletions.
477 changes: 0 additions & 477 deletions tap-snapshots/test/bin/semver.js.test.cjs

This file was deleted.

240 changes: 180 additions & 60 deletions test/bin/semver.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
'use strict'
const t = require('tap')
const t = require('node:test')
const a = require('node:assert')

const thisVersion = require('../../package.json').version
t.cleanSnapshot = str => str.split(thisVersion).join('@@VERSION@@')

const spawn = require('child_process').spawn
const bin = require.resolve('../../bin/semver')
Expand All @@ -12,9 +11,9 @@ const run = args => new Promise((resolve, reject) => {
const out = []
const err = []
c.stdout.setEncoding('utf-8')
c.stdout.on('data', chunk => out.push(chunk))
c.stdout.on('data', chunk => out.push(chunk.replace('\r\n', '\n')))
c.stderr.setEncoding('utf-8')
c.stderr.on('data', chunk => err.push(chunk))
c.stderr.on('data', chunk => err.push(chunk.replace('\r\n', '\n')))
c.on('close', (code, signal) => {
resolve({
out: out.join(''),
Expand All @@ -25,58 +24,179 @@ const run = args => new Promise((resolve, reject) => {
})
})

t.test('inc tests', t => Promise.all([
['-i', 'major', '1.0.0'],
['-i', 'major', '1.0.0', '1.0.1'],
['-i', 'premajor', '1.0.0', '--preid=beta'],
['-i', 'premajor', '1.0.0', '--preid=beta', '-n', '1'],
['-i', 'premajor', '1.0.0', '--preid=beta', '-n', 'false'],
['-i', '1.2.3'],
].map(args => t.resolveMatchSnapshot(run(args), args.join(' ')))))

t.test('help output', t => Promise.all([
['-h'],
['-?'],
['--help'],
[],
].map(h => t.resolveMatchSnapshot(run(h), h[0] || '(no args)'))))

t.test('sorting and filtering', t => Promise.all([
['1.2.3', '3.2.1', '2.3.4'],
['1.2.3', '3.2.1', '2.3.4', '2.3.4-beta'],
['1.2.3', '-v', '3.2.1', '--version', '2.3.4'],
['1.2.3', '-v', '3.2.1', '--version', '2.3.4', '-rv'],
['1.2.3foo', '1.2.3-bar'],
['1.2.3foo', '1.2.3-bar', '-l'],
['1.2.3', '3.2.1', '-r', '2.x', '2.3.4'],
['1.2.3', '3.2.1', '2.3.4', '2.3.4-beta', '2.0.0asdf', '-r', '2.x'],
['1.2.3', '3.2.1', '2.3.4', '2.3.4-beta', '2.0.0asdf', '-r', '2.x', '-p'],
['3.2.1', '2.3.4', '2.3.4-beta', '2.0.0asdf', '-r', '2.x', '-p', '-l'],
['1.2.3', '3.2.1', '-r', '2.x'],
].map(args => t.resolveMatchSnapshot(run(args), args.join(' ')))))

t.test('coercing', t => Promise.all([
['1.2.3.4.5.6', '-c'],
['1.2.3.4.5.6', '-c', '--rtl'],
['1.2.3.4.5.6', '-c', '--rtl', '--ltr'],
['not a version', '1.2.3', '-c'],
['not a version', '-c'],
].map(args => t.resolveMatchSnapshot(run(args), args.join(' ')))))

t.test('args with equals', t => Promise.all([
[['--version', '1.2.3'], '1.2.3'],
[['--range', '1'], ['1.2.3'], ['2.3.4'], '1.2.3'],
[['--increment', 'major'], ['1.0.0'], '2.0.0'],
[['--increment', 'premajor'], ['--preid', 'beta'], ['1.0.0'], '2.0.0-beta.0'],
].map(async (args) => {
const expected = args.pop()
const equals = args.map((a) => a.join('='))
const spaces = args.reduce((acc, a) => acc.concat(a), [])
const res1 = await run(equals)
const res2 = await run(spaces)
t.equal(res1.signal, null)
t.equal(res1.code, 0)
t.equal(res1.err, '')
t.equal(res1.out.trim(), expected)
t.strictSame(res1, res2, args.join(' '))
})))
t.test('inc tests', async t => {
const cmds = [
[['-i', 'major', '1.0.0'], '2.0.0\n'],
[['-i', 'premajor', '1.0.0', '--preid=beta'], '2.0.0-beta.0\n'],
[['-i', 'premajor', '1.0.0', '--preid=beta', '-n', '1'], '2.0.0-beta.1\n'],
[['-i', 'premajor', '1.0.0', '--preid=beta', '-n', 'false'], '2.0.0-beta\n'],
[['-i', '1.2.3'], '1.2.4\n'],
]
for (const [args, result] of cmds) {
await t.test(args.join(' '), async t => {
const { code, err, signal, out } = await run(args)
a.equal(code, 0)
a.equal(err, '')
a.equal(signal, null)
a.equal(out, result)
})
}
await t.test('too many params', async t => {
const { code, err, signal, out } = await run(['-i', 'major', '1.0.0', '1.0.1'])
a.equal(code, 1)
a.equal(err, '--inc can only be used on a single version with no range\n')
a.equal(signal, null)
a.equal(out, '')
})
})

t.test('help output', async t => {
const h = await run(['-h'])
const help = await run(['--help'])
const no = await run([])
const q = await run(['-?'])
a.equal(h.code, 0)
a.equal(h.err, '')
a.equal(h.signal, null)
a.equal(help.code, 0)
a.equal(help.err, '')
a.equal(help.signal, null)
a.equal(no.code, 0)
a.equal(no.err, '')
a.equal(no.signal, null)
a.equal(q.code, 0)
a.equal(q.err, '')
a.equal(q.signal, null)
a.equal(h.out, help.out)
a.equal(h.out, q.out)
a.equal(h.out, no.out)
a.equal(h.out.replace(thisVersion, '@@VERSION@@'), `SemVer @@VERSION@@
A JavaScript implementation of the https://semver.org/ specification
Copyright Isaac Z. Schlueter
Usage: semver [options] <version> [<version> [...]]
Prints valid versions sorted by SemVer precedence
Options:
-r --range <range>
Print versions that match the specified range.
-i --increment [<level>]
Increment a version by the specified level. Level can
be one of: major, minor, patch, premajor, preminor,
prepatch, or prerelease. Default level is 'patch'.
Only one version may be specified.
--preid <identifier>
Identifier to be used to prefix premajor, preminor,
prepatch or prerelease version increments.
-l --loose
Interpret versions and ranges loosely
-p --include-prerelease
Always include prerelease versions in range matching
-c --coerce
Coerce a string into SemVer if possible
(does not imply --loose)
--rtl
Coerce version strings right to left
--ltr
Coerce version strings left to right (default)
-n <base>
Base number to be used for the prerelease identifier.
Can be either 0 or 1, or false to omit the number altogether.
Defaults to 0.
Program exits successfully if any valid version satisfies
all supplied ranges, and prints all satisfying versions.
If no satisfying versions are found, then exits failure.
Versions are printed in ascending order, so supplying
multiple versions to the utility will just sort them.
`)
})

t.test('sorting and filtering', async t => {
const cmds = [
[['1.2.3', '3.2.1', '2.3.4'], '1.2.3\n2.3.4\n3.2.1\n'],
[['1.2.3', '3.2.1', '2.3.4', '2.3.4-beta'], '1.2.3\n2.3.4-beta\n2.3.4\n3.2.1\n'],
[['1.2.3', '-v', '3.2.1', '--version', '2.3.4'], '1.2.3\n2.3.4\n3.2.1\n'],
[['1.2.3', '-v', '3.2.1', '--version', '2.3.4', '-rv'], '3.2.1\n2.3.4\n1.2.3\n'],
[['1.2.3foo', '1.2.3-bar'], '1.2.3-bar\n'],
[['1.2.3foo', '1.2.3-bar', '-l'], '1.2.3-bar\n'],
[['1.2.3', '3.2.1', '-r', '2.x', '2.3.4'], '2.3.4\n'],
[['1.2.3', '3.2.1', '2.3.4', '2.3.4-beta', '2.0.0asdf', '-r', '2.x'], '2.3.4\n'],
[['1.2.3', '3.2.1', '2.3.4', '2.3.4-beta', '2.0.0asdf', '-r', '2.x', '-p'],
'2.3.4-beta\n2.3.4\n'],
[['3.2.1', '2.3.4', '2.3.4-beta', '2.0.0asdf', '-r', '2.x', '-p', '-l'],
'2.3.4-beta\n2.3.4\n'],
]
for (const [args, result] of cmds) {
await t.test(args.join(' '), async t => {
const { code, err, signal, out } = await run(args)
a.equal(code, 0)
a.equal(err, '')
a.equal(signal, null)
a.equal(out, result)
})
}
await t.test('nothing in range', async t => {
const { code, err, signal, out } = await run(['1.2.3', '3.2.1', '-r', '2.x'])
a.equal(code, 1)
a.equal(err, '')
a.equal(signal, null)
a.equal(out, '')
})
})

t.test('coercing', async t => {
const cmds = [
[['1.2.3.4.5.6', '-c'], '1.2.3\n'],
[['1.2.3.4.5.6', '-c', '--rtl'], '4.5.6\n'],
[['1.2.3.4.5.6', '-c', '--rtl', '--ltr'], '1.2.3\n'],
[['not a version', '1.2.3', '-c'], '1.2.3\n'],
]
for (const [args, result] of cmds) {
await t.test(args.join(' '), async t => {
const { code, err, signal, out } = await run(args)
a.equal(code, 0)
a.equal(err, '')
a.equal(signal, null)
a.equal(out, result)
})
}
await t.test('not a version', async t => {
const { code, err, signal, out } = await run(['not a version', '-c'])
a.equal(code, 1)
a.equal(err, '')
a.equal(signal, null)
a.equal(out, '')
})
})

t.test('args with equals', async t => {
const cmds = [
[[['--version', '1.2.3']], '1.2.3'],
[[['--range', '1'], ['1.2.3'], ['2.3.4']], '1.2.3'],
[[['--increment', 'major'], ['1.0.0']], '2.0.0'],
[[['--increment', 'premajor'], ['--preid', 'beta'], ['1.0.0']], '2.0.0-beta.0'],
]
for (const [args, result] of cmds) {
const equals = args.map(arg => arg.join('='))
const spaces = args.reduce((acc, arg) => acc.concat(arg), [])
const res1 = await run(equals)
const res2 = await run(spaces)
a.equal(res1.signal, null)
a.equal(res1.code, 0)
a.equal(res1.err, '')
a.equal(res1.out.trim(), result)
a.deepEqual(res1, res2, args.join(' '))
}
})
56 changes: 25 additions & 31 deletions test/classes/comparator.js
Original file line number Diff line number Diff line change
@@ -1,63 +1,57 @@
const { test } = require('tap')
const t = require('node:test')
const a = require('node:assert')
const Comparator = require('../../classes/comparator')
const comparatorIntersection = require('../fixtures/comparator-intersection.js')

test('comparator testing', t => {
t.test('comparator testing', t => {
const c = new Comparator('>=1.2.3')
t.ok(c.test('1.2.4'))
a.ok(c.test('1.2.4'))
const c2 = new Comparator(c)
t.ok(c2.test('1.2.4'))
a.ok(c2.test('1.2.4'))
const c3 = new Comparator(c, true)
t.ok(c3.test('1.2.4'))
a.ok(c3.test('1.2.4'))
// test an invalid version, should not throw
const c4 = new Comparator(c)
t.notOk(c4.test('not a version string'))
t.end()
a.ok(!c4.test('not a version string'))
})

test('tostrings', (t) => {
t.equal(new Comparator('>= v1.2.3').toString(), '>=1.2.3')
t.end()
t.test('tostrings', (t) => {
a.equal(new Comparator('>= v1.2.3').toString(), '>=1.2.3')
})

test('intersect comparators', (t) => {
t.plan(comparatorIntersection.length)
comparatorIntersection.forEach(([c0, c1, expect, includePrerelease]) =>
t.test(`${c0} ${c1} ${expect}`, t => {
t.test('intersect comparators', async (t) => {
for (const [c0, c1, expect, includePrerelease] of comparatorIntersection) {
await t.test(`${c0} ${c1} ${expect}`, t => {
const comp0 = new Comparator(c0)
const comp1 = new Comparator(c1)

t.equal(comp0.intersects(comp1, { includePrerelease }), expect,
a.equal(comp0.intersects(comp1, { includePrerelease }), expect,
`${c0} intersects ${c1}`)

t.equal(comp1.intersects(comp0, { includePrerelease }), expect,
a.equal(comp1.intersects(comp0, { includePrerelease }), expect,
`${c1} intersects ${c0}`)
t.end()
}))
})
}
})

test('intersect demands another comparator', t => {
t.test('intersect demands another comparator', t => {
const c = new Comparator('>=1.2.3')
t.throws(() => c.intersects(), new TypeError('a Comparator is required'))
t.end()
a.throws(() => c.intersects(), new TypeError('a Comparator is required'))
})

test('ANY matches anything', t => {
t.test('ANY matches anything', t => {
const c = new Comparator('')
t.ok(c.test('1.2.3'), 'ANY matches anything')
a.ok(c.test('1.2.3'), 'ANY matches anything')
const c1 = new Comparator('>=1.2.3')
const ANY = Comparator.ANY
t.ok(c1.test(ANY), 'anything matches ANY')
t.end()
a.ok(c1.test(ANY), 'anything matches ANY')
})

test('invalid comparator parse throws', t => {
t.throws(() => new Comparator('foo bar baz'),
t.test('invalid comparator parse throws', t => {
a.throws(() => new Comparator('foo bar baz'),
new TypeError('Invalid comparator: foo bar baz'))
t.end()
})

test('= is ignored', t => {
t.match(new Comparator('=1.2.3'), new Comparator('1.2.3'))
t.end()
t.test('= is ignored', t => {
a.deepEqual(new Comparator('=1.2.3'), new Comparator('1.2.3'))
})
15 changes: 9 additions & 6 deletions test/classes/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
const t = require('tap')
t.same(require('../../classes'), {
SemVer: require('../../classes/semver'),
Range: require('../../classes/range'),
Comparator: require('../../classes/comparator'),
}, 'export all classes at semver/classes')
const t = require('node:test')
const a = require('node:assert')
t.test('classes', (t) => {
a.deepEqual(require('../../classes'), {
SemVer: require('../../classes/semver'),
Range: require('../../classes/range'),
Comparator: require('../../classes/comparator'),
}, 'export all classes at semver/classes')
})
Loading

0 comments on commit ea562e1

Please sign in to comment.