@plugjs/expect5
Version:
Unit Testing for the PlugJS Build System ========================================
253 lines (252 loc) • 9.99 kB
JavaScript
// test.ts
import { AssertionError } from "node:assert";
import { BuildFailure } from "@plugjs/plug";
import { assert } from "@plugjs/plug/asserts";
import { $blu, $grn, $gry, $ms, $p, $plur, $red, $wht, $ylw, ERROR, NOTICE, WARN, githubAnnotation, log } from "@plugjs/plug/logging";
import { dirnameFromUrl, filenameFromUrl } from "@plugjs/plug/paths";
import { Suite, skip } from "./execution/executable.mjs";
import { runSuite } from "./execution/executor.mjs";
import * as setup from "./execution/setup.mjs";
import { diff } from "./expectation/diff.mjs";
import { expect } from "./expectation/expect.mjs";
import { printDiff } from "./expectation/print.mjs";
import { ExpectationError, stringifyValue } from "./expectation/types.mjs";
var _pending = "\u22EF";
var _success = "\u2714";
var _failure = "\u2718";
var _details = "\u2192";
var Test = class {
constructor(_options = {}) {
this._options = _options;
}
async pipe(files, context) {
assert(files.length, "No files available for running tests");
const {
globals = true,
genericErrorDiffs = true,
maxFailures = Number.POSITIVE_INFINITY,
summary = false
} = this._options;
if (globals) {
const anyGlobal = globalThis;
anyGlobal["describe"] = setup.describe;
anyGlobal["fdescribe"] = setup.fdescribe;
anyGlobal["xdescribe"] = setup.xdescribe;
anyGlobal["it"] = setup.it;
anyGlobal["fit"] = setup.fit;
anyGlobal["xit"] = setup.xit;
anyGlobal["afterAll"] = setup.afterAll;
anyGlobal["afterEach"] = setup.afterEach;
anyGlobal["beforeAll"] = setup.beforeAll;
anyGlobal["beforeEach"] = setup.beforeEach;
anyGlobal["xafterAll"] = setup.xafterAll;
anyGlobal["xafterEach"] = setup.xafterEach;
anyGlobal["xbeforeAll"] = setup.xbeforeAll;
anyGlobal["xbeforeEach"] = setup.xbeforeEach;
anyGlobal["skip"] = skip;
anyGlobal["expect"] = expect;
anyGlobal["log"] = log;
anyGlobal["dirnameFromUrl"] = dirnameFromUrl;
anyGlobal["filenameFromUrl"] = filenameFromUrl;
}
const suite = new Suite(void 0, "", async () => {
let count = 0;
for (const file of files.absolutePaths()) {
log.debug("Importing", $p(file), "in suite", $gry(`(${++count}/${files.length})`));
await import(file);
}
});
await suite.setup();
const snum = suite.specs;
const fnum = files.length;
const smsg = $plur(snum, "spec", "specs");
const fmsg = $plur(fnum, "file", "files");
assert(snum, "No specs configured by test files");
const execution = runSuite(suite);
execution.on("suite:start", (current) => {
if (current.parent === suite) {
if (suite.flag !== "only") context.log.notice("");
context.log.enter(NOTICE, `${$wht(current.name)}`);
context.log.notice("");
} else if (current.parent) {
context.log.enter(NOTICE, `${$blu(_details)} ${$wht(current.name)}`);
} else {
context.log.notice(`Running ${smsg} from ${fmsg}`);
if (suite.flag === "only") context.log.notice("");
}
});
execution.on("suite:done", (current) => {
if (current.parent) context.log.leave();
});
execution.on("spec:start", (spec) => {
context.log.enter(NOTICE, `${$blu(_pending)} ${spec.name}`);
});
execution.on("spec:skip", (spec, ms) => {
if (suite.flag === "only") return context.log.leave();
context.log.leave(WARN, `${$ylw(_pending)} ${spec.name} ${$ms(ms, $ylw("skipped"))}`);
});
execution.on("spec:pass", (spec, ms, slow) => {
if (slow) {
context.log.leave(WARN, `${$ylw(_success)} ${spec.name} ${$ms(ms, $ylw("slow"))}`);
} else {
context.log.leave(NOTICE, `${$grn(_success)} ${spec.name} ${$ms(ms)}`);
}
});
execution.on("spec:fail", (spec, ms, { number }) => {
context.log.leave(
ERROR,
`${$red(_failure)} ${spec.name} ${$ms(ms)} ${$gry("[")}${$red("failed")}${$gry("|")}${$red(`${number}`)}${$gry("]")}`
);
});
execution.on("hook:fail", (hook, ms, { number }) => {
context.log.error(`${$red(_failure)} Hook "${hook.name}" ${$ms(ms)} ${$gry("[")}${$red("failed")}${$gry("|")}${$red(`${number}`)}${$gry("]")}`);
});
const { failed, passed, skipped, failures, time, records } = await execution.result;
const limit = Math.min(failures.length, maxFailures);
for (let i = 0; i < limit; i++) {
if (i === 0) context.log.error("");
const { source, error, number } = failures[i];
const names = [""];
for (let p = source.parent; p?.parent; p = p.parent) {
if (p) names.unshift(p.name);
}
const details = names.join(` ${$gry(_details)} `) + $wht(source.name);
context.log.enter(ERROR, `${$gry("[")}${$red(number)}${$gry("]:")} ${details}`);
dumpError(context.log, error, genericErrorDiffs);
context.log.leave();
}
if (summary) {
context.log.notice("");
context.log.notice($wht("Test execution summary:"));
records.forEach((record) => dumpRecords(context.log, record));
}
const totals = [`${passed} ${$gry("passed")}`];
if (skipped) totals.push(`${skipped} ${$gry("skipped")}`);
if (failed) totals.push(`${failed} ${$gry("failed")}`);
if (failures.length) totals.push(`${failures.length} ${$gry("total failures")}`);
const epilogue = `${$gry("(")}${totals.join($gry(", "))}${$gry(")")}`;
const message = `Ran ${smsg} from ${fmsg} ${epilogue} ${$ms(time)}`;
if (failures.length) {
context.log.error(message);
throw new BuildFailure();
} else if (suite.flag === "only") {
context.log.error("");
context.log.error(message);
throw new BuildFailure('Suite running in focus ("only") mode');
} else if (skipped) {
context.log.warn("");
context.log.warn(message);
} else {
context.log.notice("");
context.log.notice(message);
}
}
};
function dumpRecords(log2, record) {
if (record.type === "suite") {
log2.enter(NOTICE, `${$wht(record.name)}`);
for (const r of record.records) dumpRecords(log2, r);
log2.leave();
} else if (record.type === "spec") {
switch (record.result) {
case "passed":
if (record.slow) {
log2.notice(`${$ylw(_success)} ${record.name} ${$ms(record.ms, $ylw("slow"))}`);
} else {
log2.notice(`${$grn(_success)} ${record.name} ${$ms(record.ms)}`);
}
break;
case "skipped":
log2.notice(`${$ylw(_pending)} ${record.name} ${$ms(record.ms, $ylw("skipped"))}`);
break;
case "failed":
log2.notice(
`${$red(_failure)} ${record.name} ${$ms(record.ms)} ${$gry("[")}${$red("failed")}${$gry("|")}${$red(`${record.failure}`)}${$gry("]")}`
);
break;
}
} else if (record.type === "hook") {
log2.error(`${$red(_failure)} Hook "${record.name}" ${$gry("[")}${$red("failed")}${$gry("|")}${$red(`${record.failure}`)}${$gry("]")}`);
}
}
function dumpError(log2, error, genericErrorDiffs) {
if (error instanceof ExpectationError) {
log2.enter(ERROR, `${$gry("Expectation Error:")} ${$red(error.message)}`);
githubAnnotation({ type: "error", title: "Expectation Error" }, error.message);
try {
dumpProps(log2, 17, error);
dumpStack(log2, error);
if (error.diff) printDiff(log2, error.diff);
} finally {
log2.error("");
log2.leave();
}
} else if (error instanceof AssertionError) {
const [message = "Unknown Error", ...lines] = error.message.split("\n");
log2.enter(ERROR, `${$gry("Assertion Error:")} ${$red(message)}`);
githubAnnotation({ type: "error", title: "Assertion Error" }, message);
try {
dumpProps(log2, 15, error);
dumpStack(log2, error);
if (genericErrorDiffs) {
if (!error.generatedMessage) for (const line of lines) log2.error(" ", line);
printDiff(log2, diff(error.actual, error.expected));
} else {
while (lines.length && !lines[0]) lines.shift();
for (const line of lines) log2.error(" ", line);
}
} finally {
log2.error("");
log2.leave();
}
} else if (error instanceof Error) {
const message = error.message || (error instanceof BuildFailure ? "Build Failure" : "Unknown Error");
const string = Object.getPrototypeOf(error)?.constructor?.name || "Error";
const type = string === "AssertionError" ? `${$gry("Assertion Error")}: ` : string === "Error" ? "" : `${$gry(string)}: `;
log2.enter(ERROR, `${type}${$red(message)}`);
githubAnnotation({ type: "error", title: string }, message);
try {
dumpProps(log2, type.length, error);
dumpStack(log2, error);
if (genericErrorDiffs && ("actual" in error || "expected" in error)) {
printDiff(log2, diff(error.actual, error.expected));
}
} finally {
log2.error("");
log2.leave();
}
} else {
log2.error($gry("Uknown error:"), error);
}
}
function dumpProps(log2, pad, error) {
Object.keys(error).filter((k) => ![
"diff",
// expectations error,
"actual",
// assertion error, chai
"expected",
// assertion error, chai,
"generatedMessage",
// assertion error,
"message",
// error
"showDiff",
// chai
"stack"
// error
].includes(k)).filter((k) => !(error[k] === null)).filter((k) => !(error[k] === void 0)).forEach((k) => {
const value = error[k];
if (k === "code" && value === "ERR_ASSERTION") return;
const details = typeof value === "string" ? value : stringifyValue(value);
log2.error($gry(`${k}:`.padStart(pad - 1)), $ylw(details));
});
}
function dumpStack(log2, error) {
if (!error.stack) return log2.error("<no stack trace>");
error.stack.split("\n").filter((line) => line.match(/^\s+at\s+/)).map((line) => line.trim()).forEach((line) => log2.error(line));
}
export {
Test
};
//# sourceMappingURL=test.mjs.map