test
Version:
Node.js 18's node:test, as an npm package
142 lines (124 loc) • 5.46 kB
JavaScript
const { createReadStream, promises: fs } = require('node:fs')
const { extname, join, resolve } = require('node:path')
const { promisify } = require('node:util')
const { exec } = require('node:child_process')
const { createInterface } = require('node:readline')
const { bin } = require('../package.json')
const binPath = resolve(__dirname, '..', bin.test)
const MESSAGE_FOLDER = join(__dirname, './message/')
const WAIT_FOR_ELLIPSIS = Symbol('wait for ellispis')
const TEST_RUNNER_FLAGS = ['--test', '--test-only', '--test-name-pattern', '--test-reporter', '--test-reporter-destination']
function readLines (file) {
return createInterface({
input: createReadStream(file),
crlfDelay: Infinity
})
}
const stackTraceStartLine = /^\s+stack: \|-$/
const errorStartLine = /^\s+Error: /
const stackTraceLine = /^\s+\*$/
const stackTraceEndLine = /^\s+\.\.\.$/
const nodejs14NotEmittedWarn = /^# Warning:.*\breject/
const nodejs14NotEmittedUnhandledRejection = /unhandledRejection/
// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/test/message/testcfg.py#L53
async function IsFailureOutput (self, output, filename) {
// Convert output lines to regexps that we can match
const patterns = []
for await (const line of readLines(self.expected)) {
// Our implementation outputs different stack traces than the Node.js implementation.
if (stackTraceLine.test(line) && patterns[patterns.length - 1] === WAIT_FOR_ELLIPSIS) continue
// Node.js 14 doesn't emit some warnings
if (process.version.startsWith('v14.') && nodejs14NotEmittedWarn.test(line)) continue
if (process.version.startsWith('v14.') && nodejs14NotEmittedUnhandledRejection.test(line)) {
patterns.push(WAIT_FOR_ELLIPSIS)
continue
}
// Sometimes Node.js won't have any stack trace, but we would
if (stackTraceEndLine.test(line) && patterns[patterns.length - 1].toString().endsWith("code: 'ERR_TEST_FAILURE'$")) {
patterns.push(stackTraceStartLine, WAIT_FOR_ELLIPSIS)
}
const pattern = line
.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
.replace(/\\\*/g, '.*')
patterns.push(`^${pattern}$`)
if (stackTraceStartLine.test(line) || (filename === 'test_runner_output_spec_reporter.js' && errorStartLine.test(line))) {
// Our implementation outputs different stack traces than the Node.js implementation.
patterns.push(WAIT_FOR_ELLIPSIS)
}
}
// Compare actual output with the expected
const outlines = (output.stdout + output.stderr).split('\n').filter(
(line) => line && !line.startsWith('==') && !line.startsWith('**')
)
let waitingForEllipsis = false
for (let i = 0; i < outlines.length; i++) {
let regex
if (patterns[i] === WAIT_FOR_ELLIPSIS) {
waitingForEllipsis = true
} else if (!(regex = new RegExp(patterns[i])).test(outlines[i].replace(/\r/, '')) && !regex.test(outlines[i].replace(/\r/, '').trimEnd())) {
if (waitingForEllipsis) {
patterns.splice(i, 0, WAIT_FOR_ELLIPSIS)
continue
}
console.log('match failed', { line: i + 1, expected: patterns[i], actual: outlines[i] })
console.log(Array.from({ length: Math.min(patterns.length, outlines.length) }, (_, i) => ({ line: i + 1, expected: patterns[i], actual: outlines[i] })).slice(Math.max(0, i - 5), i + 5))
return true
} else if (waitingForEllipsis && stackTraceEndLine.test(outlines[i])) {
waitingForEllipsis = false
}
}
return false
}
const main = async () => {
const dir = await fs.opendir(MESSAGE_FOLDER)
for await (const dirent of dir) {
const ext = extname(dirent.name)
if (ext === '.js' || ext === '.mjs') {
if (typeof AbortSignal === 'undefined' && dirent.name.startsWith('test_runner_abort')) {
console.log('no AbortSignal support, skipping', dirent.name)
continue
}
const filePath = join(MESSAGE_FOLDER, dirent.name)
const expected = filePath.replace(/\.m?js$/, '.out')
const testFile = await fs.open(filePath)
const fileContent = await testFile.read({ length: 512 })
await testFile.close()
const flagIndex = fileContent.buffer.indexOf('// Flags: ')
const flags =
flagIndex === -1
? []
: fileContent.buffer
.subarray(
flagIndex + 10,
fileContent.buffer.indexOf(10, flagIndex)
)
.toString().split(' ')
const nodeFlags = flags.filter(flag => !TEST_RUNNER_FLAGS.find(f => flag.startsWith(f))).join(' ')
const testRunnerFlags = flags.filter(flag => TEST_RUNNER_FLAGS.find(f => flag.startsWith(f))).join(' ')
const command = testRunnerFlags.length
? `${process.execPath} ${nodeFlags} ${binPath} ${testRunnerFlags} ${filePath}`
: `${process.execPath} ${nodeFlags} ${filePath}`
console.log(`Running ${command}`)
let stdout, stderr
try {
const res = await promisify(exec)(command)
stdout = res.stdout.trim()
stderr = res.stderr.trim()
} catch (err) {
if (err?.stdout == null || err.stderr == null) throw err
stdout = err.stdout.trim()
stderr = err.stderr.trim()
}
if (await IsFailureOutput({ expected }, { stdout, stderr }, dirent.name)) {
throw new Error()
}
console.log('pass')
}
}
}
main().catch(err => {
console.error(err)
process.exit(1)
})