forked from rosa/hakurei
Compare commits
9 Commits
f2acdc6cfc
...
f6e1fbc1b5
| Author | SHA1 | Date | |
|---|---|---|---|
|
f6e1fbc1b5
|
|||
|
9e75360f4c
|
|||
|
0e901d95ad
|
|||
|
da82eaec78
|
|||
|
b63724005c
|
|||
|
f28265062d
|
|||
|
178748dd75
|
|||
|
4d6a30dba1
|
|||
|
4c9043cb0c
|
@@ -5,7 +5,12 @@ type TestTree = TestGroup | Test;
|
||||
type TestGroup = { name: string; children: TestTree[] };
|
||||
type Test = { name: string; test: (t: TestController) => void };
|
||||
|
||||
// A registrar provides a central location to register test suites.
|
||||
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[];
|
||||
|
||||
constructor() {
|
||||
@@ -44,6 +49,10 @@ export function test(name: string, test: (t: TestController) => void): TestTree
|
||||
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 }[]) {
|
||||
let seen = new Set<string>();
|
||||
for (const { name } of names) {
|
||||
@@ -149,11 +158,37 @@ function extractExceptionString(e: any): string {
|
||||
// Reporting
|
||||
|
||||
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;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
suites: TestGroup[];
|
||||
results: ({ path: string[] } & TestResult)[];
|
||||
@@ -184,6 +219,7 @@ export interface Stream {
|
||||
|
||||
const SEP = " ❯ ";
|
||||
|
||||
// A simple reporter that outputs to some stream; suitable for CLIs.
|
||||
export class StreamReporter implements Reporter {
|
||||
stream: Stream;
|
||||
verbose: boolean;
|
||||
@@ -203,6 +239,7 @@ export class StreamReporter implements Reporter {
|
||||
return this.#successes.length > 0 && this.#failures.length === 0;
|
||||
}
|
||||
|
||||
// We don't need the structure for reporting.
|
||||
register(suites: TestGroup[]) {}
|
||||
|
||||
update(path: string[], result: TestResult) {
|
||||
@@ -211,6 +248,17 @@ export class StreamReporter implements Reporter {
|
||||
switch (result.state) {
|
||||
case "success":
|
||||
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}`);
|
||||
break;
|
||||
case "failure":
|
||||
@@ -238,8 +286,10 @@ export class StreamReporter implements Reporter {
|
||||
#displaySection(name: string, data: ({ path: string[] } & TestResult)[], ignoreEmpty: boolean = false) {
|
||||
if (!data.length) return;
|
||||
|
||||
// Transform [{ path: ["a", "b", "c"] }, { path: ["a", "b", "d"] }]
|
||||
// into { "a ❯ b": ["c", "d"] }.
|
||||
// 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 t of data) {
|
||||
if (t.path.length === 0) throw new RangeError("path is empty");
|
||||
@@ -288,11 +338,14 @@ function assertGetElementById(id: string): HTMLElement {
|
||||
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 {
|
||||
// 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
|
||||
// it's unknown of there's any way to implement it without writing one's own
|
||||
// data types.
|
||||
// data types. Oh well; using the DOM as a data structure might seem hacky
|
||||
// but it does have its benefits, apart from encouraging a tagless final.
|
||||
register(suites: TestGroup[]) {}
|
||||
|
||||
update(path: string[], result: TestResult) {
|
||||
@@ -332,6 +385,7 @@ export class DOMReporter implements Reporter {
|
||||
|
||||
switch (result.state) {
|
||||
case "failure":
|
||||
// Only expand failures, to minimize successes and skips.
|
||||
child.open = true;
|
||||
child.classList.add("failure");
|
||||
child.classList.remove("skip");
|
||||
@@ -402,6 +456,8 @@ export class GoTestReporter implements Reporter {
|
||||
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() {
|
||||
console.log("null");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user