1
0
forked from rosa/hakurei

7 Commits

View File

@@ -5,12 +5,7 @@ type TestTree = TestGroup | Test;
type TestGroup = { name: string; children: TestTree[] }; type TestGroup = { name: string; children: TestTree[] };
type Test = { name: string; test: (t: TestController) => void }; type Test = { name: string; test: (t: TestController) => void };
// A registrar provides a central location to register test suites.
export class TestRegistrar { export class TestRegistrar {
// Note that, while this is equivalent to a new tree node sans a name, the
// lack of a name provides the illusion of multiple “top-level” suites,
// while still allowing reporters to pick their favorite name—say, “JS
// tests”—were they to need to label all suites together.
#suites: TestGroup[]; #suites: TestGroup[];
constructor() { constructor() {
@@ -49,10 +44,6 @@ export function test(name: string, test: (t: TestController) => void): TestTree
return { name, test }; return { name, test };
} }
// While this function could certainly refine the type to a map instead of
// simply checking for duplicates and discarding that knowledge, these test
// trees are primarily for flooding—that is, iteration—for which an array is
// better suited.
function checkDuplicates(parent: string, names: { name: string }[]) { function checkDuplicates(parent: string, names: { name: string }[]) {
let seen = new Set<string>(); let seen = new Set<string>();
for (const { name } of names) { for (const { name } of names) {
@@ -158,37 +149,11 @@ function extractExceptionString(e: any): string {
// Reporting // Reporting
export interface Reporter { export interface Reporter {
// A notable feature—or flaw, to some—of the DSL is that the tree of tests
// is statically known, which might greatly aid in implementing a reporter.
register(suites: TestGroup[]): void; register(suites: TestGroup[]): void;
// 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; 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; finalize(): void;
} }
// A reporter that diligently reports absolutely nothing. This is essentially
// a way to “undo” the incremental reporting update() provides, getting back the
// underlying result tree; this makes it extremely convenient in some cases like
// testing ourself.
export class NoOpReporter implements Reporter { export class NoOpReporter implements Reporter {
suites: TestGroup[]; suites: TestGroup[];
results: ({ path: string[] } & TestResult)[]; results: ({ path: string[] } & TestResult)[];
@@ -219,7 +184,6 @@ export interface Stream {
const SEP = " "; const SEP = " ";
// A simple reporter that outputs to some stream; suitable for CLIs.
export class StreamReporter implements Reporter { export class StreamReporter implements Reporter {
stream: Stream; stream: Stream;
verbose: boolean; verbose: boolean;
@@ -239,7 +203,6 @@ export class StreamReporter implements Reporter {
return this.#successes.length > 0 && this.#failures.length === 0; return this.#successes.length > 0 && this.#failures.length === 0;
} }
// We don't need the structure for reporting.
register(suites: TestGroup[]) {} register(suites: TestGroup[]) {}
update(path: string[], result: TestResult) { update(path: string[], result: TestResult) {
@@ -248,17 +211,6 @@ export class StreamReporter implements Reporter {
switch (result.state) { switch (result.state) {
case "success": case "success":
this.#successes.push({ path, ...result }); this.#successes.push({ path, ...result });
// NOTE: emojis are used instead of colored Unicode symbols as
// coloring isn't possible through all streams, which would make
// this terminal-specific, and even in terminals and emulators
// thereof, it's very tedious to correctly detect whether one should
// use colors (https://no-color.org, https://bixense.com/clicolors,
// https://force-color.org), ensure reasonable contrast is retained
// on every possible theme (using reverse video is often the only
// way), and 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.
if (this.verbose) this.stream.writeln(`✅️ ${pathStr}`); if (this.verbose) this.stream.writeln(`✅️ ${pathStr}`);
break; break;
case "failure": case "failure":
@@ -286,10 +238,8 @@ export class StreamReporter implements Reporter {
#displaySection(name: string, data: ({ path: string[] } & TestResult)[], ignoreEmpty: boolean = false) { #displaySection(name: string, data: ({ path: string[] } & TestResult)[], ignoreEmpty: boolean = false) {
if (!data.length) return; if (!data.length) return;
// Transform [{ path: ["a", "b", "c"] }, { path: ["a", "b", "d"] }] into // Transform [{ path: ["a", "b", "c"] }, { path: ["a", "b", "d"] }]
// { "a b": ["c", "d"] }. NOTE: intermediate nodes are collapsed as // into { "a b": ["c", "d"] }.
// excessive nesting is difficult to convey clearly in a text-only
// environment.
let pathMap = new Map<string, ({ name: string } & TestResult)[]>(); let pathMap = new Map<string, ({ name: string } & TestResult)[]>();
for (const t of data) { for (const t of data) {
if (t.path.length === 0) throw new RangeError("path is empty"); if (t.path.length === 0) throw new RangeError("path is empty");
@@ -338,14 +288,11 @@ function assertGetElementById(id: string): HTMLElement {
return elem; return elem;
} }
// A reporter that directly translates a tree of results into a tree of
// collapsible elements in the DOM.
export class DOMReporter implements Reporter { export class DOMReporter implements Reporter {
// It is very difficult to implement this using the statically known tree, // It is very difficult to implement this using the statically known tree,
// because Map doesn't handle array keys properly (to store the path), and // because Map doesn't handle array keys properly (to store the path), and
// it's unknown of there's any way to implement it without writing one's own // it's unknown of there's any way to implement it without writing one's own
// data types. Oh well; using the DOM as a data structure might seem hacky // data types.
// but it does have its benefits, apart from encouraging a tagless final.
register(suites: TestGroup[]) {} register(suites: TestGroup[]) {}
update(path: string[], result: TestResult) { update(path: string[], result: TestResult) {
@@ -385,7 +332,6 @@ export class DOMReporter implements Reporter {
switch (result.state) { switch (result.state) {
case "failure": case "failure":
// Only expand failures, to minimize successes and skips.
child.open = true; child.open = true;
child.classList.add("failure"); child.classList.add("failure");
child.classList.remove("skip"); child.classList.remove("skip");
@@ -456,8 +402,6 @@ export class GoTestReporter implements Reporter {
console.log(JSON.stringify({ path, state, logs: result.logs })); console.log(JSON.stringify({ path, state, logs: result.logs }));
} }
// Unnecessary but convenient on the Go side, so that it doesn't have to
// infer this via process exit.
finalize() { finalize() {
console.log("null"); console.log("null");
} }