|
|
|
|
@@ -1,9 +1,9 @@
|
|
|
|
|
// =============================================================================
|
|
|
|
|
// DSL
|
|
|
|
|
|
|
|
|
|
type TestTree = TestGroup | Test;
|
|
|
|
|
type TestGroup = { name: string, children: TestTree[] };
|
|
|
|
|
type Test = { name: string, test: (TestController) => void };
|
|
|
|
|
type TestTree = { name: string } & (TestGroup | Test);
|
|
|
|
|
type TestGroup = { children: TestTree[] };
|
|
|
|
|
type Test = { test: (TestController) => void };
|
|
|
|
|
|
|
|
|
|
let TESTS: ({ name: string } & TestGroup)[] = [];
|
|
|
|
|
|
|
|
|
|
@@ -34,26 +34,17 @@ function checkDuplicates(parent: string, names: { name: string }[]) {
|
|
|
|
|
|
|
|
|
|
class FailNowSentinel {}
|
|
|
|
|
|
|
|
|
|
export type Journal = ({ method: "fail" } | { method: "log", message: string })[];
|
|
|
|
|
function formatJournal(journal: Journal): string {
|
|
|
|
|
return journal
|
|
|
|
|
.filter((e) => e.method === "log")
|
|
|
|
|
.map((e) => e.message)
|
|
|
|
|
.join("\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class TestController {
|
|
|
|
|
journal: Journal;
|
|
|
|
|
class TestController {
|
|
|
|
|
#logBuf: string[];
|
|
|
|
|
#failed: boolean;
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
this.journal = [];
|
|
|
|
|
this.#logBuf = [];
|
|
|
|
|
this.#failed = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fail() {
|
|
|
|
|
this.#failed = true;
|
|
|
|
|
this.journal.push({ method: "fail" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
failed(): boolean {
|
|
|
|
|
@@ -66,7 +57,7 @@ export class TestController {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log(message: string) {
|
|
|
|
|
this.journal.push({ method: "log", message });
|
|
|
|
|
this.#logBuf.push(message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
error(message: string) {
|
|
|
|
|
@@ -78,6 +69,10 @@ export class TestController {
|
|
|
|
|
this.log(message);
|
|
|
|
|
this.failNow();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getLog(): string {
|
|
|
|
|
return this.#logBuf.join("\n");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// =============================================================================
|
|
|
|
|
@@ -85,34 +80,35 @@ export class TestController {
|
|
|
|
|
|
|
|
|
|
export interface TestResult {
|
|
|
|
|
success: boolean;
|
|
|
|
|
journal: Journal;
|
|
|
|
|
output: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function run(reporter: Reporter) {
|
|
|
|
|
reporter.register(TESTS);
|
|
|
|
|
for (const suite of TESTS) {
|
|
|
|
|
for (const c of suite.children) runTests(reporter, [suite.name], c);
|
|
|
|
|
}
|
|
|
|
|
reporter.finalize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function runTests(reporter: Reporter, parents: string[], node: TestTree) {
|
|
|
|
|
const path = [...parents, node.name];
|
|
|
|
|
if ("children" in node) {
|
|
|
|
|
for (const c of node.children) runTests(reporter, path, c);
|
|
|
|
|
function runTests(reporter: Reporter, parents: string[], tree: TestTree) {
|
|
|
|
|
const path = [...parents, tree.name];
|
|
|
|
|
if ("children" in tree) {
|
|
|
|
|
for (const c of tree.children) runTests(reporter, path, c);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let controller = new TestController();
|
|
|
|
|
let excStr: string;
|
|
|
|
|
try {
|
|
|
|
|
node.test(controller);
|
|
|
|
|
tree.test(controller);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (!(e instanceof FailNowSentinel)) {
|
|
|
|
|
controller.fail();
|
|
|
|
|
excStr = extractExceptionString(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (excStr !== undefined) controller.error(excStr);
|
|
|
|
|
reporter.update(path, { success: !controller.failed(), journal: controller.journal });
|
|
|
|
|
const log = controller.getLog();
|
|
|
|
|
const output = (log && excStr) ? `${log}\n${excStr}` : `${log}${excStr ?? ''}`;
|
|
|
|
|
reporter.update(path, { success: !controller.failed(), output });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function run(reporter: Reporter) {
|
|
|
|
|
for (const suite of TESTS) {
|
|
|
|
|
for (const c of suite.children) runTests(reporter, [suite.name], c);
|
|
|
|
|
}
|
|
|
|
|
reporter.finalize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function extractExceptionString(e: any): string {
|
|
|
|
|
@@ -130,7 +126,6 @@ function extractExceptionString(e: any): string {
|
|
|
|
|
// Reporting
|
|
|
|
|
|
|
|
|
|
export interface Reporter {
|
|
|
|
|
register(suites: TestGroup[]): void
|
|
|
|
|
update(path: string[], result: TestResult): void;
|
|
|
|
|
finalize(): void;
|
|
|
|
|
}
|
|
|
|
|
@@ -154,8 +149,6 @@ export class StreamReporter implements Reporter {
|
|
|
|
|
this.counts = { successes: 0, failures: 0 };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
register(suites: TestGroup[]) {}
|
|
|
|
|
|
|
|
|
|
update(path: string[], result: TestResult) {
|
|
|
|
|
if (path.length === 0) throw new RangeError("path is empty");
|
|
|
|
|
const pathStr = path.join(SEP);
|
|
|
|
|
@@ -199,11 +192,10 @@ export class StreamReporter implements Reporter {
|
|
|
|
|
|
|
|
|
|
#writeOutput(test: { name: string } & TestResult, prefix: string, nested: boolean) {
|
|
|
|
|
let output = "";
|
|
|
|
|
let logOutput = formatJournal(test.journal);
|
|
|
|
|
if (logOutput) {
|
|
|
|
|
const lines = logOutput.split("\n");
|
|
|
|
|
if (test.output) {
|
|
|
|
|
const lines = test.output.split("\n");
|
|
|
|
|
if (lines.length <= 1) {
|
|
|
|
|
output = `: ${logOutput}`;
|
|
|
|
|
output = `: ${test.output}`;
|
|
|
|
|
} else {
|
|
|
|
|
const padding = nested ? " " : " ";
|
|
|
|
|
output = ":\n" + lines.map((line) => padding + line).join("\n");
|
|
|
|
|
@@ -214,8 +206,6 @@ export class StreamReporter implements Reporter {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class DOMReporter implements Reporter {
|
|
|
|
|
register(suites: TestGroup[]) {}
|
|
|
|
|
|
|
|
|
|
update(path: string[], result: TestResult) {
|
|
|
|
|
if (path.length === 0) throw new RangeError("path is empty");
|
|
|
|
|
const counter = document.getElementById(result.success ? "successes" : "failures");
|
|
|
|
|
@@ -247,10 +237,9 @@ export class DOMReporter implements Reporter {
|
|
|
|
|
}
|
|
|
|
|
const p = document.createElement("p");
|
|
|
|
|
p.classList.add("test-desc");
|
|
|
|
|
const logOutput = formatJournal(result.journal);
|
|
|
|
|
if (logOutput) {
|
|
|
|
|
if (result.output) {
|
|
|
|
|
const pre = document.createElement("pre");
|
|
|
|
|
pre.appendChild(document.createTextNode(logOutput));
|
|
|
|
|
pre.appendChild(document.createTextNode(result.output));
|
|
|
|
|
p.appendChild(pre);
|
|
|
|
|
} else {
|
|
|
|
|
p.classList.add("italic");
|
|
|
|
|
@@ -261,32 +250,3 @@ export class DOMReporter implements Reporter {
|
|
|
|
|
|
|
|
|
|
finalize() {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface GoNode {
|
|
|
|
|
name: string;
|
|
|
|
|
subtests?: GoNode[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Used to display results via `go test`, via some glue code from the Go side.
|
|
|
|
|
export class GoTestReporter implements Reporter {
|
|
|
|
|
// Convert a test tree into the one expected by the Go code.
|
|
|
|
|
static serialize(node: TestTree): GoNode {
|
|
|
|
|
if (!("children" in node)) return { name: node.name };
|
|
|
|
|
return {
|
|
|
|
|
name: node.name,
|
|
|
|
|
subtests: node.children.map(GoTestReporter.serialize),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
register(suites: TestGroup[]) {
|
|
|
|
|
console.log(JSON.stringify(suites.map(GoTestReporter.serialize)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
update(path: string[], result: TestResult) {
|
|
|
|
|
console.log(JSON.stringify({ "path": path, ...result }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
finalize() {
|
|
|
|
|
console.log("null");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|