1
0
forked from rosa/hakurei

9 Commits

Author SHA1 Message Date
kat
f6e1fbc1b5 TODO: consider writing tests for the test runner. 2026-05-16 00:46:43 +10:00
kat
9e75360f4c TODO: actually write tests lol. 2026-05-16 00:46:43 +10:00
kat
0e901d95ad TODO: display elapsed time
both on a test level and for the whole thing. i think the reporter or
registrar abstractions should deal with all timeouts, and just feed
elapsed time through all the functions: update() gets time for the
specific test, and finalize passes you the total time. this way you
don't need to do the same logic in every reporter, and you also give
a suggestion to reporter writers (i.e.: you in the future) to expose
test durations. actually tbh per-test isn't possible anywhere but in the
executor, especially when taking potential future parallel execution
into account

on the topic of parallelism: per-test is wall clock for that test,
regardless of perceived time, because no other number is useful. whole
thing is wall clock too, not cpu time

remember:
  - use monotonic clocks!! we need elapsed time, not absolute time
  - format them to more readable strings like “15h 12m” instead of
    “54738 seconds”. once things get large we can be less precise

for the go reporter: ask ozy if the go one already measures it. if so
then don't even bother serializing it

for the stream reporter: the live feed should include per-test time in
brackets or something. the final tree should only include timeout for
outliers on the long side (just shove a box plot-esque algo on it), and
if a flag is given print it for all nodes, and if another flag is given
print the n longest tests. the total time should be in the summary line
at the end in brackets à la pytest

for the dom reporter, we do the same as with the stream reporter's
outlier detection, and have a checkbox or button to dynamically
show/hide all timeouts, and another button to toggle a widget of sorts
that shows up right above the result tree which includes the n longest
tests. all these buttons should be on the same line as the summary
(successes/failures/skips). the total time should be included in the
“execution finished” text form the previous commit, i.e. “execution
finished in 15s”

~april+may
2026-05-16 00:46:43 +10:00
kat
da82eaec78 TODO: display text execution progress (see long description)
since the test tree is statically known, we also statically know how
many tests are present. we should hence be using this to provide
a counter, say [1/48], to give a rough estimate as to when tests might
finish. not a time estimate of course, since we can't determine that

nota bene, we can't pass the current test count, and instead need to let
the reporter deal with that, since otherwise we can't easily parallelize
execution in the future. definitely mention this in a comment somewhere
to elaborate on the design

for the go reporter, ask ozy if go has any way to tell it this info.
i doubt it since they don't have a statically known test count. if it
does, then just send the count alongside the tree

for the stream reporter, ignore it entirely; we don't even display
successes by default so the number has nowhere to be attached to

for the dom reporter, put it somewhere in the header, i think alongside
the success/failure/skip count. something like “in progress (4/28)”.
then once finalize() is called change the whole thing to “execution
finished”

~april+may
2026-05-16 00:46:43 +10:00
kat
b63724005c cmd/mbf: jstest: implement skipping from DSL 2026-05-16 00:46:43 +10:00
kat
f28265062d cmd/mbf: jstest: add JSON reporter for go test integration 2026-05-16 00:46:43 +10:00
kat
178748dd75 cmd/mbf: jstest: implement DSL and runner 2026-05-16 00:46:43 +10:00
kat
4d6a30dba1 cmd/mbf: jstest: add DOM reporter 2026-05-16 00:46:43 +10:00
kat
4c9043cb0c cmd/mbf: jstest: add basic CLI reporter 2026-05-16 00:46:43 +10:00

View File

@@ -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");
}