Skip to content

Commit ac68f75

Browse files
manekinekkoMoLow
authored andcommitted
feat: add initial TAP parser
Work in progress PR-URL: nodejs/node#43525 Refs: nodejs/node#43344 Reviewed-By: Franziska Hinkelmann <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Moshe Atlow <[email protected]> (cherry picked from commit f8ce9117b19702487eb600493d941f7876e00e01)
1 parent 5f3b2de commit ac68f75

17 files changed

+4431
-39
lines changed

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ expected.
368368

369369
```mjs
370370
import assert from 'node:assert';
371-
import { mock, test } from 'node:test';
371+
import { mock, test } from 'test';
372372
test('spies on a function', () => {
373373
const sum = mock.fn((a, b) => {
374374
return a + b;
@@ -388,7 +388,7 @@ test('spies on a function', () => {
388388
```cjs
389389
'use strict';
390390
const assert = require('node:assert');
391-
const { mock, test } = require('node:test');
391+
const { mock, test } = require('test');
392392
test('spies on a function', () => {
393393
const sum = mock.fn((a, b) => {
394394
return a + b;
@@ -964,8 +964,7 @@ Emitted when [`context.diagnostic`][] is called.
964964
### Event: `'test:fail'`
965965

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

978977
* `data` {Object}
979-
* `duration` {number} The test duration.
978+
* `details` {Object} Additional execution metadata.
980979
* `name` {string} The test name.
981980
* `testNumber` {number} The ordinal number of the test.
982981
* `todo` {string|undefined} Present if [`context.todo`][] is called

lib/internal/errors.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/lib/internal/errors.js
1+
// https://github.com/nodejs/node/blob/f8ce9117b19702487eb600493d941f7876e00e01/lib/internal/errors.js
22

33
'use strict'
44

@@ -346,6 +346,21 @@ module.exports = {
346346
kIsNodeError
347347
}
348348

349+
E('ERR_TAP_LEXER_ERROR', function (errorMsg) {
350+
hideInternalStackFrames(this)
351+
return errorMsg
352+
}, Error)
353+
E('ERR_TAP_PARSER_ERROR', function (errorMsg, details, tokenCausedError, source) {
354+
hideInternalStackFrames(this)
355+
this.cause = tokenCausedError
356+
const { column, line, start, end } = tokenCausedError.location
357+
const errorDetails = `${details} at line ${line}, column ${column} (start ${start}, end ${end})`
358+
return errorMsg + errorDetails
359+
}, SyntaxError)
360+
E('ERR_TAP_VALIDATION_ERROR', function (errorMsg) {
361+
hideInternalStackFrames(this)
362+
return errorMsg
363+
}, Error)
349364
E('ERR_TEST_FAILURE', function (error, failureType) {
350365
hideInternalStackFrames(this)
351366
assert(typeof failureType === 'string',

lib/internal/per_context/primordials.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ exports.ArrayFrom = (it, mapFn) => Array.from(it, mapFn)
66
exports.ArrayIsArray = Array.isArray
77
exports.ArrayPrototypeConcat = (arr, ...el) => arr.concat(...el)
88
exports.ArrayPrototypeFilter = (arr, fn) => arr.filter(fn)
9+
exports.ArrayPrototypeFind = (arr, fn) => arr.find(fn)
910
exports.ArrayPrototypeForEach = (arr, fn, thisArg) => arr.forEach(fn, thisArg)
1011
exports.ArrayPrototypeIncludes = (arr, el, fromIndex) => arr.includes(el, fromIndex)
1112
exports.ArrayPrototypeJoin = (arr, str) => arr.join(str)
@@ -17,6 +18,7 @@ exports.ArrayPrototypeShift = arr => arr.shift()
1718
exports.ArrayPrototypeSlice = (arr, offset) => arr.slice(offset)
1819
exports.ArrayPrototypeSome = (arr, fn) => arr.some(fn)
1920
exports.ArrayPrototypeSort = (arr, fn) => arr.sort(fn)
21+
exports.ArrayPrototypeSplice = (arr, offset, len, ...el) => arr.splice(offset, len, ...el)
2022
exports.ArrayPrototypeUnshift = (arr, ...el) => arr.unshift(...el)
2123
exports.Error = Error
2224
exports.ErrorCaptureStackTrace = (...args) => Error.captureStackTrace(...args)
@@ -26,6 +28,7 @@ exports.FunctionPrototypeCall = (fn, obj, ...args) => fn.call(obj, ...args)
2628
exports.MathMax = (...args) => Math.max(...args)
2729
exports.Number = Number
2830
exports.NumberIsInteger = Number.isInteger
31+
exports.NumberParseInt = (str, radix) => Number.parseInt(str, radix)
2932
exports.NumberMIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER
3033
exports.NumberMAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER
3134
exports.ObjectAssign = (target, ...sources) => Object.assign(target, ...sources)
@@ -56,12 +59,15 @@ exports.SafeWeakSet = WeakSet
5659
exports.StringPrototypeEndsWith = (haystack, needle, index) => haystack.endsWith(needle, index)
5760
exports.StringPrototypeIncludes = (str, needle) => str.includes(needle)
5861
exports.StringPrototypeMatch = (str, reg) => str.match(reg)
62+
exports.StringPrototypeRepeat = (str, times) => str.repeat(times)
5963
exports.StringPrototypeReplace = (str, search, replacement) =>
6064
str.replace(search, replacement)
6165
exports.StringPrototypeReplaceAll = replaceAll
6266
exports.StringPrototypeStartsWith = (haystack, needle, index) => haystack.startsWith(needle, index)
6367
exports.StringPrototypeSlice = (str, ...args) => str.slice(...args)
6468
exports.StringPrototypeSplit = (str, search, limit) => str.split(search, limit)
69+
exports.StringPrototypeToUpperCase = str => str.toUpperCase()
70+
exports.StringPrototypeTrim = str => str.trim()
6571
exports.Symbol = Symbol
6672
exports.SymbolFor = repr => Symbol.for(repr)
6773
exports.ReflectApply = (target, self, args) => Reflect.apply(target, self, args)

lib/internal/test_runner/runner.js

Lines changed: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
// https://github.com/nodejs/node/blob/9825a7e01d35b9d49ebb58efed2c316012c19db6/lib/internal/test_runner/runner.js
1+
// https://github.com/nodejs/node/blob/f8ce9117b19702487eb600493d941f7876e00e01/lib/internal/test_runner/runner.js
22
'use strict'
33
const {
44
ArrayFrom,
55
ArrayPrototypeFilter,
6+
ArrayPrototypeForEach,
67
ArrayPrototypeIncludes,
78
ArrayPrototypeJoin,
89
ArrayPrototypePush,
@@ -11,7 +12,8 @@ const {
1112
ObjectAssign,
1213
PromisePrototypeThen,
1314
SafePromiseAll,
14-
SafeSet
15+
SafeSet,
16+
StringPrototypeRepeat
1517
} = require('#internal/per_context/primordials')
1618

1719
const { spawn } = require('child_process')
@@ -28,7 +30,9 @@ const { validateArray } = require('#internal/validators')
2830
const { getInspectPort, isUsingInspector, isInspectorMessage } = require('#internal/util/inspector')
2931
const { kEmptyObject } = require('#internal/util')
3032
const { createTestTree } = require('#internal/test_runner/harness')
31-
const { kSubtestsFailed, Test } = require('#internal/test_runner/test')
33+
const { kDefaultIndent, kSubtestsFailed, Test } = require('#internal/test_runner/test')
34+
const { TapParser } = require('#internal/test_runner/tap_parser')
35+
const { TokenKind } = require('#internal/test_runner/tap_lexer')
3236
const {
3337
isSupportedFileType,
3438
doesPathMatchFilter
@@ -114,16 +118,117 @@ function getRunArgs ({ path, inspectPort }) {
114118
return argv
115119
}
116120

117-
function runTestFile (path, root, inspectPort) {
118-
const subtest = root.createSubtest(Test, path, async (t) => {
121+
class FileTest extends Test {
122+
#buffer = []
123+
#handleReportItem ({ kind, node, nesting = 0 }) {
124+
const indent = StringPrototypeRepeat(kDefaultIndent, nesting + 1)
125+
126+
const details = (diagnostic) => {
127+
return (
128+
diagnostic && {
129+
__proto__: null,
130+
yaml:
131+
`${indent} ` +
132+
ArrayPrototypeJoin(diagnostic, `\n${indent} `) +
133+
'\n'
134+
}
135+
)
136+
}
137+
138+
switch (kind) {
139+
case TokenKind.TAP_VERSION:
140+
// TODO(manekinekko): handle TAP version coming from the parser.
141+
// this.reporter.version(node.version);
142+
break
143+
144+
case TokenKind.TAP_PLAN:
145+
this.reporter.plan(indent, node.end - node.start + 1)
146+
break
147+
148+
case TokenKind.TAP_SUBTEST_POINT:
149+
this.reporter.subtest(indent, node.name)
150+
break
151+
152+
case TokenKind.TAP_TEST_POINT:
153+
// eslint-disable-next-line no-case-declarations
154+
const { todo, skip, pass } = node.status
155+
// eslint-disable-next-line no-case-declarations
156+
let directive
157+
158+
if (skip) {
159+
directive = this.reporter.getSkip(node.reason)
160+
} else if (todo) {
161+
directive = this.reporter.getTodo(node.reason)
162+
} else {
163+
directive = kEmptyObject
164+
}
165+
166+
if (pass) {
167+
this.reporter.ok(
168+
indent,
169+
node.id,
170+
node.description,
171+
details(node.diagnostics),
172+
directive
173+
)
174+
} else {
175+
this.reporter.fail(
176+
indent,
177+
node.id,
178+
node.description,
179+
details(node.diagnostics),
180+
directive
181+
)
182+
}
183+
break
184+
185+
case TokenKind.COMMENT:
186+
if (indent === kDefaultIndent) {
187+
// Ignore file top level diagnostics
188+
break
189+
}
190+
this.reporter.diagnostic(indent, node.comment)
191+
break
192+
193+
case TokenKind.UNKNOWN:
194+
this.reporter.diagnostic(indent, node.value)
195+
break
196+
}
197+
}
198+
199+
addToReport (ast) {
200+
if (!this.isClearToSend()) {
201+
ArrayPrototypePush(this.#buffer, ast)
202+
return
203+
}
204+
this.reportSubtest()
205+
this.#handleReportItem(ast)
206+
}
207+
208+
report () {
209+
this.reportSubtest()
210+
ArrayPrototypeForEach(this.#buffer, (ast) => this.#handleReportItem(ast))
211+
super.report()
212+
}
213+
}
214+
215+
function runTestFile (path, root, inspectPort, filesWatcher) {
216+
const subtest = root.createSubtest(FileTest, path, async (t) => {
119217
const args = getRunArgs({ path, inspectPort })
218+
const stdio = ['pipe', 'pipe', 'pipe']
219+
const env = { ...process.env }
220+
if (filesWatcher) {
221+
stdio.push('ipc')
222+
env.WATCH_REPORT_DEPENDENCIES = '1'
223+
}
224+
225+
const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8', env, stdio })
120226

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

230+
filesWatcher?.watchChildProcessModules(child, path)
231+
127232
child.on('error', (error) => {
128233
err = error
129234
})
@@ -141,6 +246,17 @@ function runTestFile (path, root, inspectPort) {
141246
})
142247
}
143248

249+
const parser = new TapParser()
250+
child.stderr.pipe(parser).on('data', (ast) => {
251+
if (ast.lexeme && isInspectorMessage(ast.lexeme)) {
252+
process.stderr.write(ast.lexeme + '\n')
253+
}
254+
})
255+
256+
child.stdout.pipe(parser).on('data', (ast) => {
257+
subtest.addToReport(ast)
258+
})
259+
144260
const { 0: { 0: code, 1: signal }, 1: stdout } = await SafePromiseAll([
145261
once(child, 'exit', { signal: t.signal }),
146262
toArray.call(child.stdout, { signal: t.signal })

0 commit comments

Comments
 (0)