UNPKG

nstdlib-nightly

Version:

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

555 lines (482 loc) 16 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/test_runner/utils.js import { AsyncResource } from "nstdlib/lib/async_hooks"; import { relative } from "nstdlib/lib/path"; import { createWriteStream } from "nstdlib/lib/fs"; import { pathToFileURL } from "nstdlib/lib/internal/url"; import { createDeferredPromise } from "nstdlib/lib/internal/util"; import { getOptionValue } from "nstdlib/lib/internal/options"; import { green, yellow, red, white, shouldColorize, } from "nstdlib/lib/internal/util/colors"; import { codes as __codes__, kIsNodeError } from "nstdlib/lib/internal/errors"; import { compose } from "nstdlib/lib/stream"; import * as __hoisted_internal_modules_esm_loader__ from "nstdlib/lib/internal/modules/esm/loader"; const { ERR_INVALID_ARG_VALUE, ERR_TEST_FAILURE } = __codes__; const coverageColors = { __proto__: null, high: green, medium: yellow, low: red, }; const kMultipleCallbackInvocations = "multipleCallbackInvocations"; const kRegExpPattern = /^\/(.*)\/([a-z]*)$/; const kPatterns = ["test", "test/**/*", "test-*", "*[.-_]test"]; const kDefaultPattern = `**/{${Array.prototype.join.call(kPatterns, ",")}}.?(c|m)js`; function createDeferredCallback() { let calledCount = 0; const { promise, resolve, reject } = createDeferredPromise(); const cb = (err) => { calledCount++; // If the callback is called a second time, let the user know, but // don't let them know more than once. if (calledCount > 1) { if (calledCount === 2) { throw new ERR_TEST_FAILURE( "callback invoked multiple times", kMultipleCallbackInvocations, ); } return; } if (err) { return reject(err); } resolve(); }; return { __proto__: null, promise, cb }; } function isTestFailureError(err) { return err?.code === "ERR_TEST_FAILURE" && kIsNodeError in err; } function convertStringToRegExp(str, name) { const match = RegExp.prototype.exec.call(kRegExpPattern, str); const pattern = match?.[1] ?? str; const flags = match?.[2] || ""; try { return new RegExp(pattern, flags); } catch (err) { const msg = err?.message; throw new ERR_INVALID_ARG_VALUE( name, str, `is an invalid regular expression.${msg ? ` ${msg}` : ""}`, ); } } const kBuiltinDestinations = new Map([ ["stdout", process.stdout], ["stderr", process.stderr], ]); const kBuiltinReporters = new Map([ ["spec", "internal/test_runner/reporter/spec"], ["dot", "internal/test_runner/reporter/dot"], ["tap", "internal/test_runner/reporter/tap"], ["junit", "internal/test_runner/reporter/junit"], ["lcov", "internal/test_runner/reporter/lcov"], ]); const kDefaultReporter = process.stdout.isTTY ? "spec" : "tap"; const kDefaultDestination = "stdout"; function tryBuiltinReporter(name) { const builtinPath = kBuiltinReporters.get(name); if (builtinPath === undefined) { return; } return require(builtinPath); } function shouldColorizeTestFiles(destinations) { // This function assumes only built-in destinations (stdout/stderr) supports coloring return Array.prototype.some.call(destinations, (_, index) => { const destination = kBuiltinDestinations.get(destinations[index]); return destination && shouldColorize(destination); }); } async function getReportersMap(reporters, destinations) { return Promise.all(reporters, async (name, i) => { const destination = kBuiltinDestinations.get(destinations[i]) ?? createWriteStream(destinations[i]); // Load the test reporter passed to --test-reporter let reporter = tryBuiltinReporter(name); if (reporter === undefined) { let parentURL; try { parentURL = pathToFileURL(process.cwd() + "/").href; } catch { parentURL = "file:///"; } const cascadedLoader = __hoisted_internal_modules_esm_loader__.getOrInitializeCascadedLoader(); reporter = await cascadedLoader.import(name, parentURL, { __proto__: null, }); } if (reporter?.default) { reporter = reporter.default; } if ( reporter?.prototype && Object.getOwnPropertyDescriptor(reporter.prototype, "constructor") ) { reporter = new reporter(); } if (!reporter) { throw new ERR_INVALID_ARG_VALUE( "Reporter", name, "is not a valid reporter", ); } return { __proto__: null, reporter, destination }; }); } const reporterScope = new AsyncResource("TestReporterScope"); let globalTestOptions; function parseCommandLine() { if (globalTestOptions) { return globalTestOptions; } const isTestRunner = getOptionValue("--test"); const coverage = getOptionValue("--experimental-test-coverage"); const forceExit = getOptionValue("--test-force-exit"); const sourceMaps = getOptionValue("--enable-source-maps"); const updateSnapshots = getOptionValue("--test-update-snapshots"); const watch = getOptionValue("--watch"); const isChildProcess = process.env.NODE_TEST_CONTEXT === "child"; const isChildProcessV8 = process.env.NODE_TEST_CONTEXT === "child-v8"; let concurrency; let coverageExcludeGlobs; let coverageIncludeGlobs; let destinations; let only; let reporters; let shard; let testNamePatterns; let testSkipPatterns; let timeout; if (isChildProcessV8) { kBuiltinReporters.set( "v8-serializer", "internal/test_runner/reporter/v8-serializer", ); reporters = ["v8-serializer"]; destinations = [kDefaultDestination]; } else if (isChildProcess) { reporters = ["tap"]; destinations = [kDefaultDestination]; } else { destinations = getOptionValue("--test-reporter-destination"); reporters = getOptionValue("--test-reporter"); if (reporters.length === 0 && destinations.length === 0) { Array.prototype.push.call(reporters, kDefaultReporter); } if (reporters.length === 1 && destinations.length === 0) { Array.prototype.push.call(destinations, kDefaultDestination); } if (destinations.length !== reporters.length) { throw new ERR_INVALID_ARG_VALUE( "--test-reporter", reporters, "must match the number of specified '--test-reporter-destination'", ); } } if (isTestRunner) { timeout = getOptionValue("--test-timeout") || Infinity; concurrency = getOptionValue("--test-concurrency") || true; only = false; testNamePatterns = null; const shardOption = getOptionValue("--test-shard"); if (shardOption) { if (!RegExp.prototype.exec.call(/^\d+\/\d+$/, shardOption)) { throw new ERR_INVALID_ARG_VALUE( "--test-shard", shardOption, "must be in the form of <index>/<total>", ); } const indexAndTotal = String.prototype.split.call(shardOption, "/"); shard = { __proto__: null, index: Number.parseInt(indexAndTotal[0], 10), total: Number.parseInt(indexAndTotal[1], 10), }; } } else { timeout = Infinity; concurrency = 1; const testNamePatternFlag = getOptionValue("--test-name-pattern"); only = getOptionValue("--test-only"); testNamePatterns = testNamePatternFlag?.length > 0 ? Array.prototype.map.call(testNamePatternFlag, (re) => convertStringToRegExp(re, "--test-name-pattern"), ) : null; const testSkipPatternFlag = getOptionValue("--test-skip-pattern"); testSkipPatterns = testSkipPatternFlag?.length > 0 ? Array.prototype.map.call(testSkipPatternFlag, (re) => convertStringToRegExp(re, "--test-skip-pattern"), ) : null; } if (coverage) { coverageExcludeGlobs = getOptionValue("--test-coverage-exclude"); coverageIncludeGlobs = getOptionValue("--test-coverage-include"); } const setup = reporterScope.bind(async (rootReporter) => { const reportersMap = await getReportersMap(reporters, destinations); for (let i = 0; i < reportersMap.length; i++) { const { reporter, destination } = reportersMap[i]; compose(rootReporter, reporter).pipe(destination); } }); globalTestOptions = { __proto__: null, isTestRunner, concurrency, coverage, coverageExcludeGlobs, coverageIncludeGlobs, destinations, forceExit, only, reporters, setup, shard, sourceMaps, testNamePatterns, testSkipPatterns, timeout, updateSnapshots, watch, }; return globalTestOptions; } function countCompletedTest(test, harness = test.root.harness) { if (test.nesting === 0) { harness.counters.topLevel++; } if (test.reportedType === "suite") { harness.counters.suites++; return; } // Check SKIP and TODO tests first, as those should not be counted as // failures. if (test.skipped) { harness.counters.skipped++; } else if (test.isTodo) { harness.counters.todo++; } else if (test.cancelled) { harness.counters.cancelled++; } else if (!test.passed) { harness.counters.failed++; } else { harness.counters.passed++; } harness.counters.all++; } const memo = new Map(); function addTableLine(prefix, width) { const key = `${prefix}-${width}`; let value = memo.get(key); if (value === undefined) { value = `${prefix}${String.prototype.repeat.call("-", width)}\n`; memo.set(key, value); } return value; } const kHorizontalEllipsis = "\u2026"; function truncateStart(string, width) { return string.length > width ? `${kHorizontalEllipsis}${String.prototype.slice.call(string, string.length - width + 1)}` : string; } function truncateEnd(string, width) { return string.length > width ? `${String.prototype.slice.call(string, 0, width - 1)}${kHorizontalEllipsis}` : string; } function formatLinesToRanges(values) { return Array.prototype.map.call( Array.prototype.reduce.call( values, (prev, current, index, array) => { if (index > 0 && current - array[index - 1] === 1) { prev[prev.length - 1][1] = current; } else { prev.push([current]); } return prev; }, [], ), (range) => Array.prototype.join.call(range, "-"), ); } function getUncoveredLines(lines) { return Array.prototype.flatMap.call(lines, (line) => line.count === 0 ? line.line : [], ); } function formatUncoveredLines(lines, table) { if (table) return Array.prototype.join.call(formatLinesToRanges(lines), " "); return Array.prototype.join.call(lines, ", "); } const kColumns = ["line %", "branch %", "funcs %"]; const kColumnsKeys = [ "coveredLinePercent", "coveredBranchPercent", "coveredFunctionPercent", ]; const kSeparator = " | "; function getCoverageReport(pad, summary, symbol, color, table) { const prefix = `${pad}${symbol}`; let report = `${color}${prefix}start of coverage report\n`; let filePadLength; let columnPadLengths = []; let uncoveredLinesPadLength; let tableWidth; if (table) { // Get expected column sizes filePadLength = table && Array.prototype.reduce.call( summary.files, (acc, file) => Math.max(acc, relative(summary.workingDirectory, file.path).length), 0, ); filePadLength = Math.max(filePadLength, "file".length); const fileWidth = filePadLength + 2; columnPadLengths = Array.prototype.map.call(kColumns, (column) => table ? Math.max(column.length, 6) : 0, ); const columnsWidth = Array.prototype.reduce.call( columnPadLengths, (acc, columnPadLength) => acc + columnPadLength + 3, 0, ); uncoveredLinesPadLength = table && Array.prototype.reduce.call( summary.files, (acc, file) => Math.max( acc, formatUncoveredLines(getUncoveredLines(file.lines), table).length, ), 0, ); uncoveredLinesPadLength = Math.max( uncoveredLinesPadLength, "uncovered lines".length, ); const uncoveredLinesWidth = uncoveredLinesPadLength + 2; tableWidth = fileWidth + columnsWidth + uncoveredLinesWidth; // Fit with sensible defaults const availableWidth = (process.stdout.columns || Infinity) - prefix.length; const columnsExtras = tableWidth - availableWidth; if (table && columnsExtras > 0) { // Ensure file name is sufficiently visible const minFilePad = Math.min(8, filePadLength); filePadLength -= Math.floor(columnsExtras * 0.2); filePadLength = Math.max(filePadLength, minFilePad); // Get rest of available space, subtracting margins uncoveredLinesPadLength = Math.max( availableWidth - columnsWidth - (filePadLength + 2) - 2, 1, ); // Update table width tableWidth = availableWidth; } else { uncoveredLinesPadLength = Infinity; } } function getCell(string, width, pad, truncate, coverage) { if (!table) return string; let result = string; if (pad) result = pad(result, width); if (truncate) result = truncate(result, width); if (color && coverage !== undefined) { if (coverage > 90) return `${coverageColors.high}${result}${color}`; if (coverage > 50) return `${coverageColors.medium}${result}${color}`; return `${coverageColors.low}${result}${color}`; } return result; } // Head if (table) report += addTableLine(prefix, tableWidth); report += `${prefix}${getCell("file", filePadLength, StringPrototypePadEnd, truncateEnd)}${kSeparator}` + `${Array.prototype.join.call( Array.prototype.map.call(kColumns, (column, i) => getCell(column, columnPadLengths[i], StringPrototypePadStart), ), kSeparator, )}${kSeparator}` + `${getCell("uncovered lines", uncoveredLinesPadLength, false, truncateEnd)}\n`; if (table) report += addTableLine(prefix, tableWidth); // Body for (let i = 0; i < summary.files.length; ++i) { const file = summary.files[i]; const relativePath = relative(summary.workingDirectory, file.path); let fileCoverage = 0; const coverages = Array.prototype.map.call(kColumnsKeys, (columnKey) => { const percent = file[columnKey]; fileCoverage += percent; return percent; }); fileCoverage /= kColumnsKeys.length; report += `${prefix}${getCell(relativePath, filePadLength, StringPrototypePadEnd, truncateStart, fileCoverage)}${kSeparator}` + `${Array.prototype.join.call( Array.prototype.map.call(coverages, (coverage, j) => getCell( Number.prototype.toFixed.call(coverage, 2), columnPadLengths[j], StringPrototypePadStart, false, coverage, ), ), kSeparator, )}${kSeparator}` + `${getCell(formatUncoveredLines(getUncoveredLines(file.lines), table), uncoveredLinesPadLength, false, truncateEnd)}\n`; } // Foot if (table) report += addTableLine(prefix, tableWidth); report += `${prefix}${getCell("all files", filePadLength, StringPrototypePadEnd, truncateEnd)}${kSeparator}` + `${Array.prototype.join.call( Array.prototype.map.call(kColumnsKeys, (columnKey, j) => getCell( Number.prototype.toFixed.call(summary.totals[columnKey], 2), columnPadLengths[j], StringPrototypePadStart, false, summary.totals[columnKey], ), ), kSeparator, )} |\n`; if (table) report += addTableLine(prefix, tableWidth); report += `${prefix}end of coverage report\n`; if (color) { report += white; } return report; } export { convertStringToRegExp }; export { countCompletedTest }; export { createDeferredCallback }; export { isTestFailureError }; export { kDefaultPattern }; export { parseCommandLine }; export { reporterScope }; export { shouldColorizeTestFiles }; export { getCoverageReport };