Skip to content

Commit

Permalink
feat: add initial TAP parser
Browse files Browse the repository at this point in the history
Work in progress

PR-URL: nodejs/node#43525
Refs: nodejs/node#43344
Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
(cherry picked from commit f8ce9117b19702487eb600493d941f7876e00e01)
  • Loading branch information
manekinekko authored and MoLow committed Feb 2, 2023
1 parent 2e499ee commit 6cd562d
Show file tree
Hide file tree
Showing 17 changed files with 4,430 additions and 38 deletions.
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ expected.

```mjs
import assert from 'node:assert';
import { mock, test } from 'node:test';
import { mock, test } from 'test';
test('spies on a function', () => {
const sum = mock.fn((a, b) => {
return a + b;
Expand All @@ -388,7 +388,7 @@ test('spies on a function', () => {
```cjs
'use strict';
const assert = require('node:assert');
const { mock, test } = require('node:test');
const { mock, test } = require('test');
test('spies on a function', () => {
const sum = mock.fn((a, b) => {
return a + b;
Expand Down Expand Up @@ -964,8 +964,7 @@ Emitted when [`context.diagnostic`][] is called.
### Event: `'test:fail'`

* `data` {Object}
* `duration` {number} The test duration.
* `error` {Error} The failure casing test to fail.
* `details` {Object} Additional execution metadata.
* `name` {string} The test name.
* `testNumber` {number} The ordinal number of the test.
* `todo` {string|undefined} Present if [`context.todo`][] is called
Expand All @@ -976,7 +975,7 @@ Emitted when a test fails.
### Event: `'test:pass'`

* `data` {Object}
* `duration` {number} The test duration.
* `details` {Object} Additional execution metadata.
* `name` {string} The test name.
* `testNumber` {number} The ordinal number of the test.
* `todo` {string|undefined} Present if [`context.todo`][] is called
Expand Down
17 changes: 16 additions & 1 deletion lib/internal/errors.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/lib/internal/errors.js
// https://github.com/nodejs/node/blob/f8ce9117b19702487eb600493d941f7876e00e01/lib/internal/errors.js

'use strict'

Expand Down Expand Up @@ -346,6 +346,21 @@ module.exports = {
kIsNodeError
}

E('ERR_TAP_LEXER_ERROR', function (errorMsg) {
hideInternalStackFrames(this)
return errorMsg
}, Error)
E('ERR_TAP_PARSER_ERROR', function (errorMsg, details, tokenCausedError, source) {
hideInternalStackFrames(this)
this.cause = tokenCausedError
const { column, line, start, end } = tokenCausedError.location
const errorDetails = `${details} at line ${line}, column ${column} (start ${start}, end ${end})`
return errorMsg + errorDetails
}, SyntaxError)
E('ERR_TAP_VALIDATION_ERROR', function (errorMsg) {
hideInternalStackFrames(this)
return errorMsg
}, Error)
E('ERR_TEST_FAILURE', function (error, failureType) {
hideInternalStackFrames(this)
assert(typeof failureType === 'string',
Expand Down
6 changes: 6 additions & 0 deletions lib/internal/per_context/primordials.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ exports.ArrayFrom = (it, mapFn) => Array.from(it, mapFn)
exports.ArrayIsArray = Array.isArray
exports.ArrayPrototypeConcat = (arr, ...el) => arr.concat(...el)
exports.ArrayPrototypeFilter = (arr, fn) => arr.filter(fn)
exports.ArrayPrototypeFind = (arr, fn) => arr.find(fn)
exports.ArrayPrototypeForEach = (arr, fn, thisArg) => arr.forEach(fn, thisArg)
exports.ArrayPrototypeIncludes = (arr, el, fromIndex) => arr.includes(el, fromIndex)
exports.ArrayPrototypeJoin = (arr, str) => arr.join(str)
Expand All @@ -17,6 +18,7 @@ exports.ArrayPrototypeShift = arr => arr.shift()
exports.ArrayPrototypeSlice = (arr, offset) => arr.slice(offset)
exports.ArrayPrototypeSome = (arr, fn) => arr.some(fn)
exports.ArrayPrototypeSort = (arr, fn) => arr.sort(fn)
exports.ArrayPrototypeSplice = (arr, offset, len, ...el) => arr.splice(offset, len, ...el)
exports.ArrayPrototypeUnshift = (arr, ...el) => arr.unshift(...el)
exports.Error = Error
exports.ErrorCaptureStackTrace = (...args) => Error.captureStackTrace(...args)
Expand All @@ -26,6 +28,7 @@ exports.FunctionPrototypeCall = (fn, obj, ...args) => fn.call(obj, ...args)
exports.MathMax = (...args) => Math.max(...args)
exports.Number = Number
exports.NumberIsInteger = Number.isInteger
exports.NumberParseInt = (str, radix) => Number.parseInt(str, radix)
exports.NumberMIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER
exports.NumberMAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER
exports.ObjectAssign = (target, ...sources) => Object.assign(target, ...sources)
Expand Down Expand Up @@ -56,12 +59,15 @@ exports.SafeWeakSet = WeakSet
exports.StringPrototypeEndsWith = (haystack, needle, index) => haystack.endsWith(needle, index)
exports.StringPrototypeIncludes = (str, needle) => str.includes(needle)
exports.StringPrototypeMatch = (str, reg) => str.match(reg)
exports.StringPrototypeRepeat = (str, times) => str.repeat(times)
exports.StringPrototypeReplace = (str, search, replacement) =>
str.replace(search, replacement)
exports.StringPrototypeReplaceAll = replaceAll
exports.StringPrototypeStartsWith = (haystack, needle, index) => haystack.startsWith(needle, index)
exports.StringPrototypeSlice = (str, ...args) => str.slice(...args)
exports.StringPrototypeSplit = (str, search, limit) => str.split(search, limit)
exports.StringPrototypeToUpperCase = str => str.toUpperCase()
exports.StringPrototypeTrim = str => str.trim()
exports.Symbol = Symbol
exports.SymbolFor = repr => Symbol.for(repr)
exports.ReflectApply = (target, self, args) => Reflect.apply(target, self, args)
Expand Down
132 changes: 124 additions & 8 deletions lib/internal/test_runner/runner.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// https://github.com/nodejs/node/blob/9825a7e01d35b9d49ebb58efed2c316012c19db6/lib/internal/test_runner/runner.js
// https://github.com/nodejs/node/blob/f8ce9117b19702487eb600493d941f7876e00e01/lib/internal/test_runner/runner.js
'use strict'
const {
ArrayFrom,
ArrayPrototypeFilter,
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
ArrayPrototypeJoin,
ArrayPrototypePush,
Expand All @@ -11,7 +12,8 @@ const {
ObjectAssign,
PromisePrototypeThen,
SafePromiseAll,
SafeSet
SafeSet,
StringPrototypeRepeat
} = require('#internal/per_context/primordials')

const { spawn } = require('child_process')
Expand All @@ -28,7 +30,9 @@ const { validateArray } = require('#internal/validators')
const { getInspectPort, isUsingInspector, isInspectorMessage } = require('#internal/util/inspector')
const { kEmptyObject } = require('#internal/util')
const { createTestTree } = require('#internal/test_runner/harness')
const { kSubtestsFailed, Test } = require('#internal/test_runner/test')
const { kDefaultIndent, kSubtestsFailed, Test } = require('#internal/test_runner/test')
const { TapParser } = require('#internal/test_runner/tap_parser')
const { TokenKind } = require('#internal/test_runner/tap_lexer')
const {
isSupportedFileType,
doesPathMatchFilter
Expand Down Expand Up @@ -114,16 +118,117 @@ function getRunArgs ({ path, inspectPort }) {
return argv
}

function runTestFile (path, root, inspectPort) {
const subtest = root.createSubtest(Test, path, async (t) => {
class FileTest extends Test {
#buffer = []
#handleReportItem ({ kind, node, nesting = 0 }) {
const indent = StringPrototypeRepeat(kDefaultIndent, nesting + 1)

const details = (diagnostic) => {
return (
diagnostic && {
__proto__: null,
yaml:
`${indent} ` +
ArrayPrototypeJoin(diagnostic, `\n${indent} `) +
'\n'
}
)
}

switch (kind) {
case TokenKind.TAP_VERSION:
// TODO(manekinekko): handle TAP version coming from the parser.
// this.reporter.version(node.version);
break

case TokenKind.TAP_PLAN:
this.reporter.plan(indent, node.end - node.start + 1)
break

case TokenKind.TAP_SUBTEST_POINT:
this.reporter.subtest(indent, node.name)
break

case TokenKind.TAP_TEST_POINT:
// eslint-disable-next-line no-case-declarations
const { todo, skip, pass } = node.status
// eslint-disable-next-line no-case-declarations
let directive

if (skip) {
directive = this.reporter.getSkip(node.reason)
} else if (todo) {
directive = this.reporter.getTodo(node.reason)
} else {
directive = kEmptyObject
}

if (pass) {
this.reporter.ok(
indent,
node.id,
node.description,
details(node.diagnostics),
directive
)
} else {
this.reporter.fail(
indent,
node.id,
node.description,
details(node.diagnostics),
directive
)
}
break

case TokenKind.COMMENT:
if (indent === kDefaultIndent) {
// Ignore file top level diagnostics
break
}
this.reporter.diagnostic(indent, node.comment)
break

case TokenKind.UNKNOWN:
this.reporter.diagnostic(indent, node.value)
break
}
}

addToReport (ast) {
if (!this.isClearToSend()) {
ArrayPrototypePush(this.#buffer, ast)
return
}
this.reportSubtest()
this.#handleReportItem(ast)
}

report () {
this.reportSubtest()
ArrayPrototypeForEach(this.#buffer, (ast) => this.#handleReportItem(ast))
super.report()
}
}

function runTestFile (path, root, inspectPort, filesWatcher) {
const subtest = root.createSubtest(FileTest, path, async (t) => {
const args = getRunArgs({ path, inspectPort })
const stdio = ['pipe', 'pipe', 'pipe']
const env = { ...process.env }
if (filesWatcher) {
stdio.push('ipc')
env.WATCH_REPORT_DEPENDENCIES = '1'
}

const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8', env, stdio })

const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8' })
// TODO(cjihrig): Implement a TAP parser to read the child's stdout
// instead of just displaying it all if the child fails.
let err
let stderr = ''

filesWatcher?.watchChildProcessModules(child, path)

child.on('error', (error) => {
err = error
})
Expand All @@ -141,6 +246,17 @@ function runTestFile (path, root, inspectPort) {
})
}

const parser = new TapParser()
child.stderr.pipe(parser).on('data', (ast) => {
if (ast.lexeme && isInspectorMessage(ast.lexeme)) {
process.stderr.write(ast.lexeme + '\n')
}
})

child.stdout.pipe(parser).on('data', (ast) => {
subtest.addToReport(ast)
})

const { 0: { 0: code, 1: signal }, 1: stdout } = await SafePromiseAll([
once(child, 'exit', { signal: t.signal }),
toArray.call(child.stdout, { signal: t.signal })
Expand Down
Loading

0 comments on commit 6cd562d

Please sign in to comment.