-
-
Notifications
You must be signed in to change notification settings - Fork 31.7k
test_runner: add TAP parser #43525
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
test_runner: add TAP parser #43525
Changes from all commits
Commits
Show all changes
98 commits
Select commit
Hold shift + click to select a range
a9412ce
test_runner: add initial TAP parser
manekinekko 487ccd4
test_runner: wip
manekinekko 605a29c
test_runner: tap parser (wip)
manekinekko 01ecd07
test_runner: tap parser (wip)
manekinekko 3b01d1c
test_runner: tap parser (wip)
manekinekko 3951075
test_runner: tap parser
manekinekko 61dc763
test_runner: tap parser
manekinekko 23b0e10
test_runner: tap parser
manekinekko 660cf7f
test_runner: tap parser
manekinekko dfbaedd
test_runner: tap parser
manekinekko 37d9ecf
test_runner: tap parser
manekinekko 4a8c65b
test_runner: tap parser
manekinekko 3ee7515
test_runner: tap parser
manekinekko 2416978
test_runner: tap parser
manekinekko 0ece7b0
test_runner: tap parser
manekinekko 9ab6884
test_runner: tap parser
manekinekko 517a908
test_runner: tap parser
manekinekko 9475fa9
test_runner: tap parser
manekinekko 3833046
test_runner: tap parser
manekinekko 62246df
test_runner: tap parser
manekinekko e4b2e9b
test_runner: tap parser
manekinekko 5ed3ca3
test_runner: tap parser
manekinekko 7acf083
test_runner: tap parser
manekinekko a0d5a8e
test_runner: remove scanAll()
manekinekko 0a1924a
test_runner: add tests for TapParser
manekinekko 65ae33c
test_runner: improve YAML block parsing
manekinekko c8406cc
test_runner: improve edge cases parsing
manekinekko 09ce126
test_runner: add TAP checker
manekinekko bc02445
test_runner: use consistent method names
manekinekko e7217b0
test_runner: refactor tap checker
manekinekko f5a5d50
test_runner: multiple changes
manekinekko 8a77263
test_runner: multiple changes in lexer
manekinekko aa9ce96
test_runner: fix linting issues
manekinekko 32028b9
test_runner: add initial stream parsing foundation
manekinekko 4f2392a
test_runner: add stream parsing support
manekinekko 86d554b
test_runner: remove unused fixture
manekinekko ff3e1a5
test_runner: rename level to nesting
manekinekko 7e65f67
test_runner: remove unnecessary calls
manekinekko b05921f
test_runner: fix formatting
manekinekko 8684a53
test_runner: update test events in test.md
manekinekko a6051d2
test_runner: remove unnecessary async
manekinekko 5090016
test_runner: rename variable
manekinekko 7a56b6e
test_runner: fix lint errors
manekinekko 83c5d05
test_runner: lowercase TAP validation errors
manekinekko 4a2e830
test_runner: remove --expose-internals
manekinekko 2a5c87b
test_runner: add regex parser
manekinekko 7a5a6a2
test_runner: optimize lexer
manekinekko 1714ed8
test_runner: add benchmark results
manekinekko c75d215
test_runner: fix lint errors
manekinekko 24f5463
test_runner: output invalid nesting as comment
manekinekko 84b9608
test_runner: support other TAP specs in stream
manekinekko b7d58d6
test_runner: Update doc/api/test.md
manekinekko 92a1ff7
test_runner: Update doc/api/test.md
manekinekko a73a2c8
test_runner: fix lint errors
manekinekko 2c4996a
test_runner: update tests
manekinekko 62d42e7
test_runner: migrate to primordials
manekinekko 7bdd9fc
test_runner: fix lint errors
manekinekko a724ebb
test_runner: clean unit tests
manekinekko 0d2f1b7
test_runner: remove benchmark code
manekinekko ee914de
test_runner: address code review changes
manekinekko d3f5b73
test_runner: scanTAPkeyword -> scanTAPKeyword
manekinekko d33b5c6
test_runner: address code review
manekinekko 153a1eb
test_runner: fix typo
manekinekko 2522806
test_runner: handle empty TAP content
manekinekko 75607bf
test_runner: handle tests emitted by child process
manekinekko abb9841
test_runner: fix lint errors
manekinekko 754289b
test_runner: add time + comments to test nodes
manekinekko 2959e52
test_runner: update runner with new AST signature
manekinekko 882ac35
test_runner: add test for TAP stream parser
manekinekko 8b9aeb8
test_runner: emit diagnostic data with test points
manekinekko addc228
test_runner: add new line in test file
manekinekko 880eacd
test_runner: add output message tests
manekinekko e78a43f
test_runner: parse extra AssertionError YAML keys
manekinekko b68be46
test_runner: surface data on stderr
manekinekko 88d63da
test_runner: revert tap_stream code
manekinekko eef13be
test_runner: surface invalid data on stderr
manekinekko 3281172
test_runner: rename fixture file name
manekinekko 5500967
Update test/parallel/test-runner-tap-checker.js
manekinekko 84048d3
Update lib/internal/test_runner/tap_stream.js
manekinekko 05966ea
Update lib/internal/test_runner/tap_stream.js
manekinekko 5d17b66
Update test/message/test_runner_output_cli.js
manekinekko 6a9c2db
Update lib/internal/test_runner/tap_checker.js
manekinekko 815619d
test_runner: sort TAP errors alphabetically
manekinekko e87ea14
test_runner: move kDefaultIndent to test.js
manekinekko 7391efa
test_runner: update TAP version in test message
manekinekko 87fb339
test_runner: update formatting
manekinekko 07aa2cb
test_runner: revert surfacing data on stderr
manekinekko 002e780
test_runner: improve buffered test points
manekinekko bb6dcf8
test_runner: remove message tests - add parallel
manekinekko 56d7cec
test_runner: fix regex lint errors
manekinekko 7559958
test_runner: revert testcfg.py
manekinekko 7a606e6
test_runner: clean console logs
manekinekko c454f9b
test_runner: buffer stream data
manekinekko 8c83787
test_runner: fix undefined callbacks
manekinekko 4194651
test_runner: add aduh95 suggestions from code review
manekinekko 962fae8
test_runner: fix conflicts with main
manekinekko d355e4c
test_runner: get rid of the unused code blocks in TAPChecker tests
manekinekko 4f5bbc2
test_runner: fix indentation in TAPChecker tests
manekinekko File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
const { | ||
ArrayFrom, | ||
ArrayPrototypeFilter, | ||
ArrayPrototypeForEach, | ||
ArrayPrototypeIncludes, | ||
ArrayPrototypeJoin, | ||
ArrayPrototypePush, | ||
|
@@ -14,6 +15,7 @@ const { | |
SafePromiseAllSettledReturnVoid, | ||
SafeMap, | ||
SafeSet, | ||
StringPrototypeRepeat, | ||
} = primordials; | ||
|
||
const { spawn } = require('child_process'); | ||
|
@@ -31,7 +33,10 @@ const { validateArray, validateBoolean } = 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, | ||
|
@@ -120,11 +125,103 @@ function getRunArgs({ path, inspectPort }) { | |
return argv; | ||
} | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. probably better to wrap this case in curlies rather than disable a good lint rule. case TokenKind.TAP_TEST_POINT: {
const { todo, skip, pass } = node.status;
} |
||
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(); | ||
} | ||
} | ||
|
||
const runningProcesses = new SafeMap(); | ||
const runningSubtests = new SafeMap(); | ||
|
||
function runTestFile(path, root, inspectPort, filesWatcher) { | ||
const subtest = root.createSubtest(Test, path, async (t) => { | ||
const subtest = root.createSubtest(FileTest, path, async (t) => { | ||
const args = getRunArgs({ path, inspectPort }); | ||
const stdio = ['pipe', 'pipe', 'pipe']; | ||
const env = { ...process.env }; | ||
|
@@ -135,8 +232,7 @@ function runTestFile(path, root, inspectPort, filesWatcher) { | |
|
||
const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8', env, stdio }); | ||
runningProcesses.set(path, child); | ||
// 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 = ''; | ||
|
||
|
@@ -159,6 +255,17 @@ function runTestFile(path, root, inspectPort, filesWatcher) { | |
}); | ||
} | ||
|
||
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 }), | ||
child.stdout.toArray({ signal: t.signal }), | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
'use strict'; | ||
|
||
const { | ||
ArrayPrototypeFilter, | ||
ArrayPrototypeFind, | ||
NumberParseInt, | ||
} = primordials; | ||
const { | ||
codes: { ERR_TAP_VALIDATION_ERROR }, | ||
} = require('internal/errors'); | ||
const { TokenKind } = require('internal/test_runner/tap_lexer'); | ||
|
||
// TODO(@manekinekko): add more validation rules based on the TAP14 spec. | ||
// See https://testanything.org/tap-version-14-specification.html | ||
class TAPValidationStrategy { | ||
cjihrig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
validate(ast) { | ||
this.#validateVersion(ast); | ||
this.#validatePlan(ast); | ||
this.#validateTestPoints(ast); | ||
|
||
return true; | ||
} | ||
|
||
#validateVersion(ast) { | ||
const entry = ArrayPrototypeFind( | ||
ast, | ||
(node) => node.kind === TokenKind.TAP_VERSION | ||
); | ||
|
||
if (!entry) { | ||
throw new ERR_TAP_VALIDATION_ERROR('missing TAP version'); | ||
} | ||
|
||
const { version } = entry.node; | ||
|
||
// TAP14 specification is compatible with observed behavior of existing TAP13 consumers and producers | ||
if (version !== '14' && version !== '13') { | ||
throw new ERR_TAP_VALIDATION_ERROR('TAP version should be 13 or 14'); | ||
} | ||
} | ||
|
||
#validatePlan(ast) { | ||
const entry = ArrayPrototypeFind( | ||
ast, | ||
(node) => node.kind === TokenKind.TAP_PLAN | ||
); | ||
|
||
if (!entry) { | ||
throw new ERR_TAP_VALIDATION_ERROR('missing TAP plan'); | ||
} | ||
|
||
const plan = entry.node; | ||
|
||
if (!plan.start) { | ||
throw new ERR_TAP_VALIDATION_ERROR('missing plan start'); | ||
} | ||
|
||
if (!plan.end) { | ||
throw new ERR_TAP_VALIDATION_ERROR('missing plan end'); | ||
} | ||
|
||
const planStart = NumberParseInt(plan.start, 10); | ||
const planEnd = NumberParseInt(plan.end, 10); | ||
|
||
if (planEnd !== 0 && planStart > planEnd) { | ||
throw new ERR_TAP_VALIDATION_ERROR( | ||
`plan start ${planStart} is greater than plan end ${planEnd}` | ||
); | ||
} | ||
} | ||
|
||
// TODO(@manekinekko): since we are dealing with a flat AST, we need to | ||
// validate test points grouped by their "nesting" level. This is because a set of | ||
// Test points belongs to a TAP document. Each new subtest block creates a new TAP document. | ||
// https://testanything.org/tap-version-14-specification.html#subtests | ||
#validateTestPoints(ast) { | ||
cjihrig marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const bailoutEntry = ArrayPrototypeFind( | ||
ast, | ||
(node) => node.kind === TokenKind.TAP_BAIL_OUT | ||
); | ||
const planEntry = ArrayPrototypeFind( | ||
ast, | ||
(node) => node.kind === TokenKind.TAP_PLAN | ||
); | ||
const testPointEntries = ArrayPrototypeFilter( | ||
ast, | ||
(node) => node.kind === TokenKind.TAP_TEST_POINT | ||
); | ||
|
||
const plan = planEntry.node; | ||
|
||
const planStart = NumberParseInt(plan.start, 10); | ||
const planEnd = NumberParseInt(plan.end, 10); | ||
|
||
if (planEnd === 0 && testPointEntries.length > 0) { | ||
throw new ERR_TAP_VALIDATION_ERROR( | ||
`found ${testPointEntries.length} Test Point${ | ||
testPointEntries.length > 1 ? 's' : '' | ||
} but plan is ${planStart}..0` | ||
); | ||
} | ||
|
||
if (planEnd > 0) { | ||
if (testPointEntries.length === 0) { | ||
throw new ERR_TAP_VALIDATION_ERROR('missing Test Points'); | ||
} | ||
|
||
if (!bailoutEntry && testPointEntries.length !== planEnd) { | ||
throw new ERR_TAP_VALIDATION_ERROR( | ||
`test Points count ${testPointEntries.length} does not match plan count ${planEnd}` | ||
); | ||
} | ||
|
||
for (let i = 0; i < testPointEntries.length; i++) { | ||
const test = testPointEntries[i].node; | ||
const testId = NumberParseInt(test.id, 10); | ||
|
||
if (testId < planStart || testId > planEnd) { | ||
throw new ERR_TAP_VALIDATION_ERROR( | ||
`test ${testId} is out of plan range ${planStart}..${planEnd}` | ||
); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
// TAP14 and TAP13 are compatible with each other | ||
class TAP13ValidationStrategy extends TAPValidationStrategy {} | ||
class TAP14ValidationStrategy extends TAPValidationStrategy {} | ||
|
||
class TapChecker { | ||
static TAP13 = '13'; | ||
static TAP14 = '14'; | ||
|
||
constructor({ specs }) { | ||
switch (specs) { | ||
case TapChecker.TAP13: | ||
this.strategy = new TAP13ValidationStrategy(); | ||
break; | ||
default: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should be: switch (specs) {
case TapChecker.TAP13:
case TapChecker.TAP14:
this.strategy = new TAP13ValidationStrategy();
break;
default:
throw new Error(`Unsupported tap version ${specs}`);
} |
||
this.strategy = new TAP14ValidationStrategy(); | ||
} | ||
} | ||
|
||
check(ast) { | ||
return this.strategy.validate(ast); | ||
} | ||
} | ||
|
||
module.exports = { | ||
TapChecker, | ||
TAP14ValidationStrategy, | ||
TAP13ValidationStrategy, | ||
}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.