1
0
forked from rosa/hakurei
Files
hakurei/cmd/mbf/internal/pkgserver/ui/jstest/jstest.ts

144 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export interface TestResult {
success: boolean;
logs: string[];
}
// =============================================================================
// Reporting
export interface Reporter {
// While we could simply call a function with a tree representing all
// results, which would indeed greatly simplify implementation of reporters,
// simply registering a path and allowing the reporter to—either implicitly
// or explicitly—construct a tree themselves allows for results to be
// *incrementally reported*, instead of a great deal of silence until all
// tests finish.
update(path: string[], result: TestResult): void;
// With just update(), the reporter never knows when all tests have
// completed. The simplest possible use for this is to notify the user, but
// its intent is actually more tailored to the StreamReporter scenario:
// while destructively updated report rendering (as with a tree of DOM nodes
// which are mutated) always displays the results in a structured manner
// matching that of the tests, “rerendering” or otherwise destructively
// updating the rendered output might be infeasible in some paradigms, such
// as command-line applications—all existing implementations of such
// rendering both mess up the scrollback position and necessarily crop out
// some of the data at the bottom (since the top of the tree is forced to be
// at the top of the screen, as one cannot unscroll portably). Explicitly
// signaling to the reporter that no more results will be received permits
// it to simply display live test progress linearly, while building up
// a tree and displaying it once it's known the tree is complete.
finalize(): void;
}
export interface Stream {
writeln(s: string): void;
}
const SEP = " ";
// A simple reporter that outputs to some stream; suitable for CLIs.
export class StreamReporter implements Reporter {
stream: Stream;
verbose: boolean;
#failures: ({ path: string[] } & TestResult)[];
counts: { successes: number, failures: number };
constructor(stream: Stream, verbose: boolean = false) {
this.stream = stream;
this.verbose = verbose;
this.#failures = [];
this.counts = { successes: 0, failures: 0 };
}
update(path: string[], result: TestResult) {
if (path.length === 0) throw new RangeError("path is empty");
const pathStr = path.join(SEP);
if (result.success) {
this.counts.successes++;
// NOTE: emojis are used instead of colored Unicode symbols as
// coloring isn't possible through all streams and detecting if
// colors should be used is very difficult¹. Furthermore, ensuring
// reasonable contrast is retained on every possible theme is
// difficult, with reverse video often being the only way (which
// also has questionable support across terminal emulators), and the
// Unicode characters might be too small to be immediately
// noticeable. Emojis have an upper hand in that they're more common
// than obscure Unicode characters—which also means you're more
// likely to have an emoji font but not a font with those
// symbols—and that they're double-width.
//
// ¹This necessitates checking if the stream is a TTY, checking if
// $TERM is `dumb` when connected to a TTY, checking
// https://no-color.org, https://bixense.com/clicolors, and
// https://force-color.org, checking if setting the
// ENABLE_VIRTUAL_TERMINAL_PROCESSING bit on the TTY works when on
// on Windows, and doing something similar for Cygwin.
if (this.verbose) this.stream.writeln(`✅️ ${pathStr}`);
} else {
this.counts.failures++;
this.stream.writeln(`⚠️ ${pathStr}`);
this.#failures.push({ path, ...result });
}
}
finalize() {
// Transform [{ path: ["a", "b", "c"] }, { path: ["a", "b", "d"] }] into
// { "a b": ["c", "d"] }. NOTE: intermediate nodes are collapsed as
// excessive nesting is difficult to convey clearly in a text-only
// environment.
let pathMap = new Map<string, ({ name: string } & TestResult)[]>();
for (const f of this.#failures) {
if (f.path.length === 0) throw new RangeError("path is empty");
const key = f.path.slice(0, -1).join(SEP);
if (!pathMap.has(key)) pathMap.set(key, []);
pathMap.get(key)!.push({ name: f.path.at(-1)!, ...f });
}
this.stream.writeln("");
this.stream.writeln("FAILURES");
this.stream.writeln("========");
for (const [path, tests] of pathMap) {
if (tests.length === 1) {
this.#writeOutput(tests[0], path ? `${path}${SEP}` : "", false);
} else {
this.stream.writeln(path);
for (const t of tests) this.#writeOutput(t, " - ", true);
}
}
this.stream.writeln("");
const { successes, failures } = this.counts;
this.stream.writeln(`${successes} succeeded, ${failures} failed`);
}
#writeOutput(test: { name: string } & TestResult, prefix: string, nested: boolean) {
let output = "";
if (test.logs.length) {
// Individual logs might span multiple lines, so join them together
// then split it again.
const logStr = test.logs.join("\n");
const lines = logStr.split("\n");
if (lines.length <= 1) {
output = `: ${logStr}`;
} else {
const padding = nested ? " " : " ";
output = ":\n" + lines.map((line) => padding + line).join("\n");
}
}
this.stream.writeln(`${prefix}${test.name}${output}`);
}
}
const r = new StreamReporter({ writeln: console.log }, true);
r.update(["alien", "can walk"], { success: false, logs: ["assertion failed"] });
r.update(["alien", "can speak"], { success: false, logs: ["Uncaught ReferenceError: larynx is not defined"] });
r.update(["alien", "sleep"], { success: true, logs: [] });
r.update(["Tetromino", "generate", "tessellates"], { success: false, logs: ["assertion failed: 1 != 2"] });
r.update(["Tetromino", "solve", "works"], { success: true, logs: [] });
r.update(["discombobulate", "english"], { success: false, logs: ["hippopotomonstrosesquipedaliophobia\npneumonoultramicroscopicsilicovolcanoconiosis", "supercalifragilisticexpialidocious"] });
r.update(["discombobulate", "geography"], { success: false, logs: ["Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu"] });
r.update(["recombobulate"], { success: true, logs: [] });
r.finalize();