1
0
forked from rosa/hakurei

cmd/mbf: jstest: add JSON reporter for go test integration

This commit is contained in:
kat
2026-03-29 04:01:48 +11:00
parent abecc481d1
commit 8c08d9401c
3 changed files with 71 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
import "../all_tests.js";
import { GoTestReporter, GLOBAL_REGISTRAR } from "./jstest.js";
GLOBAL_REGISTRAR.run(new GoTestReporter());

View File

@@ -23,6 +23,7 @@ export class TestRegistrar {
} }
run(reporter: Reporter) { run(reporter: Reporter) {
reporter.register(this.#suites);
for (const suite of this.#suites) { for (const suite of this.#suites) {
for (const c of suite.children) runTests(reporter, [suite.name], c); for (const c of suite.children) runTests(reporter, [suite.name], c);
} }
@@ -141,6 +142,9 @@ 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;
// While we could simply call a function with a tree representing all // While we could simply call a function with a tree representing all
// results, which would indeed greatly simplify implementation of reporters, // results, which would indeed greatly simplify implementation of reporters,
// simply registering a path and allowing the reporter to—either implicitly // simply registering a path and allowing the reporter to—either implicitly
@@ -170,14 +174,20 @@ export interface Reporter {
// underlying result tree; this makes it extremely convenient in some cases like // underlying result tree; this makes it extremely convenient in some cases like
// testing ourself. // testing ourself.
export class NoOpReporter implements Reporter { export class NoOpReporter implements Reporter {
suites: TestGroup[];
results: ({ path: string[] } & TestResult)[]; results: ({ path: string[] } & TestResult)[];
finalized: boolean; finalized: boolean;
constructor() { constructor() {
this.suites = [];
this.results = []; this.results = [];
this.finalized = false; this.finalized = false;
} }
register(suites: TestGroup[]) {
this.suites = suites;
}
update(path: string[], result: TestResult) { update(path: string[], result: TestResult) {
this.results.push({ path, ...result }); this.results.push({ path, ...result });
} }
@@ -211,6 +221,9 @@ export class StreamReporter implements Reporter {
return this.counts.successes > 0 && this.counts.failures === 0; return this.counts.successes > 0 && this.counts.failures === 0;
} }
// We don't need the structure for reporting.
register(suites: TestGroup[]) {}
update(path: string[], result: TestResult) { update(path: string[], result: TestResult) {
if (path.length === 0) throw new RangeError("path is empty"); if (path.length === 0) throw new RangeError("path is empty");
const pathStr = path.join(SEP); const pathStr = path.join(SEP);
@@ -300,6 +313,13 @@ function assertGetElementById(id: string): HTMLElement {
// A reporter that directly translates a tree of results into a tree of // A reporter that directly translates a tree of results into a tree of
// collapsible elements in the DOM. // 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,
// 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. 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) { update(path: string[], result: TestResult) {
if (path.length === 0) throw new RangeError("path is empty"); if (path.length === 0) throw new RangeError("path is empty");
const counter = assertGetElementById(result.success ? "successes" : "failures"); const counter = assertGetElementById(result.success ? "successes" : "failures");
@@ -349,3 +369,34 @@ export class DOMReporter implements Reporter {
finalize() {} finalize() {}
} }
interface GoNode {
name: string;
subtests?: GoNode[];
}
// Used to display results via `go test`, via some glue code from the Go side.
// TODO(Ophestra): said glue code has to be written.
export class GoTestReporter implements Reporter {
register(suites: TestGroup[]) {
console.log(JSON.stringify(suites.map(GoTestReporter.serialize)));
}
// Convert a test tree into the one expected by the Go code.
static serialize(node: TestTree): GoNode {
return {
name: node.name,
subtests: "children" in node ? node.children.map(GoTestReporter.serialize) : undefined,
};
}
update(path: string[], result: TestResult) {
console.log(JSON.stringify({ path, ...result }));
}
// Unnecessary but convenient on the Go side, so that it doesn't have to
// infer this via process exit.
finalize() {
console.log("null");
}
}

View File

@@ -46,6 +46,23 @@ suite("cat", [
]); ]);
const reporter = new NoOpReporter(); const reporter = new NoOpReporter();
r.run(reporter); r.run(reporter);
if (reporter.suites.length !== 1) {
t.fatal(`incorrect number of suites registered got=${reporter.suites.length} want=1`);
}
const suite = reporter.suites[0];
if (suite.name !== "explod") {
t.error(`suite name incorrect got='${suite.name}' want='explod'`);
}
if (suite.children.length !== 1) {
t.fatal(`incorrect number of suite children got=${suite.children.length} want=1`);
}
const test_ = suite.children[0];
if (test_.name !== "with yarn") {
t.error(`incorrect test name got='${test_.name}' want='with yarn'`);
}
if ("children" in test_) {
t.error(`expected leaf node, got group of ${test_.children.length} children`);
}
if (!reporter.finalized) t.error(`expected reporter to have been finalized`); if (!reporter.finalized) t.error(`expected reporter to have been finalized`);
if (reporter.results.length !== 1) { if (reporter.results.length !== 1) {
t.fatal(`incorrect result count got=${reporter.results.length} want=1`); t.fatal(`incorrect result count got=${reporter.results.length} want=1`);