diff --git a/lib/internal/test_runner/tap_parser.js b/lib/internal/test_runner/tap_parser.js index 18d7c9eab0864a..7740d811e96598 100644 --- a/lib/internal/test_runner/tap_parser.js +++ b/lib/internal/test_runner/tap_parser.js @@ -88,7 +88,7 @@ class TapParser extends Transform { this.#lexer = new TapLexer(chunkAsString); try { - this.#tokens = this.#lexer.scan(); + this.#tokens = this.#scanTokens(); this.#parseTokens(callback); } catch (error) { callback(null, error); @@ -96,10 +96,14 @@ class TapParser extends Transform { } parseSync(input = '', callback = null) { + if (typeof input !== 'string' || input === '') { + return []; + } + this.#isSyncParsingEnabled = true; this.input = input; this.#lexer = new TapLexer(input); - this.#tokens = this.#lexer.scan(); + this.#tokens = this.#scanTokens(); this.#parseTokens(callback); @@ -170,6 +174,10 @@ class TapParser extends Transform { // ----------------------------- Private API ----------------------------// // ----------------------------------------------------------------------// + #scanTokens() { + return this.#lexer.scan(); + } + #parseTokens(callback = null) { for (let index = 0; index < this.#tokens.length; index++) { const chunk = this.#tokens[index]; @@ -200,12 +208,14 @@ class TapParser extends Transform { const astNode = this.#TAPDocument(chunk); - callback?.({ - ...astNode, - nesting: this.#subTestNestingLevel, - lexeme: ArrayPrototypeMap(chunk, (token) => token.value).join(''), - tokens: chunk, - }); + // Mark the current chunk as processed + // only when there is data to emit + if (astNode) { + callback?.({ + ...astNode, + lexeme: this.#serializeChunk(chunk), + }); + } // Move pointers to the next chunk and reset the current token index this.#currentTokenChunk++; @@ -322,6 +332,14 @@ class TapParser extends Transform { nesting: this.#subTestNestingLevel, }; + // If we find invalid TAP content, don't add it to AST + if ( + value.kind === TokenKind.UNKNOWN && + value.node.value.includes('nesting:') + ) { + return value; + } + // If we are emitting a YAML block, we need to update the last node in the // AST to include the YAML block as diagnostic. // We only do this to produce an AST with fewer nodes. @@ -337,9 +355,16 @@ class TapParser extends Transform { lastNode.node.diagnostics ||= []; ArrayPrototypeForEach(value.node.diagnostics, (diagnostic) => { - ArrayPrototypePush(lastNode.node.diagnostics, diagnostic); + // Avoid adding empty diagnostics + if (diagnostic) { + ArrayPrototypePush(lastNode.node.diagnostics, diagnostic); + } }); + // no need to pass this node to the stream interface + // because diagnostics are already added to the last test node + return; + // eslint-disable-next-line brace-style } @@ -427,21 +452,25 @@ class TapParser extends Transform { break; case TokenKind.EOL: case TokenKind.EOF: - // Read and ignore token - this.#next(false); - break; + // Consume and ignore token + return this.#next(false); default: // Read token because error needs the last token details this.#next(false); this.#error('Expected a valid token'); } } - return { + + const node = { kind: TokenKind.UNKNOWN, node: { value: tokenChunksAsString, }, }; + + // We make sure the emitted node has the same shape + // both in sync and async parsing (for the stream interface) + return this.#emit(node); } // ----------------Version---------------- @@ -699,14 +728,13 @@ class TapParser extends Transform { this.#isYAMLBlock = true; this.#yamlCurrentIndentationLevel = this.#subTestNestingLevel; + + // Consume the YAML start marker this.#next(false); // skip "---" - return { - kind: TokenKind.TAP_YAML_START, - node: { - diagnostics: this.#yamlBlockBuffer, - }, - }; + // No need to pass this token to the stream interface + return; + } else if (yamlBlockSymbol.kind === TokenKind.TAP_YAML_END) { this.#next(false); // skip "..." @@ -733,12 +761,12 @@ class TapParser extends Transform { if (this.#isYAMLBlock) { this.#YAMLLine(); } else { - return { + return this.#emit({ kind: TokenKind.UNKNOWN, node: { value: yamlBlockSymbol.value, }, - }; + }); } } @@ -752,6 +780,7 @@ class TapParser extends Transform { case 'duration_ms': this.#lastTestPointDuration = Number(value); break; + // TODO(@manekinekko): should we handle other diagnostics data? } ArrayPrototypePush(this.#yamlBlockBuffer, yamlLiteral); diff --git a/test/parallel/test-runner-tap-parser-stream.js b/test/parallel/test-runner-tap-parser-stream.js new file mode 100644 index 00000000000000..6b37d94796c853 --- /dev/null +++ b/test/parallel/test-runner-tap-parser-stream.js @@ -0,0 +1,250 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +const assert = require('node:assert'); +const { TapParser } = require('internal/test_runner/tap_parser'); + +const cases = [ + { + input: 'TAP version 13', + expected: [ + { + nesting: 0, + kind: 'VersionKeyword', + node: { version: '13' }, + lexeme: 'TAP version 13', + }, + ], + }, + { + input: 'invalid tap', + expected: [ + { + nesting: 0, + kind: 'Unknown', + node: { value: 'invalid tap' }, + lexeme: 'invalid tap', + }, + ], + }, + { + input: 'TAP version 13\ninvalid tap after harness', + expected: [ + { + nesting: 0, + kind: 'VersionKeyword', + node: { version: '13' }, + lexeme: 'TAP version 13', + }, + { + nesting: 0, + kind: 'Unknown', + node: { value: 'invalid tap after harness' }, + lexeme: 'invalid tap after harness', + }, + ], + }, + { + input: `TAP version 13 + # nested diagnostic +# diagnostic comment`, + expected: [ + { + nesting: 0, + kind: 'VersionKeyword', + node: { version: '13' }, + lexeme: 'TAP version 13', + }, + { + nesting: 1, + kind: 'Comment', + node: { comment: 'nested diagnostic' }, + lexeme: ' # nested diagnostic', + }, + { + nesting: 0, + kind: 'Comment', + node: { comment: 'diagnostic comment' }, + lexeme: '# diagnostic comment', + }, + ], + }, + { + input: `TAP version 13 + 1..5 +1..3 +2..2`, + expected: [ + { + nesting: 0, + kind: 'VersionKeyword', + node: { version: '13' }, + lexeme: 'TAP version 13', + }, + { + nesting: 1, + kind: 'PlanKeyword', + node: { start: '1', end: '5' }, + lexeme: ' 1..5', + }, + { + nesting: 0, + kind: 'PlanKeyword', + node: { start: '1', end: '3' }, + lexeme: '1..3', + }, + { + nesting: 0, + kind: 'PlanKeyword', + node: { start: '2', end: '2' }, + lexeme: '2..2', + }, + ], + }, + { + input: `TAP version 13 +ok 1 - test +ok 2 - test # SKIP +not ok 3 - test # TODO reason`, + expected: [ + { + nesting: 0, + kind: 'VersionKeyword', + node: { version: '13' }, + lexeme: 'TAP version 13', + }, + { + nesting: 0, + kind: 'TestPointKeyword', + node: { + status: { fail: false, pass: true, todo: false, skip: false }, + id: '1', + description: 'test', + reason: '', + time: 0, + }, + lexeme: 'ok 1 - test', + }, + { + nesting: 0, + kind: 'TestPointKeyword', + node: { + status: { fail: false, pass: true, todo: false, skip: true }, + id: '2', + description: 'test', + reason: '', + time: 0, + }, + lexeme: 'ok 2 - test # SKIP', + }, + { + nesting: 0, + kind: 'TestPointKeyword', + node: { + status: { fail: true, pass: false, todo: true, skip: false }, + id: '3', + description: 'test', + reason: 'reason', + time: 0, + }, + lexeme: 'not ok 3 - test # TODO reason', + }, + ], + }, + { + input: `TAP version 13 +# Subtest: test +ok 1 - test +ok 2 - test`, + expected: [ + { + nesting: 0, + kind: 'VersionKeyword', + node: { version: '13' }, + lexeme: 'TAP version 13', + }, + { + nesting: 0, + kind: 'SubTestPointKeyword', + node: { name: 'test' }, + lexeme: '# Subtest: test', + }, + { + nesting: 0, + kind: 'TestPointKeyword', + node: { + status: { fail: false, pass: true, todo: false, skip: false }, + id: '1', + description: 'test', + reason: '', + time: 0, + }, + lexeme: 'ok 1 - test', + }, + { + nesting: 0, + kind: 'TestPointKeyword', + node: { + status: { fail: false, pass: true, todo: false, skip: false }, + id: '2', + description: 'test', + reason: '', + time: 0, + }, + lexeme: 'ok 2 - test', + }, + ], + }, + { + input: `TAP version 13 +# Subtest: test +ok 1 - test + --- + foo: bar + duration_ms: 0.0001 + prop: |- + multiple + lines + ...`, + expected: [ + { + nesting: 0, + kind: 'VersionKeyword', + node: { version: '13' }, + lexeme: 'TAP version 13', + }, + { + nesting: 0, + kind: 'SubTestPointKeyword', + node: { name: 'test' }, + lexeme: '# Subtest: test', + }, + { + nesting: 0, + kind: 'TestPointKeyword', + node: { + status: { fail: false, pass: true, todo: false, skip: false }, + id: '1', + description: 'test', + reason: '', + time: 0.0001, + diagnostics: ['foo: bar', 'duration_ms: 0.0001', 'prop: |-', ' multiple', ' lines'], + }, + lexeme: 'ok 1 - test', + }, + ], + }, +]; + +(async () => { + for (const { input, expected } of cases) { + const parser = new TapParser(); + parser.write(input); + parser.end(); + const actual = await parser.toArray(); + assert.deepStrictEqual( + actual, + expected.map((item) => ({ __proto__: null, ...item })) + ); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-runner-tap-parser.js b/test/parallel/test-runner-tap-parser.js index ca2b408debebb0..93056cef58395f 100644 --- a/test/parallel/test-runner-tap-parser.js +++ b/test/parallel/test-runner-tap-parser.js @@ -1055,9 +1055,7 @@ not ok 1 - /test.js # skipped 0 # todo 0 # duration_ms 87.077507 - `); - - console.log({ ast }); +`); assert.deepStrictEqual(ast, [ {