test
Version:
Node.js 18's node:test, as an npm package
991 lines (836 loc) • 28.6 kB
JavaScript
// https://github.com/nodejs/node/blob/4c08c20e575a0954fe3977a20e9f52b4980a2e48/lib/internal/test_runner/tap_parser.js
'use strict'
const {
ArrayPrototypeFilter,
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePop,
ArrayPrototypePush,
Boolean,
Number,
RegExpPrototypeExec,
String,
StringPrototypeEndsWith,
StringPrototypeReplaceAll,
StringPrototypeSlice,
StringPrototypeSplit,
StringPrototypeTrim
} = require('#internal/per_context/primordials')
const Transform = require('stream').Transform
const { TapLexer, TokenKind } = require('#internal/test_runner/tap_lexer')
const { TapChecker } = require('#internal/test_runner/tap_checker')
const {
codes: { ERR_TAP_VALIDATION_ERROR, ERR_TAP_PARSER_ERROR }
} = require('#internal/errors')
const { kEmptyObject } = require('#internal/util')
/**
*
* TAP14 specifications
*
* See https://testanything.org/tap-version-14-specification.html
*
* Note that the following grammar is intended as a rough "pseudocode" guidance.
* It is not strict EBNF:
*
* TAPDocument := Version Plan Body | Version Body Plan
* Version := "TAP version 14\n"
* Plan := "1.." (Number) (" # " Reason)? "\n"
* Body := (TestPoint | BailOut | Pragma | Comment | Anything | Empty | Subtest)*
* TestPoint := ("not ")? "ok" (" " Number)? ((" -")? (" " Description) )? (" " Directive)? "\n" (YAMLBlock)?
* Directive := " # " ("todo" | "skip") (" " Reason)?
* YAMLBlock := " ---\n" (YAMLLine)* " ...\n"
* YAMLLine := " " (YAML)* "\n"
* BailOut := "Bail out!" (" " Reason)? "\n"
* Reason := [^\n]+
* Pragma := "pragma " [+-] PragmaKey "\n"
* PragmaKey := ([a-zA-Z0-9_-])+
* Subtest := ("# Subtest" (": " SubtestName)?)? "\n" SubtestDocument TestPoint
* Comment := ^ (" ")* "#" [^\n]* "\n"
* Empty := [\s\t]* "\n"
* Anything := [^\n]+ "\n"
*
*/
/**
* An LL(1) parser for TAP14/TAP13.
*/
class TapParser extends Transform {
#checker = null
#lexer = null
#currentToken = null
#input = ''
#currentChunkAsString = ''
#lastLine = ''
#tokens = [[]]
#flatAST = []
#bufferedComments = []
#bufferedTestPoints = []
#lastTestPointDetails = {}
#yamlBlockBuffer = []
#currentTokenIndex = 0
#currentTokenChunk = 0
#subTestNestingLevel = 0
#yamlCurrentIndentationLevel = 0
#kSubtestBlockIndentationFactor = 4
#isYAMLBlock = false
#isSyncParsingEnabled = false
constructor ({ specs = TapChecker.TAP13 } = kEmptyObject) {
super({ __proto__: null, readableObjectMode: true })
this.#checker = new TapChecker({ specs })
}
// ----------------------------------------------------------------------//
// ----------------------------- Public API -----------------------------//
// ----------------------------------------------------------------------//
parse (chunkAsString = '', callback = null) {
this.#isSyncParsingEnabled = false
this.#currentTokenChunk = 0
this.#currentTokenIndex = 0
// Note: we are overwriting the input on each stream call
// This is fine because we don't want to parse previous chunks
this.#input = chunkAsString
this.#lexer = new TapLexer(chunkAsString)
try {
this.#tokens = this.#scanTokens()
this.#parseTokens(callback)
} catch (error) {
callback(null, error)
}
}
parseSync (input = '', callback = null) {
if (typeof input !== 'string' || input === '') {
return []
}
this.#isSyncParsingEnabled = true
this.#input = input
this.#lexer = new TapLexer(input)
this.#tokens = this.#scanTokens()
this.#parseTokens(callback)
if (this.#isYAMLBlock) {
// Looks like we have a non-ending YAML block
this.#error('Expected end of YAML block')
}
// Manually flush the remaining buffered comments and test points
this._flush()
return this.#flatAST
}
// Check if the TAP content is semantically valid
// Note: Validating the TAP content requires the whole AST to be available.
check () {
if (this.#isSyncParsingEnabled) {
return this.#checker.check(this.#flatAST)
}
// TODO(@manekinekko): when running in async mode, it doesn't make sense to
// validate the current chunk. Validation needs to whole AST to be available.
throw new ERR_TAP_VALIDATION_ERROR(
'TAP validation is not supported for async parsing'
)
}
// ----------------------------------------------------------------------//
// --------------------------- Transform API ----------------------------//
// ----------------------------------------------------------------------//
processChunk (chunk) {
const str = this.#lastLine + chunk.toString('utf8')
const lines = StringPrototypeSplit(str, '\n')
this.#lastLine = ArrayPrototypePop(lines)
let chunkAsString = ArrayPrototypeJoin(lines, '\n')
// Special case where chunk is emitted by a child process
chunkAsString = StringPrototypeReplaceAll(
chunkAsString,
'[out] ',
''
)
chunkAsString = StringPrototypeReplaceAll(
chunkAsString,
'[err] ',
''
)
if (StringPrototypeEndsWith(chunkAsString, '\n')) {
chunkAsString = StringPrototypeSlice(chunkAsString, 0, -1)
}
if (StringPrototypeEndsWith(chunkAsString, 'EOF')) {
chunkAsString = StringPrototypeSlice(chunkAsString, 0, -3)
}
return chunkAsString
}
_transform (chunk, _encoding, next) {
const chunkAsString = this.processChunk(chunk)
if (!chunkAsString) {
// Ignore empty chunks
next()
return
}
this.parse(chunkAsString, (node, error) => {
if (error) {
next(error)
return
}
if (node.kind === TokenKind.EOF) {
// Emit when the current chunk is fully processed and consumed
next()
}
})
}
// Flush the remaining buffered comments and test points
// This will be called automatically when the stream is closed
// We also call this method manually when we reach the end of the sync parsing
_flush (next = null) {
if (!this.#lastLine) {
this.#__flushPendingTestPointsAndComments()
next?.()
return
}
// Parse the remaining line
this.parse(this.#lastLine, (node, error) => {
this.#lastLine = ''
if (error) {
next?.(error)
return
}
if (node.kind === TokenKind.EOF) {
this.#__flushPendingTestPointsAndComments()
next?.()
}
})
}
#__flushPendingTestPointsAndComments () {
ArrayPrototypeForEach(this.#bufferedTestPoints, (node) => {
this.#emit(node)
})
ArrayPrototypeForEach(this.#bufferedComments, (node) => {
this.#emit(node)
})
// Clean up
this.#bufferedTestPoints = []
this.#bufferedComments = []
}
// ----------------------------------------------------------------------//
// ----------------------------- Private API ----------------------------//
// ----------------------------------------------------------------------//
#scanTokens () {
return this.#lexer.scan()
}
#parseTokens (callback = null) {
for (let index = 0; index < this.#tokens.length; index++) {
const chunk = this.#tokens[index]
this.#parseChunk(chunk)
}
callback?.({ kind: TokenKind.EOF }) // eslint-disable-line n/no-callback-literal
}
#parseChunk (chunk) {
this.#subTestNestingLevel = this.#getCurrentIndentationLevel(chunk)
// We compute the current index of the token in the chunk
// based on the indentation level (number of spaces).
// We also need to take into account if we are in a YAML block or not.
// If we are in a YAML block, we compute the current index of the token
// based on the indentation level of the YAML block (start block).
if (this.#isYAMLBlock) {
this.#currentTokenIndex =
this.#yamlCurrentIndentationLevel *
this.#kSubtestBlockIndentationFactor
} else {
this.#currentTokenIndex =
this.#subTestNestingLevel * this.#kSubtestBlockIndentationFactor
this.#yamlCurrentIndentationLevel = this.#subTestNestingLevel
}
let node
// Parse current chunk
try {
node = this.#TAPDocument(chunk)
} catch {
node = {
kind: TokenKind.UNKNOWN,
node: {
value: this.#currentChunkAsString
}
}
}
// Emit the parsed node to both the stream and the AST
this.#emitOrBufferCurrentNode(node)
// Move pointers to the next chunk and reset the current token index
this.#currentTokenChunk++
this.#currentTokenIndex = 0
}
#error (message) {
const token = this.#currentToken || { value: '', kind: '' }
// Escape NewLine characters
if (token.value === '\n') {
token.value = '\\n'
}
throw new ERR_TAP_PARSER_ERROR(
message,
`, received "${token.value}" (${token.kind})`,
token,
this.#input
)
}
#peek (shouldSkipBlankTokens = true) {
if (shouldSkipBlankTokens) {
this.#skip(TokenKind.WHITESPACE)
}
return this.#tokens[this.#currentTokenChunk][this.#currentTokenIndex]
}
#next (shouldSkipBlankTokens = true) {
if (shouldSkipBlankTokens) {
this.#skip(TokenKind.WHITESPACE)
}
if (this.#tokens[this.#currentTokenChunk]) {
this.#currentToken =
this.#tokens[this.#currentTokenChunk][this.#currentTokenIndex++]
} else {
this.#currentToken = null
}
return this.#currentToken
}
// Skip the provided tokens in the current chunk
#skip (...tokensToSkip) {
let token = this.#tokens[this.#currentTokenChunk][this.#currentTokenIndex]
while (token && ArrayPrototypeIncludes(tokensToSkip, token.kind)) {
// pre-increment to skip current tokens but make sure we don't advance index on the last iteration
token = this.#tokens[this.#currentTokenChunk][++this.#currentTokenIndex]
}
}
#readNextLiterals () {
const literals = []
let nextToken = this.#peek(false)
// Read all literal, numeric, whitespace and escape tokens until we hit a different token
// or reach end of current chunk
while (
nextToken &&
ArrayPrototypeIncludes(
[
TokenKind.LITERAL,
TokenKind.NUMERIC,
TokenKind.DASH,
TokenKind.PLUS,
TokenKind.WHITESPACE,
TokenKind.ESCAPE
],
nextToken.kind
)
) {
const word = this.#next(false).value
// Don't output escaped characters
if (nextToken.kind !== TokenKind.ESCAPE) {
ArrayPrototypePush(literals, word)
}
nextToken = this.#peek(false)
}
return ArrayPrototypeJoin(literals, '')
}
#countLeadingSpacesInCurrentChunk (chunk) {
// Count the number of whitespace tokens in the chunk, starting from the first token
let whitespaceCount = 0
while (chunk?.[whitespaceCount]?.kind === TokenKind.WHITESPACE) {
whitespaceCount++
}
return whitespaceCount
}
#addDiagnosticsToLastTestPoint (currentNode) {
const { length, [length - 1]: lastTestPoint } = this.#bufferedTestPoints
// Diagnostic nodes are only added to Test points of the same nesting level
if (lastTestPoint && lastTestPoint.nesting === currentNode.nesting) {
lastTestPoint.node.time = this.#lastTestPointDetails.duration
// TODO(@manekinekko): figure out where to put the other diagnostic properties
// See https://github.com/nodejs/node/pull/44952
lastTestPoint.node.diagnostics = lastTestPoint.node.diagnostics || []
ArrayPrototypeForEach(currentNode.node.diagnostics, (diagnostic) => {
// Avoid adding empty diagnostics
if (diagnostic) {
ArrayPrototypePush(lastTestPoint.node.diagnostics, diagnostic)
}
})
this.#bufferedTestPoints = []
}
return lastTestPoint
}
#flushBufferedTestPointNode (shouldClearBuffer = true) {
if (this.#bufferedTestPoints.length > 0) {
this.#emit(this.#bufferedTestPoints[0])
if (shouldClearBuffer) {
this.#bufferedTestPoints = []
}
}
}
#addCommentsToCurrentNode (currentNode) {
if (this.#bufferedComments.length > 0) {
currentNode.comments = ArrayPrototypeMap(
this.#bufferedComments,
(c) => c.node.comment
)
this.#bufferedComments = []
}
return currentNode
}
#flushBufferedComments (shouldClearBuffer = true) {
if (this.#bufferedComments.length > 0) {
ArrayPrototypeForEach(this.#bufferedComments, (node) => {
this.#emit(node)
})
if (shouldClearBuffer) {
this.#bufferedComments = []
}
}
}
#getCurrentIndentationLevel (chunk) {
const whitespaceCount = this.#countLeadingSpacesInCurrentChunk(chunk)
return (whitespaceCount / this.#kSubtestBlockIndentationFactor) | 0
}
#emit (node) {
if (node.kind !== TokenKind.EOF) {
ArrayPrototypePush(this.#flatAST, node)
this.push({
__proto__: null,
...node
})
}
}
#emitOrBufferCurrentNode (currentNode) {
currentNode = {
...currentNode,
nesting: this.#subTestNestingLevel,
lexeme: this.#currentChunkAsString
}
switch (currentNode.kind) {
// Emit these nodes
case TokenKind.UNKNOWN:
if (!currentNode.node.value) {
// Ignore unrecognized and empty nodes
break
}
// falls through
case TokenKind.TAP_PLAN:
case TokenKind.TAP_PRAGMA:
case TokenKind.TAP_VERSION:
case TokenKind.TAP_BAIL_OUT:
case TokenKind.TAP_SUBTEST_POINT:
// Check if we have a buffered test point, and if so, emit it
this.#flushBufferedTestPointNode()
// If we have buffered comments, add them to the current node
currentNode = this.#addCommentsToCurrentNode(currentNode)
// Emit the current node
this.#emit(currentNode)
break
// By default, we buffer the next test point node in case we have a diagnostic
// to add to it in the next iteration
// Note: in case we hit and EOF, we flush the comments buffer (see _flush())
case TokenKind.TAP_TEST_POINT:
// In case of an already buffered test point, we flush it and buffer the current one
// Because diagnostic nodes are only added to the last processed test point
this.#flushBufferedTestPointNode()
// Buffer this node (and also add any pending comments to it)
ArrayPrototypePush(
this.#bufferedTestPoints,
this.#addCommentsToCurrentNode(currentNode)
)
break
// Keep buffering comments until we hit a non-comment node, then add them to the that node
// Note: in case we hit and EOF, we flush the comments buffer (see _flush())
case TokenKind.COMMENT:
ArrayPrototypePush(this.#bufferedComments, currentNode)
break
// Diagnostic nodes are added to Test points of the same nesting level
case TokenKind.TAP_YAML_END:
// Emit either the last updated test point (w/ diagnostics) or the current diagnostics node alone
this.#emit(
this.#addDiagnosticsToLastTestPoint(currentNode) || currentNode
)
break
// In case we hit an EOF, we emit it to indicate the end of the stream
case TokenKind.EOF:
this.#emit(currentNode)
break
}
}
#serializeChunk (chunk) {
return ArrayPrototypeJoin(
ArrayPrototypeMap(
// Exclude NewLine and EOF tokens
ArrayPrototypeFilter(
chunk,
(token) =>
token.kind !== TokenKind.NEWLINE && token.kind !== TokenKind.EOF
),
(token) => token.value
),
''
)
}
// --------------------------------------------------------------------------//
// ------------------------------ Parser rules ------------------------------//
// --------------------------------------------------------------------------//
// TAPDocument := Version Plan Body | Version Body Plan
#TAPDocument (tokenChunks) {
this.#currentChunkAsString = this.#serializeChunk(tokenChunks)
const firstToken = this.#peek(false)
if (firstToken) {
const { kind } = firstToken
switch (kind) {
case TokenKind.TAP:
return this.#Version()
case TokenKind.NUMERIC:
return this.#Plan()
case TokenKind.TAP_TEST_OK:
case TokenKind.TAP_TEST_NOTOK:
return this.#TestPoint()
case TokenKind.COMMENT:
case TokenKind.HASH:
return this.#Comment()
case TokenKind.TAP_PRAGMA:
return this.#Pragma()
case TokenKind.WHITESPACE:
return this.#YAMLBlock()
case TokenKind.LITERAL:
// Check for "Bail out!" literal (case insensitive)
if (
RegExpPrototypeExec(/^Bail\s+out!/i, this.#currentChunkAsString)
) {
return this.#Bailout()
} else if (this.#isYAMLBlock) {
return this.#YAMLBlock()
}
// Read token because error needs the last token details
this.#next(false)
this.#error('Expected a valid token')
break
case TokenKind.EOF:
return firstToken
case TokenKind.NEWLINE:
// Consume and ignore NewLine token
return this.#next(false)
default:
// Read token because error needs the last token details
this.#next(false)
this.#error('Expected a valid token')
}
}
const node = {
kind: TokenKind.UNKNOWN,
node: {
value: this.#currentChunkAsString
}
}
// We make sure the emitted node has the same shape
// both in sync and async parsing (for the stream interface)
return node
}
// ----------------Version----------------
// Version := "TAP version Number\n"
#Version () {
const tapToken = this.#peek()
if (tapToken.kind === TokenKind.TAP) {
this.#next() // Consume the TAP token
} else {
this.#error('Expected "TAP" keyword')
}
const versionToken = this.#peek()
if (versionToken?.kind === TokenKind.TAP_VERSION) {
this.#next() // Consume the version token
} else {
this.#error('Expected "version" keyword')
}
const numberToken = this.#peek()
if (numberToken?.kind === TokenKind.NUMERIC) {
const version = this.#next().value
const node = { kind: TokenKind.TAP_VERSION, node: { version } }
return node
}
this.#error('Expected a version number')
}
// ----------------Plan----------------
// Plan := "1.." (Number) (" # " Reason)? "\n"
#Plan () {
// Even if specs mention plan starts at 1, we need to make sure we read the plan start value
// in case of a missing or invalid plan start value
const planStart = this.#next()
if (planStart.kind !== TokenKind.NUMERIC) {
this.#error('Expected a plan start count')
}
const planToken = this.#next()
if (planToken?.kind !== TokenKind.TAP_PLAN) {
this.#error('Expected ".." symbol')
}
const planEnd = this.#next()
if (planEnd?.kind !== TokenKind.NUMERIC) {
this.#error('Expected a plan end count')
}
const plan = {
start: planStart.value,
end: planEnd.value
}
// Read optional reason
const hashToken = this.#peek()
if (hashToken) {
if (hashToken.kind === TokenKind.HASH) {
this.#next() // skip hash
plan.reason = StringPrototypeTrim(this.#readNextLiterals())
} else if (hashToken.kind === TokenKind.LITERAL) {
this.#error('Expected "#" symbol before a reason')
}
}
const node = {
kind: TokenKind.TAP_PLAN,
node: plan
}
return node
}
// ----------------TestPoint----------------
// TestPoint := ("not ")? "ok" (" " Number)? ((" -")? (" " Description) )? (" " Directive)? "\n" (YAMLBlock)?
// Directive := " # " ("todo" | "skip") (" " Reason)?
// YAMLBlock := " ---\n" (YAMLLine)* " ...\n"
// YAMLLine := " " (YAML)* "\n"
// Test Status: ok/not ok (required)
// Test number (recommended)
// Description (recommended, prefixed by " - ")
// Directive (only when necessary)
#TestPoint () {
const notToken = this.#peek()
let isTestFailed = false
if (notToken.kind === TokenKind.TAP_TEST_NOTOK) {
this.#next() // skip "not" token
isTestFailed = true
}
const okToken = this.#next()
if (okToken.kind !== TokenKind.TAP_TEST_OK) {
this.#error('Expected "ok" or "not ok" keyword')
}
// Read optional test number
let numberToken = this.#peek()
if (numberToken && numberToken.kind === TokenKind.NUMERIC) {
numberToken = this.#next().value
} else {
numberToken = '' // Set an empty ID to indicate that the test hasn't provider an ID
}
const test = {
// Output both failed and passed properties to make it easier for the checker to detect the test status
status: {
fail: isTestFailed,
pass: !isTestFailed,
todo: false,
skip: false
},
id: numberToken,
description: '',
reason: '',
time: 0,
diagnostics: []
}
// Read optional description prefix " - "
const descriptionDashToken = this.#peek()
if (descriptionDashToken && descriptionDashToken.kind === TokenKind.DASH) {
this.#next() // skip dash
}
// Read optional description
if (this.#peek()) {
const description = StringPrototypeTrim(this.#readNextLiterals())
if (description) {
test.description = description
}
}
// Read optional directive and reason
const hashToken = this.#peek()
if (hashToken && hashToken.kind === TokenKind.HASH) {
this.#next() // skip hash
}
let todoOrSkipToken = this.#peek()
if (todoOrSkipToken && todoOrSkipToken.kind === TokenKind.LITERAL) {
if (RegExpPrototypeExec(/todo/i, todoOrSkipToken.value)) {
todoOrSkipToken = 'todo'
this.#next() // skip token
} else if (RegExpPrototypeExec(/skip/i, todoOrSkipToken.value)) {
todoOrSkipToken = 'skip'
this.#next() // skip token
}
}
const reason = StringPrototypeTrim(this.#readNextLiterals())
if (todoOrSkipToken) {
if (reason) {
test.reason = reason
}
test.status.todo = todoOrSkipToken === 'todo'
test.status.skip = todoOrSkipToken === 'skip'
}
const node = {
kind: TokenKind.TAP_TEST_POINT,
node: test
}
return node
}
// ----------------Bailout----------------
// BailOut := "Bail out!" (" " Reason)? "\n"
#Bailout () {
this.#next() // skip "Bail"
this.#next() // skip "out!"
// Read optional reason
const hashToken = this.#peek()
if (hashToken && hashToken.kind === TokenKind.HASH) {
this.#next() // skip hash
}
const reason = StringPrototypeTrim(this.#readNextLiterals())
const node = {
kind: TokenKind.TAP_BAIL_OUT,
node: { bailout: true, reason }
}
return node
}
// ----------------Comment----------------
// Comment := ^ (" ")* "#" [^\n]* "\n"
#Comment () {
const commentToken = this.#next()
if (
commentToken.kind !== TokenKind.COMMENT &&
commentToken.kind !== TokenKind.HASH
) {
this.#error('Expected "#" symbol')
}
const commentContent = this.#peek()
if (commentContent) {
if (RegExpPrototypeExec(/^Subtest:/i, commentContent.value) !== null) {
this.#next() // skip subtest keyword
const name = StringPrototypeTrim(this.#readNextLiterals())
const node = {
kind: TokenKind.TAP_SUBTEST_POINT,
node: {
name
}
}
return node
}
const comment = StringPrototypeTrim(this.#readNextLiterals())
const node = {
kind: TokenKind.COMMENT,
node: { comment }
}
return node
}
// If there is no comment content, then we ignore the current node
}
// ----------------YAMLBlock----------------
// YAMLBlock := " ---\n" (YAMLLine)* " ...\n"
#YAMLBlock () {
const space1 = this.#peek(false)
if (space1 && space1.kind === TokenKind.WHITESPACE) {
this.#next(false) // skip 1st space
}
const space2 = this.#peek(false)
if (space2 && space2.kind === TokenKind.WHITESPACE) {
this.#next(false) // skip 2nd space
}
const yamlBlockSymbol = this.#peek(false)
if (yamlBlockSymbol.kind === TokenKind.WHITESPACE) {
if (this.#isYAMLBlock === false) {
this.#next(false) // skip 3rd space
this.#error('Expected valid YAML indentation (2 spaces)')
}
}
if (yamlBlockSymbol.kind === TokenKind.TAP_YAML_START) {
if (this.#isYAMLBlock) {
// Looks like we have another YAML start block, but we didn't close the previous one
this.#error('Unexpected YAML start marker')
}
this.#isYAMLBlock = true
this.#yamlCurrentIndentationLevel = this.#subTestNestingLevel
this.#lastTestPointDetails = {}
// Consume the YAML start marker
this.#next(false) // skip "---"
// No need to pass this token to the stream interface
return
} else if (yamlBlockSymbol.kind === TokenKind.TAP_YAML_END) {
this.#next(false) // skip "..."
if (!this.#isYAMLBlock) {
// Looks like we have an YAML end block, but we didn't encounter any YAML start marker
this.#error('Unexpected YAML end marker')
}
this.#isYAMLBlock = false
const diagnostics = this.#yamlBlockBuffer
this.#yamlBlockBuffer = [] // Free the buffer for the next YAML block
const node = {
kind: TokenKind.TAP_YAML_END,
node: {
diagnostics
}
}
return node
}
if (this.#isYAMLBlock) {
this.#YAMLLine()
} else {
return {
kind: TokenKind.UNKNOWN,
node: {
value: yamlBlockSymbol.value
}
}
}
}
// ----------------YAMLLine----------------
// YAMLLine := " " (YAML)* "\n"
#YAMLLine () {
const yamlLiteral = this.#readNextLiterals()
const { 0: key, 1: value } = StringPrototypeSplit(yamlLiteral, ':', 2)
// Note that this.#lastTestPointDetails has been cleared when we encounter a YAML start marker
switch (key) {
case 'duration_ms':
this.#lastTestPointDetails.duration = Number(value)
break
// Below are diagnostic properties introduced in https://github.com/nodejs/node/pull/44952
case 'expected':
this.#lastTestPointDetails.expected = Boolean(value)
break
case 'actual':
this.#lastTestPointDetails.actual = Boolean(value)
break
case 'operator':
this.#lastTestPointDetails.operator = String(value)
break
}
ArrayPrototypePush(this.#yamlBlockBuffer, yamlLiteral)
}
// ----------------PRAGMA----------------
// Pragma := "pragma " [+-] PragmaKey "\n"
// PragmaKey := ([a-zA-Z0-9_-])+
// TODO(@manekinekko): pragmas are parsed but not used yet! TapChecker() should take care of that.
#Pragma () {
const pragmaToken = this.#next()
if (pragmaToken.kind !== TokenKind.TAP_PRAGMA) {
this.#error('Expected "pragma" keyword')
}
const pragmas = {}
let nextToken = this.#peek()
while (
nextToken &&
ArrayPrototypeIncludes(
[TokenKind.NEWLINE, TokenKind.EOF, TokenKind.EOL],
nextToken.kind
) === false
) {
let isEnabled = true
const pragmaKeySign = this.#next()
if (pragmaKeySign.kind === TokenKind.PLUS) {
isEnabled = true
} else if (pragmaKeySign.kind === TokenKind.DASH) {
isEnabled = false
} else {
this.#error('Expected "+" or "-" before pragma keys')
}
const pragmaKeyToken = this.#peek()
if (pragmaKeyToken.kind !== TokenKind.LITERAL) {
this.#error('Expected pragma key')
}
let pragmaKey = this.#next().value
// In some cases, pragma key can be followed by a comma separator,
// so we need to remove it
pragmaKey = StringPrototypeReplaceAll(pragmaKey, ',', '')
pragmas[pragmaKey] = isEnabled
nextToken = this.#peek()
}
const node = {
kind: TokenKind.TAP_PRAGMA,
node: {
pragmas
}
}
return node
}
}
module.exports = { TapParser }