From 4beca55a54e1b2b18fa5f8c7cb1a5618ab7a6c22 Mon Sep 17 00:00:00 2001 From: Shohruh Ismoilov Date: Sat, 7 Nov 2026 06:32:40 +0500 Subject: [PATCH] fix(run): report Go benchmark and repeated test results (#1868) Quiet Go benchmarks print their measurement without a terminal pass event, so they stayed running and were shown as ignored when the run ended; repeated attempts (-count, -cpu) share a test path and were counted once. The parser now completes measured benchmarks when their package finishes (a failure at a later CPU setting still wins), maps CPU-suffixed and nested benchmark names back to the started test, and counts each new attempt while keeping a failed one failed. Co-authored-by: shohruh-genius <7658451+shohruh-genius@users.noreply.github.com> --- src/features/run/testing/go.test.ts | 55 +++++++++++++++++ src/features/run/testing/go.ts | 92 +++++++++++++++++++++++------ 2 files changed, 128 insertions(+), 19 deletions(-) diff --git a/src/features/run/testing/go.test.ts b/src/features/run/testing/go.test.ts index d5d36157e..41226b5e3 100644 --- a/src/features/run/testing/go.test.ts +++ b/src/features/run/testing/go.test.ts @@ -51,6 +51,61 @@ describe("Go JSON tests", () => { expect(results.roots[0]?.output).toContain("./main.go:3:1: undefined: missing"); }); + it("counts repeated test results once per start and preserves a failed attempt", () => { + const parser = goParser(); + const results = new TestResults(); + const lines = [ + record("p", "run", "TestRepeated"), + record("p", "fail", "TestRepeated"), + record("p", "run", "TestRepeated"), + record("p", "pass", "TestRepeated"), + record("p", "pass", "TestRepeated"), + record("p", "fail"), + ]; + lines.forEach((line) => results.apply(parser.feed(line))); + expect(results.counts()).toMatchObject({ passed: 1, failed: 1, running: 0, total: 2 }); + expect(results.get("p\0TestRepeated")?.own).toBe("failed"); + }); + + it("completes benchmark measurements and explicit bench events in interleaved packages", () => { + const parser = goParser(); + const results = new TestResults(); + const lines = [ + record("one", "run", "BenchmarkQuiet"), + record("two", "run", "BenchmarkLogged"), + record("one", "output", "BenchmarkQuiet", { Output: "BenchmarkQuiet-4 \t 1\t 214 ns/op\n" }), + record("two", "output", "BenchmarkLogged", { Output: " bench_test.go:4: logged\n" }), + record("two", "bench", "BenchmarkLogged-4"), + record("one", "output", undefined, { Output: "BenchmarkQuiet-8 \t 1\t 210 ns/op\n" }), + record("one", "run", "BenchmarkFails"), + record("one", "output", "BenchmarkFails", { Output: "BenchmarkFails-4 \t 1\t 220 ns/op\n" }), + record("one", "fail", "BenchmarkFails-8"), + record("one", "fail"), + record("two", "pass"), + ]; + lines.forEach((line) => results.apply(parser.feed(line))); + expect(results.counts()).toMatchObject({ passed: 2, failed: 1, running: 0, total: 3 }); + expect(results.get("one\0BenchmarkQuiet")?.own).toBe("passed"); + expect(results.get("one\0BenchmarkFails")?.own).toBe("failed"); + expect(results.get("two\0BenchmarkLogged")?.own).toBe("passed"); + }); + + it("maps benchmark measurements without a Test field to the actual nested benchmark name", () => { + const parser = goParser(); + const results = new TestResults(); + const lines = [ + record("p", "run", "BenchmarkNested"), + record("p", "run", "BenchmarkNested/deep-2"), + record("p", "output", undefined, { Output: "BenchmarkNested/deep-2 \t 1\t 214 ns/op\n" }), + record("p", "output", undefined, { Output: "BenchmarkNested/deep-2-4 \t 1\t 210 ns/op\n" }), + record("p", "pass"), + ]; + lines.forEach((line) => results.apply(parser.feed(line))); + expect(results.counts()).toMatchObject({ passed: 1, failed: 0, running: 0, total: 1 }); + expect(results.get("p\0BenchmarkNested")?.children.map((node) => node.name)).toEqual(["deep-2"]); + expect(results.get("p\0BenchmarkNested\0deep-2")?.own).toBe("passed"); + }); + it("ignores malformed metadata and negative durations without throwing", () => { for (const line of [ "{", diff --git a/src/features/run/testing/go.ts b/src/features/run/testing/go.ts index ccfe687e8..e38f6a8f9 100644 --- a/src/features/run/testing/go.ts +++ b/src/features/run/testing/go.ts @@ -9,6 +9,7 @@ const RUN_RE = /^=== (?:RUN|CONT|PAUSE)\s+(\S+)/; const RESULT_RE = /^\s*--- (PASS|FAIL|SKIP): (\S+) \(([\d.]+s)\)/; const PKG_RE = /^(ok|FAIL|---)\s+(\S+)\s+(?:\(cached\)|([\d.]+s)|\[[^\]]+\])/; const LOCATION_RE = /^\s+([\w.-]+\.go):(\d+):/; +const BENCHMARK_RE = /^(Benchmark\S*) *\t[ \t]*\d+ *(?:\t|$)/m; function plainGoParser(): TestOutputParser { let buffer: TestEvent[] = []; @@ -85,6 +86,19 @@ interface GoEvent { Elapsed?: number; } +interface GoPackage { + tests: boolean; + failed: boolean; + /** Benchmark names, with whether Go reported a successful measurement or bench event. */ + benchmarks: Map; +} + +function benchmarkName(pkg: GoPackage, name: string): string { + if (pkg.benchmarks.has(name)) return name; + const withoutCpu = name.replace(/-\d+$/, ""); + return pkg.benchmarks.has(withoutCpu) ? withoutCpu : name; +} + export function goJsonEvent(line: string): GoEvent | null { if (!line.startsWith("{")) return null; try { @@ -94,9 +108,19 @@ export function goJsonEvent(line: string): GoEvent | null { if ( typeof pkg !== "string" || !pkg || - !["start", "run", "pause", "cont", "output", "pass", "fail", "skip", "build-output", "build-fail"].includes( - event.Action, - ) + ![ + "start", + "run", + "pause", + "cont", + "output", + "pass", + "bench", + "fail", + "skip", + "build-output", + "build-fail", + ].includes(event.Action) ) return null; if (event.Test !== undefined && typeof event.Test !== "string") return null; @@ -111,19 +135,48 @@ export function goParser(): TestOutputParser { const plain = plainGoParser(); const located = new Set(); const finished = new Set(); - const packages = new Map(); + const packages = new Map(); const totals = { passed: 0, failed: 0, skipped: 0 }; + + function finish(pkg: GoPackage, path: string[], status: keyof typeof totals, durationMs?: number): TestEvent[] { + const key = path.join("\0"); + if (finished.has(key)) return []; + finished.add(key); + if (path.length === 2) { + totals[status]++; + if (status === "failed") pkg.failed = true; + } else if (path.length === 1 && (!pkg.tests || (status === "failed" && !pkg.failed))) totals[status]++; + return [{ type: "result", path, status, durationMs }]; + } + return { feed(line) { const e = goJsonEvent(line); if (!e) return plain.feed(line); - const pkg = packages.get(e.Package) ?? { tests: false, failed: false }; + const pkg = packages.get(e.Package) ?? { tests: false, failed: false, benchmarks: new Map() }; packages.set(e.Package, pkg); - const path = [e.Package, ...(e.Test ? e.Test.split("/") : [])]; - if (e.Test) pkg.tests = true; - if (e.Action === "run") return [{ type: "start", path }]; + const benchmark = e.Test?.startsWith("Benchmark") ? e.Test : undefined; + const name = benchmark && e.Action !== "run" ? benchmarkName(pkg, benchmark) : e.Test; + const path = [e.Package, ...(name ? name.split("/") : [])]; + if (name) pkg.tests = true; + if (e.Action === "run") { + finished.delete(path.join("\0")); + if (benchmark) pkg.benchmarks.set(benchmark, false); + return [{ type: "start", path }]; + } + if (e.Action === "bench") { + if (name) pkg.benchmarks.set(name, true); + return []; + } if (e.Action === "output" || e.Action === "build-output") { if (!e.Output) return []; + if (e.Action === "output" && (!name || benchmark)) { + const measurement = BENCHMARK_RE.exec(e.Output)?.[1]; + if (measurement) { + const measured = benchmarkName(pkg, measurement); + if (pkg.benchmarks.has(measured) && (!name || name === measured)) pkg.benchmarks.set(measured, true); + } + } const lines = e.Output.replace(/\n$/, "").split("\n"); const events: TestEvent[] = lines.map((line) => ({ type: "output", path, line })); const key = path.join("\0"); @@ -144,20 +197,21 @@ export function goParser(): TestOutputParser { return events; } if (!["pass", "fail", "skip", "build-fail"].includes(e.Action)) return []; - const key = path.join("\0"); - if (finished.has(key)) return []; - finished.add(key); const status = e.Action === "pass" ? "passed" : e.Action === "skip" ? "skipped" : "failed"; - if (e.Test && path.length === 2) { - totals[status]++; - if (status === "failed") pkg.failed = true; - } else if (!e.Test && (!pkg.tests || (status === "failed" && !pkg.failed))) totals[status]++; const durationMs = typeof e.Elapsed === "number" && Number.isFinite(e.Elapsed) && e.Elapsed >= 0 ? e.Elapsed * 1000 : undefined; - return [ - { type: "result", path, status, durationMs }, - { type: "summary", ...totals }, - ]; + const events: TestEvent[] = []; + if (!name) { + // Quiet benchmarks can emit measurements without a terminal event. Wait for the + // package result so a failure from a later CPU setting can take precedence. + for (const [test, measured] of pkg.benchmarks) { + if (measured) events.push(...finish(pkg, [e.Package, ...test.split("/")], "passed")); + } + pkg.benchmarks.clear(); + } + events.push(...finish(pkg, path, status, durationMs)); + if (events.length) events.push({ type: "summary", ...totals }); + return events; }, end: () => plain.end?.() ?? [], };