Skip to content
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

test_runner: add TAP parser #43525

Merged
merged 98 commits into from Nov 21, 2022
Merged
Show file tree
Hide file tree
Changes from 96 commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
a9412ce
test_runner: add initial TAP parser
manekinekko Jun 11, 2022
487ccd4
test_runner: wip
manekinekko Jun 12, 2022
605a29c
test_runner: tap parser (wip)
manekinekko Jun 13, 2022
01ecd07
test_runner: tap parser (wip)
manekinekko Jun 13, 2022
3b01d1c
test_runner: tap parser (wip)
manekinekko Jun 14, 2022
3951075
test_runner: tap parser
manekinekko Jun 14, 2022
61dc763
test_runner: tap parser
manekinekko Jun 16, 2022
23b0e10
test_runner: tap parser
manekinekko Jun 17, 2022
660cf7f
test_runner: tap parser
manekinekko Jun 17, 2022
dfbaedd
test_runner: tap parser
manekinekko Jun 17, 2022
37d9ecf
test_runner: tap parser
manekinekko Jun 17, 2022
4a8c65b
test_runner: tap parser
manekinekko Jun 17, 2022
3ee7515
test_runner: tap parser
manekinekko Jun 17, 2022
2416978
test_runner: tap parser
manekinekko Jun 17, 2022
0ece7b0
test_runner: tap parser
manekinekko Jun 19, 2022
9ab6884
test_runner: tap parser
manekinekko Jun 19, 2022
517a908
test_runner: tap parser
manekinekko Jun 20, 2022
9475fa9
test_runner: tap parser
manekinekko Jun 20, 2022
3833046
test_runner: tap parser
manekinekko Jun 20, 2022
62246df
test_runner: tap parser
manekinekko Jun 20, 2022
e4b2e9b
test_runner: tap parser
manekinekko Jun 20, 2022
5ed3ca3
test_runner: tap parser
manekinekko Jun 20, 2022
7acf083
test_runner: tap parser
manekinekko Jun 21, 2022
a0d5a8e
test_runner: remove scanAll()
manekinekko Jun 27, 2022
0a1924a
test_runner: add tests for TapParser
manekinekko Jun 27, 2022
65ae33c
test_runner: improve YAML block parsing
manekinekko Jun 30, 2022
c8406cc
test_runner: improve edge cases parsing
manekinekko Jul 1, 2022
09ce126
test_runner: add TAP checker
manekinekko Jul 1, 2022
bc02445
test_runner: use consistent method names
manekinekko Jul 2, 2022
e7217b0
test_runner: refactor tap checker
manekinekko Jul 12, 2022
f5a5d50
test_runner: multiple changes
manekinekko Jul 12, 2022
8a77263
test_runner: multiple changes in lexer
manekinekko Jul 12, 2022
aa9ce96
test_runner: fix linting issues
manekinekko Jul 13, 2022
32028b9
test_runner: add initial stream parsing foundation
manekinekko Sep 22, 2022
4f2392a
test_runner: add stream parsing support
manekinekko Sep 22, 2022
86d554b
test_runner: remove unused fixture
manekinekko Sep 23, 2022
ff3e1a5
test_runner: rename level to nesting
manekinekko Sep 23, 2022
7e65f67
test_runner: remove unnecessary calls
manekinekko Sep 23, 2022
b05921f
test_runner: fix formatting
manekinekko Sep 23, 2022
8684a53
test_runner: update test events in test.md
manekinekko Sep 23, 2022
a6051d2
test_runner: remove unnecessary async
manekinekko Sep 23, 2022
5090016
test_runner: rename variable
manekinekko Sep 23, 2022
7a56b6e
test_runner: fix lint errors
manekinekko Sep 27, 2022
83c5d05
test_runner: lowercase TAP validation errors
manekinekko Sep 27, 2022
4a2e830
test_runner: remove --expose-internals
manekinekko Sep 27, 2022
2a5c87b
test_runner: add regex parser
manekinekko Sep 30, 2022
7a5a6a2
test_runner: optimize lexer
manekinekko Oct 1, 2022
1714ed8
test_runner: add benchmark results
manekinekko Oct 1, 2022
c75d215
test_runner: fix lint errors
manekinekko Oct 1, 2022
24f5463
test_runner: output invalid nesting as comment
manekinekko Oct 1, 2022
84b9608
test_runner: support other TAP specs in stream
manekinekko Oct 1, 2022
b7d58d6
test_runner: Update doc/api/test.md
manekinekko Oct 3, 2022
92a1ff7
test_runner: Update doc/api/test.md
manekinekko Oct 3, 2022
a73a2c8
test_runner: fix lint errors
manekinekko Oct 3, 2022
2c4996a
test_runner: update tests
manekinekko Oct 3, 2022
62d42e7
test_runner: migrate to primordials
manekinekko Oct 5, 2022
7bdd9fc
test_runner: fix lint errors
manekinekko Oct 5, 2022
a724ebb
test_runner: clean unit tests
manekinekko Oct 6, 2022
0d2f1b7
test_runner: remove benchmark code
manekinekko Oct 7, 2022
ee914de
test_runner: address code review changes
manekinekko Oct 10, 2022
d3f5b73
test_runner: scanTAPkeyword -> scanTAPKeyword
manekinekko Oct 10, 2022
d33b5c6
test_runner: address code review
manekinekko Oct 10, 2022
153a1eb
test_runner: fix typo
manekinekko Oct 10, 2022
2522806
test_runner: handle empty TAP content
manekinekko Oct 10, 2022
75607bf
test_runner: handle tests emitted by child process
manekinekko Oct 11, 2022
abb9841
test_runner: fix lint errors
manekinekko Oct 11, 2022
754289b
test_runner: add time + comments to test nodes
manekinekko Oct 11, 2022
2959e52
test_runner: update runner with new AST signature
manekinekko Oct 12, 2022
882ac35
test_runner: add test for TAP stream parser
manekinekko Oct 12, 2022
8b9aeb8
test_runner: emit diagnostic data with test points
manekinekko Oct 12, 2022
addc228
test_runner: add new line in test file
manekinekko Oct 13, 2022
880eacd
test_runner: add output message tests
manekinekko Oct 16, 2022
e78a43f
test_runner: parse extra AssertionError YAML keys
manekinekko Oct 16, 2022
b68be46
test_runner: surface data on stderr
manekinekko Oct 16, 2022
88d63da
test_runner: revert tap_stream code
manekinekko Oct 16, 2022
eef13be
test_runner: surface invalid data on stderr
manekinekko Oct 16, 2022
3281172
test_runner: rename fixture file name
manekinekko Oct 16, 2022
5500967
Update test/parallel/test-runner-tap-checker.js
manekinekko Oct 17, 2022
84048d3
Update lib/internal/test_runner/tap_stream.js
manekinekko Oct 17, 2022
05966ea
Update lib/internal/test_runner/tap_stream.js
manekinekko Oct 17, 2022
5d17b66
Update test/message/test_runner_output_cli.js
manekinekko Oct 17, 2022
6a9c2db
Update lib/internal/test_runner/tap_checker.js
manekinekko Oct 17, 2022
815619d
test_runner: sort TAP errors alphabetically
manekinekko Oct 17, 2022
e87ea14
test_runner: move kDefaultIndent to test.js
manekinekko Oct 17, 2022
7391efa
test_runner: update TAP version in test message
manekinekko Oct 17, 2022
87fb339
test_runner: update formatting
manekinekko Oct 17, 2022
07aa2cb
test_runner: revert surfacing data on stderr
manekinekko Oct 17, 2022
002e780
test_runner: improve buffered test points
manekinekko Oct 20, 2022
bb6dcf8
test_runner: remove message tests - add parallel
manekinekko Oct 21, 2022
56d7cec
test_runner: fix regex lint errors
manekinekko Oct 21, 2022
7559958
test_runner: revert testcfg.py
manekinekko Oct 21, 2022
7a606e6
test_runner: clean console logs
manekinekko Oct 21, 2022
c454f9b
test_runner: buffer stream data
manekinekko Oct 27, 2022
8c83787
test_runner: fix undefined callbacks
manekinekko Oct 28, 2022
4194651
test_runner: add aduh95 suggestions from code review
manekinekko Nov 19, 2022
962fae8
test_runner: fix conflicts with main
manekinekko Nov 19, 2022
d355e4c
test_runner: get rid of the unused code blocks in TAPChecker tests
manekinekko Nov 21, 2022
4f5bbc2
test_runner: fix indentation in TAPChecker tests
manekinekko Nov 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 19 additions & 0 deletions doc/api/errors.md
Expand Up @@ -2690,6 +2690,25 @@ An unspecified or non-specific system error has occurred within the Node.js
process. The error object will have an `err.info` object property with
additional details.

