nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
555 lines (482 loc) • 16 kB
JavaScript
// 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 };