UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

306 lines (261 loc) 8.55 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/test_runner/reporter/tap.js import { inspectWithNoCustomRetry } from "nstdlib/lib/internal/errors"; import { isError, kEmptyObject } from "nstdlib/lib/internal/util"; import { getCoverageReport } from "nstdlib/lib/internal/test_runner/utils"; import * as __hoisted_internal_test_runner_test__ from "nstdlib/lib/internal/test_runner/test"; const kDefaultIndent = " "; // 4 spaces const kFrameStartRegExp = /^ {4}at /; const kLineBreakRegExp = /\n|\r\n/; const kDefaultTAPVersion = 13; const inspectOptions = { __proto__: null, colors: false, breakLength: Infinity, }; let testModule; // Lazy loaded due to circular dependency. function lazyLoadTest() { testModule ??= __hoisted_internal_test_runner_test__; return testModule; } async function* tapReporter(source) { yield `TAP version ${kDefaultTAPVersion}\n`; for await (const { type, data } of source) { switch (type) { case "test:fail": { yield reportTest( data.nesting, data.testNumber, "not ok", data.name, data.skip, data.todo, ); const location = data.file ? `${data.file}:${data.line}:${data.column}` : null; yield reportDetails(data.nesting, data.details, location); break; } case "test:pass": yield reportTest( data.nesting, data.testNumber, "ok", data.name, data.skip, data.todo, ); yield reportDetails(data.nesting, data.details, null); break; case "test:plan": yield `${indent(data.nesting)}1..${data.count}\n`; break; case "test:start": yield `${indent(data.nesting)}# Subtest: ${tapEscape(data.name)}\n`; break; case "test:stderr": case "test:stdout": { const lines = RegExp.prototype[Symbol.split].call( kLineBreakRegExp, data.message, ); for (let i = 0; i < lines.length; i++) { if (lines[i].length === 0) continue; yield `# ${tapEscape(lines[i])}\n`; } break; } case "test:diagnostic": yield `${indent(data.nesting)}# ${tapEscape(data.message)}\n`; break; case "test:coverage": yield getCoverageReport( indent(data.nesting), data.summary, "# ", "", true, ); break; } } } function reportTest(nesting, testNumber, status, name, skip, todo) { let line = `${indent(nesting)}${status} ${testNumber}`; if (name) { line += ` ${tapEscape(`- ${name}`)}`; } if (skip !== undefined) { line += ` # SKIP${typeof skip === "string" && skip.length ? ` ${tapEscape(skip)}` : ""}`; } else if (todo !== undefined) { line += ` # TODO${typeof todo === "string" && todo.length ? ` ${tapEscape(todo)}` : ""}`; } line += "\n"; return line; } function reportDetails(nesting, data = kEmptyObject, location) { const { error, duration_ms } = data; const _indent = indent(nesting); let details = `${_indent} ---\n`; details += jsToYaml(_indent, "duration_ms", duration_ms); details += jsToYaml(_indent, "type", data.type); if (location) { details += jsToYaml(_indent, "location", location); } details += jsToYaml(_indent, null, error, new Set()); details += `${_indent} ...\n`; return details; } const memo = new Map(); function indent(nesting) { let value = memo.get(nesting); if (value === undefined) { value = String.prototype.repeat.call(kDefaultIndent, nesting); memo.set(nesting, value); } return value; } // In certain places, # and \ need to be escaped as \# and \\. function tapEscape(input) { let result = String.prototype.replaceAll.call(input, "\b", "\\b"); result = String.prototype.replaceAll.call(result, "\f", "\\f"); result = String.prototype.replaceAll.call(result, "\t", "\\t"); result = String.prototype.replaceAll.call(result, "\n", "\\n"); result = String.prototype.replaceAll.call(result, "\r", "\\r"); result = String.prototype.replaceAll.call(result, "\v", "\\v"); result = String.prototype.replaceAll.call(result, "\\", "\\\\"); result = String.prototype.replaceAll.call(result, "#", "\\#"); return result; } function jsToYaml(indent, name, value, seen) { if (value === undefined) { return ""; } const prefix = `${indent} ${name}:`; if (value === null) { return `${prefix} ~\n`; } if (typeof value !== "object") { if (typeof value !== "string") { return `${prefix} ${inspectWithNoCustomRetry(value, inspectOptions)}\n`; } const lines = RegExp.prototype[Symbol.split].call(kLineBreakRegExp, value); if (lines.length === 1) { return `${prefix} ${inspectWithNoCustomRetry(value, inspectOptions)}\n`; } let str = `${prefix} |-\n`; for (let i = 0; i < lines.length; i++) { str += `${indent} ${lines[i]}\n`; } return str; } seen.add(value); const entries = Object.entries(value); const isErrorObj = isError(value); let propsIndent = indent; let result = ""; if (name != null) { result += prefix; if (require("binding/types").isDate(value)) { // YAML uses the ISO-8601 standard to express dates. result += " " + Date.prototype.toISOString.call(value); } result += "\n"; propsIndent += " "; } for (let i = 0; i < entries.length; i++) { const { 0: key, 1: value } = entries[i]; if (isErrorObj && (key === "cause" || key === "code")) { continue; } if (seen.has(value)) { result += `${propsIndent} ${key}: <Circular>\n`; continue; } result += jsToYaml(propsIndent, key, value, seen); } if (isErrorObj) { const { kUnwrapErrors } = lazyLoadTest(); const { cause, code, failureType, message, expected, actual, operator, stack, name, } = value; let errMsg = message ?? "<unknown error>"; let errName = name; let errStack = stack; let errCode = code; let errExpected = expected; let errActual = actual; let errOperator = operator; let errIsAssertion = isAssertionLike(value); // If the ERR_TEST_FAILURE came from an error provided by user code, // then try to unwrap the original error message and stack. if (code === "ERR_TEST_FAILURE" && kUnwrapErrors.has(failureType)) { errStack = cause?.stack ?? errStack; errCode = cause?.code ?? errCode; errName = cause?.name ?? errName; errMsg = cause?.message ?? errMsg; if (isAssertionLike(cause)) { errExpected = cause.expected; errActual = cause.actual; errOperator = cause.operator ?? errOperator; errIsAssertion = true; } } result += jsToYaml(indent, "error", errMsg, seen); if (errCode) { result += jsToYaml(indent, "code", errCode, seen); } if (errName && errName !== "Error") { result += jsToYaml(indent, "name", errName, seen); } if (errIsAssertion) { // Note that we're deliberately creating shallow copies of the `seen` // set here in order to isolate the discovery of circular references // within the expected and actual properties respectively. result += jsToYaml(indent, "expected", errExpected, new Set(seen)); result += jsToYaml(indent, "actual", errActual, new Set(seen)); if (errOperator) { result += jsToYaml(indent, "operator", errOperator, seen); } } if (typeof errStack === "string") { const frames = []; Array.prototype.forEach.call( RegExp.prototype[Symbol.split].call(kLineBreakRegExp, errStack), (frame) => { const processed = RegExp.prototype[Symbol.replace].call( kFrameStartRegExp, frame, "", ); if (processed.length > 0 && processed.length !== frame.length) { Array.prototype.push.call(frames, processed); } }, ); if (frames.length > 0) { const frameDelimiter = `\n${indent} `; result += `${indent} stack: |-${frameDelimiter}`; result += `${Array.prototype.join.call(frames, frameDelimiter)}\n`; } } } return result; } function isAssertionLike(value) { return ( value && typeof value === "object" && "expected" in value && "actual" in value ); } export default tapReporter;