<a id="ERR_TAP_LEXER_ERROR"></a>

### `ERR_TAP_LEXER_ERROR`

An error representing a failing lexer state.

<a id="ERR_TAP_PARSER_ERROR"></a>

### `ERR_TAP_PARSER_ERROR`

An error representing a failing parser state. Additional information about
the token causing the error is available via the `cause` property.

<a id="ERR_TAP_VALIDATION_ERROR"></a>

### `ERR_TAP_VALIDATION_ERROR`

This error represents a failed TAP validation.

<a id="ERR_TEST_FAILURE"></a>

### `ERR_TEST_FAILURE`
Expand Down
5 changes: 2 additions & 3 deletions doc/api/test.md
Expand Up @@ -1042,8 +1042,7 @@ Emitted when [`context.diagnostic`][] is called.
### Event: `'test:fail'`

* `data` {Object}
* `duration` {number} The test duration.
cjihrig marked this conversation as resolved.
Show resolved Hide resolved
* `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 @@ -1054,7 +1053,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
15 changes: 15 additions & 0 deletions lib/internal/errors.js
Expand Up @@ -1595,6 +1595,21 @@ E('ERR_STREAM_WRAP', 'Stream has StringDecoder set or is in objectMode', Error);
E('ERR_STREAM_WRITE_AFTER_END', 'write after end', Error);
E('ERR_SYNTHETIC', 'JavaScript Callstack', Error);
E('ERR_SYSTEM_ERROR', 'A system error occurred', SystemError);
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
115 changes: 111 additions & 4 deletions lib/internal/test_runner/runner.js
Expand Up @@ -2,6 +2,7 @@
const {
ArrayFrom,
ArrayPrototypeFilter,
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
ArrayPrototypeJoin,
ArrayPrototypePush,
Expand All @@ -14,6 +15,7 @@ const {
SafePromiseAllSettledReturnVoid,
SafeMap,
SafeSet,
StringPrototypeRepeat,
} = primordials;

const { spawn } = require('child_process');
Expand All @@ -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,
Expand Down Expand Up @@ -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

Choose a reason for hiding this comment

The 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 };
Expand All @@ -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 = '';

Expand All @@ -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 }),
Expand Down
155 changes: 155 additions & 0 deletions lib/internal/test_runner/tap_checker.js
@@ -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:

Choose a reason for hiding this comment

The 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,
};