From d1343a7074dd5ff1d9afaaf496f450b24c7f35d1 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Tue, 13 Dec 2022 16:51:35 +0200 Subject: [PATCH] feat: parse yaml PR-URL: https://github.com/nodejs/node/pull/45815 Reviewed-By: Colin Ihrig Reviewed-By: Benjamin Gruenbaum Reviewed-By: Antoine du Hamel (cherry picked from commit 232efb06fe8787e9573e298ce7ac293ad23b7684) --- lib/internal/per_context/primordials.js | 4 + lib/internal/test_runner/runner.js | 20 +--- lib/internal/test_runner/tap_stream.js | 7 +- lib/internal/test_runner/test.js | 4 +- lib/internal/test_runner/yaml_parser.js | 121 ++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 22 deletions(-) create mode 100644 lib/internal/test_runner/yaml_parser.js diff --git a/lib/internal/per_context/primordials.js b/lib/internal/per_context/primordials.js index f3c0b24..951bf28 100644 --- a/lib/internal/per_context/primordials.js +++ b/lib/internal/per_context/primordials.js @@ -20,6 +20,7 @@ exports.ArrayPrototypeSome = (arr, fn) => arr.some(fn) exports.ArrayPrototypeSort = (arr, fn) => arr.sort(fn) exports.ArrayPrototypeSplice = (arr, offset, len, ...el) => arr.splice(offset, len, ...el) exports.ArrayPrototypeUnshift = (arr, ...el) => arr.unshift(...el) +exports.Boolean = Boolean exports.Error = Error exports.ErrorCaptureStackTrace = (...args) => Error.captureStackTrace(...args) exports.FunctionPrototype = Function.prototype @@ -28,6 +29,7 @@ exports.FunctionPrototypeCall = (fn, obj, ...args) => fn.call(obj, ...args) exports.MathMax = (...args) => Math.max(...args) exports.Number = Number exports.NumberIsInteger = Number.isInteger +exports.NumberIsNaN = Number.isNaN exports.NumberParseInt = (str, radix) => Number.parseInt(str, radix) exports.NumberMIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER exports.NumberMAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER @@ -56,6 +58,7 @@ exports.SafePromiseRace = (array, mapFn) => Promise.race(mapFn ? array.map(mapFn exports.SafeSet = Set exports.SafeWeakMap = WeakMap exports.SafeWeakSet = WeakSet +exports.String = String exports.StringPrototypeEndsWith = (haystack, needle, index) => haystack.endsWith(needle, index) exports.StringPrototypeIncludes = (str, needle) => str.includes(needle) exports.StringPrototypeMatch = (str, reg) => str.match(reg) @@ -66,6 +69,7 @@ exports.StringPrototypeReplaceAll = replaceAll exports.StringPrototypeStartsWith = (haystack, needle, index) => haystack.startsWith(needle, index) exports.StringPrototypeSlice = (str, ...args) => str.slice(...args) exports.StringPrototypeSplit = (str, search, limit) => str.split(search, limit) +exports.StringPrototypeSubstring = (str, ...args) => str.substring(...args) exports.StringPrototypeToUpperCase = str => str.toUpperCase() exports.StringPrototypeTrim = str => str.trim() exports.Symbol = Symbol diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 33c4fb3..42c83c6 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -1,11 +1,10 @@ -// https://github.com/nodejs/node/blob/fec0fbc333c58e6ebbd2322f5120830fda880eb0/lib/internal/test_runner/runner.js +// https://github.com/nodejs/node/blob/232efb06fe8787e9573e298ce7ac293ad23b7684/lib/internal/test_runner/runner.js 'use strict' const { ArrayFrom, ArrayPrototypeFilter, ArrayPrototypeForEach, ArrayPrototypeIncludes, - ArrayPrototypeJoin, ArrayPrototypePush, ArrayPrototypeSlice, ArrayPrototypeSort, @@ -32,6 +31,7 @@ const { kEmptyObject } = require('#internal/util') const { createTestTree } = require('#internal/test_runner/harness') const { kDefaultIndent, kSubtestsFailed, Test } = require('#internal/test_runner/test') const { TapParser } = require('#internal/test_runner/tap_parser') +const { YAMLToJs } = require('#internal/test_runner/yaml_parser') const { TokenKind } = require('#internal/test_runner/tap_lexer') const { isSupportedFileType, @@ -123,18 +123,6 @@ class FileTest extends Test { #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. @@ -168,7 +156,7 @@ class FileTest extends Test { indent, node.id, node.description, - details(node.diagnostics), + YAMLToJs(node.diagnostics), directive ) } else { @@ -176,7 +164,7 @@ class FileTest extends Test { indent, node.id, node.description, - details(node.diagnostics), + YAMLToJs(node.diagnostics), directive ) } diff --git a/lib/internal/test_runner/tap_stream.js b/lib/internal/test_runner/tap_stream.js index 54b6301..97a4918 100644 --- a/lib/internal/test_runner/tap_stream.js +++ b/lib/internal/test_runner/tap_stream.js @@ -1,4 +1,4 @@ -// https://github.com/nodejs/node/blob/22dc987fde29734c5bcbb7c33da20d184ff61627/lib/internal/test_runner/tap_stream.js +// https://github.com/nodejs/node/blob/232efb06fe8787e9573e298ce7ac293ad23b7684/lib/internal/test_runner/tap_stream.js 'use strict' @@ -86,11 +86,10 @@ class TapStream extends Readable { } #details (indent, data = kEmptyObject) { - const { error, duration, yaml } = data + const { error, duration_ms } = data // eslint-disable-line camelcase let details = `${indent} ---\n` - details += `${yaml || ''}` - details += jsToYaml(indent, 'duration_ms', duration) + details += jsToYaml(indent, 'duration_ms', duration_ms) details += jsToYaml(indent, null, error) details += `${indent} ...\n` this.#tryPush(details) diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 6b356fc..5292da7 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -1,4 +1,4 @@ -// https://github.com/nodejs/node/blob/215c5317d4837287fddb2e3b97872babd53183ac/lib/internal/test_runner/test.js +// https://github.com/nodejs/node/blob/232efb06fe8787e9573e298ce7ac293ad23b7684/lib/internal/test_runner/test.js 'use strict' @@ -668,7 +668,7 @@ class Test extends AsyncResource { this.reportSubtest() } let directive - const details = { __proto__: null, duration: this.#duration() } + const details = { __proto__: null, duration_ms: this.#duration() } if (this.skipped) { directive = this.reporter.getSkip(this.message) diff --git a/lib/internal/test_runner/yaml_parser.js b/lib/internal/test_runner/yaml_parser.js new file mode 100644 index 0000000..2e85db8 --- /dev/null +++ b/lib/internal/test_runner/yaml_parser.js @@ -0,0 +1,121 @@ +// https://github.com/nodejs/node/blob/232efb06fe8787e9573e298ce7ac293ad23b7684/lib/internal/test_runner/yaml_parser.js +'use strict' +const { + codes: { + ERR_TEST_FAILURE + } +} = require('#internal/errors') +const AssertionError = require('assert').AssertionError +const { + ArrayPrototypeJoin, + ArrayPrototypePush, + Error, + Number, + NumberIsNaN, + RegExpPrototypeExec, + StringPrototypeEndsWith, + StringPrototypeRepeat, + StringPrototypeSlice, + StringPrototypeStartsWith, + StringPrototypeSubstring +} = require('#internal/per_context/primordials') + +const kYamlKeyRegex = /^(\s+)?(\w+):(\s)+([>|][-+])?(.*)$/ +const kStackDelimiter = ' at ' + +function reConstructError (parsedYaml) { + if (!('error' in parsedYaml)) { + return parsedYaml + } + const isAssertionError = parsedYaml.code === 'ERR_ASSERTION' || + 'actual' in parsedYaml || 'expected' in parsedYaml || 'operator' in parsedYaml + const isTestFailure = parsedYaml.code === 'ERR_TEST_FAILURE' || 'failureType' in parsedYaml + const stack = parsedYaml.stack ? kStackDelimiter + ArrayPrototypeJoin(parsedYaml.stack, `\n${kStackDelimiter}`) : '' + let error, cause + + if (isAssertionError) { + cause = new AssertionError({ + message: parsedYaml.error, + actual: parsedYaml.actual, + expected: parsedYaml.expected, + operator: parsedYaml.operator + }) + } else { + // eslint-disable-next-line no-restricted-syntax + cause = new Error(parsedYaml.error) + cause.code = parsedYaml.code + } + cause.stack = stack + + if (isTestFailure) { + error = new ERR_TEST_FAILURE(cause, parsedYaml.failureType) + error.stack = stack + } + + parsedYaml.error = error ?? cause + delete parsedYaml.stack + delete parsedYaml.code + delete parsedYaml.failureType + delete parsedYaml.actual + delete parsedYaml.expected + delete parsedYaml.operator + + return parsedYaml +} + +function getYamlValue (value) { + if (StringPrototypeStartsWith(value, "'") && StringPrototypeEndsWith(value, "'")) { + return StringPrototypeSlice(value, 1, -1) + } + if (value === 'true') { + return true + } + if (value === 'false') { + return false + } + if (value !== '') { + const valueAsNumber = Number(value) + return NumberIsNaN(valueAsNumber) ? value : valueAsNumber + } + return value +} + +// This parses the YAML generated by the built-in TAP reporter, +// which is a subset of the full YAML spec. There are some +// YAML features that won't be parsed here. This function should not be exposed publicly. +function YAMLToJs (lines) { + if (lines == null) { + return undefined + } + const result = { __proto__: null } + let isInYamlBlock = false + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + if (isInYamlBlock && !StringPrototypeStartsWith(line, StringPrototypeRepeat(' ', isInYamlBlock.indent))) { + result[isInYamlBlock.key] = isInYamlBlock.key === 'stack' + ? result[isInYamlBlock.key] + : ArrayPrototypeJoin(result[isInYamlBlock.key], '\n') + isInYamlBlock = false + } + if (isInYamlBlock) { + const blockLine = StringPrototypeSubstring(line, isInYamlBlock.indent) + ArrayPrototypePush(result[isInYamlBlock.key], blockLine) + continue + } + const match = RegExpPrototypeExec(kYamlKeyRegex, line) + if (match !== null) { + const { 1: leadingSpaces, 2: key, 4: block, 5: value } = match + if (block) { + isInYamlBlock = { key, indent: (leadingSpaces?.length ?? 0) + 2 } + result[key] = [] + } else { + result[key] = getYamlValue(value) + } + } + } + return reConstructError(result) +} + +module.exports = { + YAMLToJs +}