From b2862cb53ec76431fe8f6db6d914b31dd96cafc4 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 20 Oct 2026 08:46:41 +0000 Subject: [PATCH 1/5] Test the lines that are skipped and the lines after a yield or an await that is not resumed --- test/cli/test/coverage.test.ts | 49 ++++++++++ .../bun/sourcemap/source-positions-corpus.ts | 91 +++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/test/cli/test/coverage.test.ts b/test/cli/test/coverage.test.ts index 6a0e690fa26f..294304956af0 100644 --- a/test/cli/test/coverage.test.ts +++ b/test/cli/test/coverage.test.ts @@ -695,6 +695,55 @@ test("only imports the function", () => { expect(exitCode).toBe(0); }); +test("what is after an await at the top level of a module that throws did not run", async () => { + using dir = tempDir("cov-top-level-await", { + "subject.ts": `let ran = 0; +export const first = await 1; +ran++; +try { + await Promise.reject(new Error()); + ran++; +} catch { + ran++; +} +export default await ran; +export { ran }; +`, + "subject.test.ts": ` +import { expect, test } from "bun:test"; +import ran from "./subject.ts"; + +test("ran", () => { + expect(ran).toBe(2); +}); +`, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "--coverage", "--coverage-reporter=lcov"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + + const lcov = readFileSync(path.join(String(dir), "coverage", "lcov.info"), "utf-8"); + const record = lcov.split("end_of_record").find(r => r.includes("SF:subject.ts")); + expect(record?.match(/^DA:.*$/gm)).toEqual([ + "DA:1,1", + "DA:2,1", + "DA:3,1", + "DA:4,1", + "DA:5,1", + "DA:6,0", + "DA:7,1", + "DA:8,1", + "DA:10,1", + "DA:11,1", + ]); + expect(stderr).toContain(" 1 pass"); + expect(exitCode).toBe(0); +}); + // https://github.com/oven-sh/bun/issues/40586 // Each worker executes a different function of the same module; the merged // report must count a function as covered if any worker ran it. diff --git a/test/js/bun/sourcemap/source-positions-corpus.ts b/test/js/bun/sourcemap/source-positions-corpus.ts index 22b5e6e91ae3..bb536a53158f 100644 --- a/test/js/bun/sourcemap/source-positions-corpus.ts +++ b/test/js/bun/sourcemap/source-positions-corpus.ts @@ -439,6 +439,97 @@ export const cases: Record = { `, }, // What is a function to the engine alone: not one of the file's functions, and not run until it is. + // What is after a statement that the function was left at, and did not come back to. + "suspended.ts": { + functions: "4/4", + lines: ` + 2|function* generator() { + 2| at(); + 2| yield 1; + 1| at(); + 1| yield 2; + 0| at(); + -|} + 1|const resumed = generator(); + 1|resumed.next(); + 1|resumed.next(); + 1|generator().next(); + 1|async function stuck() { + 1| at(); + 1| await new Promise(() => {}); + 0| at(); + -|} + 1|stuck(); + 1|async function rejected() { + 1| try { + 1| await Promise.reject(new Error()); + 0| at(); + 1| } catch { + 1| at(); + -| } + 1| at(); + -|} + 1|rejected(); +`, + }, + // What `?.`, a logical assignment and a value that is there skip. + "skipped.ts": { + functions: "8/8", + lines: ` + 4|function one() { + 4| at(); + 4| return 1; + -|} + 2|function chain(o?: { a(n: number): void }) { + 2| o + 1| ?.a( + 1| one(), + 1| ); + -|} + 1|chain(); + 1|chain({ a() {} }); + 2|function call(f?: (n: number) => void) { + 2| f + -| ?.( + 1| one(), + 1| ); + -|} + 1|call(); + 1|call(() => {}); + 2|function assign(o: Record) { + 2| o.a ||= + 1| one(); + 2| o.b &&= + 0| one(); + 2| o.c ??= + 0| one(); + -|} + 1|assign({ a: 1, b: 0, c: 0 }); + 1|assign({ a: 0, b: 0, c: 0 }); + 2|function parameter( + 2| v = + 1| one(), + -|) { + 2| at(); + -|} + 1|parameter(); + 1|parameter(1); + 1|function pattern({ + 1| v = + 0| one(), + -|}: { v?: number }) { + 1| at(); + -|} + 1|pattern({ v: 1 }); + 1|let node: any = { next: { next: {} } }; + 3|while (node?.next) { + 2| node = node.next; + -|} + 1|switch (2 as number) { + 0| case node?.next: at(); + -|} +`, + }, "engine-functions.ts": { functions: "5/6", lines: ` From 85dbf514d74749f8cbe29b2cf30f542edb70d8a2 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 20 Oct 2026 10:21:49 +0000 Subject: [PATCH 2/5] Use the WebKit where skipped code and code after a yield or an await that is not resumed have basic blocks --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 37dbd9913885..4d93dfb02adb 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "274ebb3cf6008f627197d818e3939d1e2bfc514d"; +export const WEBKIT_VERSION = "autobuild-preview-pr-851-b880fb21"; /** * WebKit (JavaScriptCore) — the JS engine. From 8a47dce9fd8c76495761756dbc3e7789531b0e95 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 20 Oct 2026 10:31:45 +0000 Subject: [PATCH 3/5] The generators program has what is after a yield or an await that is not resumed, with return(), throw() and yield* --- test/cli/test/coverage.test.ts | 98 +++++++++---------- .../bun/sourcemap/source-positions-corpus.ts | 48 +++++---- 2 files changed, 76 insertions(+), 70 deletions(-) diff --git a/test/cli/test/coverage.test.ts b/test/cli/test/coverage.test.ts index 294304956af0..008a19a88fa7 100644 --- a/test/cli/test/coverage.test.ts +++ b/test/cli/test/coverage.test.ts @@ -695,55 +695,6 @@ test("only imports the function", () => { expect(exitCode).toBe(0); }); -test("what is after an await at the top level of a module that throws did not run", async () => { - using dir = tempDir("cov-top-level-await", { - "subject.ts": `let ran = 0; -export const first = await 1; -ran++; -try { - await Promise.reject(new Error()); - ran++; -} catch { - ran++; -} -export default await ran; -export { ran }; -`, - "subject.test.ts": ` -import { expect, test } from "bun:test"; -import ran from "./subject.ts"; - -test("ran", () => { - expect(ran).toBe(2); -}); -`, - }); - await using proc = Bun.spawn({ - cmd: [bunExe(), "test", "--coverage", "--coverage-reporter=lcov"], - env: bunEnv, - cwd: String(dir), - stderr: "pipe", - }); - const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); - - const lcov = readFileSync(path.join(String(dir), "coverage", "lcov.info"), "utf-8"); - const record = lcov.split("end_of_record").find(r => r.includes("SF:subject.ts")); - expect(record?.match(/^DA:.*$/gm)).toEqual([ - "DA:1,1", - "DA:2,1", - "DA:3,1", - "DA:4,1", - "DA:5,1", - "DA:6,0", - "DA:7,1", - "DA:8,1", - "DA:10,1", - "DA:11,1", - ]); - expect(stderr).toContain(" 1 pass"); - expect(exitCode).toBe(0); -}); - // https://github.com/oven-sh/bun/issues/40586 // Each worker executes a different function of the same module; the merged // report must count a function as covered if any worker ran it. @@ -804,6 +755,55 @@ test("calls second", () => { // it is loaded. Each case has a subject file of its own. Where a case loads // its subject twice, one load runs the `if` branch and the other runs the // `return` after it. +test("what is after a rejected await at the top level of a module did not run", async () => { + using dir = tempDir("cov-top-level-await", { + "subject.ts": `let ran = 0; +export const first = await 1; +ran++; +try { + await Promise.reject(new Error()); + ran++; +} catch { + ran++; +} +export default await ran; +export { ran }; +`, + "subject.test.ts": ` +import { expect, test } from "bun:test"; +import ran from "./subject.ts"; + +test("ran", () => { + expect(ran).toBe(2); +}); +`, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "--coverage", "--coverage-reporter=lcov"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + + const lcov = readFileSync(path.join(String(dir), "coverage", "lcov.info"), "utf-8"); + const record = lcov.split("end_of_record").find(r => r.includes("SF:subject.ts")); + expect(record?.match(/^DA:.*$/gm)).toEqual([ + "DA:1,1", + "DA:2,1", + "DA:3,1", + "DA:4,1", + "DA:5,1", + "DA:6,0", + "DA:7,1", + "DA:8,1", + "DA:10,1", + "DA:11,1", + ]); + expect(stderr).toContain(" 1 pass"); + expect(exitCode).toBe(0); +}); + describe("a file loaded more than once counts every load", () => { const esm = `export function covered(n: number): number { if (n > 5) { diff --git a/test/js/bun/sourcemap/source-positions-corpus.ts b/test/js/bun/sourcemap/source-positions-corpus.ts index bb536a53158f..cf77499f51ae 100644 --- a/test/js/bun/sourcemap/source-positions-corpus.ts +++ b/test/js/bun/sourcemap/source-positions-corpus.ts @@ -421,28 +421,14 @@ export const cases: Record = { 1|} catch {} `, }, + // What is after a statement that the function was left at, and did not come back to. "generators.ts": { - functions: "4/4", + functions: "7/7", lines: ` - 1|function* once() { - 1| yield 1; - -|} 1|function* never() { 0| yield 1; -|} - 1|async function stuck() { - 1| await new Promise(() => {}); - -|} - 1|once().next(); 1|never(); - 1|stuck(); -`, - }, - // What is a function to the engine alone: not one of the file's functions, and not run until it is. - // What is after a statement that the function was left at, and did not come back to. - "suspended.ts": { - functions: "4/4", - lines: ` 2|function* generator() { 2| at(); 2| yield 1; @@ -454,6 +440,26 @@ export const cases: Record = { 1|resumed.next(); 1|resumed.next(); 1|generator().next(); + 2|function* left() { + 2| try { + 2| yield 1; + 0| at(); + 1| } catch { + 1| at(); + -| } + 1| at(); + -|} + 1|const returned = left(); + 1|returned.next(); + 1|returned.return(); + 1|const thrown = left(); + 1|thrown.next(); + 1|thrown.throw(new Error()); + 1|function* delegates() { + 1| yield* [1, 2]; + 0| at(); + -|} + 1|delegates().next(); 1|async function stuck() { 1| at(); 1| await new Promise(() => {}); @@ -472,7 +478,7 @@ export const cases: Record = { 1|rejected(); `, }, - // What `?.`, a logical assignment and a value that is there skip. + // What `?.`, a logical assignment and a value that is there skip. A `case` that does not match did not run. "skipped.ts": { functions: "8/8", lines: ` @@ -489,10 +495,9 @@ export const cases: Record = { 1|chain(); 1|chain({ a() {} }); 2|function call(f?: (n: number) => void) { - 2| f - -| ?.( - 1| one(), - 1| ); + 2| f?.( + 1| one(), + 1| ); -|} 1|call(); 1|call(() => {}); @@ -530,6 +535,7 @@ export const cases: Record = { -|} `, }, + // What is a function to the engine alone: not one of the file's functions, and not run until it is. "engine-functions.ts": { functions: "5/6", lines: ` From 981178b9f2ea9c16fb90f35a9d03e02d44eb476d Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 21 Oct 2026 03:06:03 +0000 Subject: [PATCH 4/5] Test the ranges node:inspector has for what is skipped and for what is after a yield or an await that is not resumed --- .../node/inspector/inspector-profiler.test.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/test/js/node/inspector/inspector-profiler.test.ts b/test/js/node/inspector/inspector-profiler.test.ts index f8a6b8cf2c18..b23a04635319 100644 --- a/test/js/node/inspector/inspector-profiler.test.ts +++ b/test/js/node/inspector/inspector-profiler.test.ts @@ -760,6 +760,70 @@ console.log( ]); }); + test.concurrent("what is skipped, or after a yield or an await that is not resumed, is a range", async () => { + using dir = tempDir("inspector-coverage-skipped", { + "fixture.cjs": ` + const inspector = require("node:inspector"); + const vm = require("node:vm"); + const code = [ + "function one() { return 1; }", + "function parameter(v = one()) { return v; }", + "function chain(o) { return o?.a(one()); }", + "function assign(o) { o.a ||= one(); return o; }", + "function* generator() { yield 1; one(); }", + "async function stuck() { await new Promise(() => {}); one(); }", + "parameter(1); parameter(); chain(); assign({ a: 1 }); generator().next(); stuck();", + "", + ].join("\\n"); + const session = new inspector.Session(); + session.connect(); + session.post("Profiler.enable"); + session.post("Profiler.startPreciseCoverage", { callCount: true, detailed: true }); + vm.runInThisContext(code, { filename: "/virtual/skipped.js" }); + session.post("Profiler.takePreciseCoverage", (err, { result }) => { + const [script, ...functions] = result.find(script => script.url.endsWith("/virtual/skipped.js")).functions; + console.log( + JSON.stringify(functions.map(f => f.ranges.map(r => [r.count, code.slice(r.startOffset, r.endOffset)]))), + ); + }); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "fixture.cjs"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect({ stderrIfFailed: exitCode === 0 ? "" : stderr, exitCode }).toEqual({ stderrIfFailed: "", exitCode: 0 }); + expect(JSON.parse(stdout)).toEqual([ + [[1, "function one() { return 1; }"]], + [ + [2, "function parameter(v = one()) { return v; }"], + [1, " one()"], + ], + [ + [1, "function chain(o) { return o?.a(one()); }"], + [0, "?.a(one())"], + ], + [ + [1, "function assign(o) { o.a ||= one(); return o; }"], + [0, " one()"], + ], + [ + [1, "function* generator() { yield 1; one(); }"], + [0, " one(); "], + ], + [ + [1, "async function stuck() { await new Promise(() => {}); one(); }"], + [0, " one(); "], + ], + [[1, "() => {}"]], + ]); + }); + test.concurrent("respects callCount: false and detailed: false", async () => { using dir = tempDir("inspector-coverage-flags", { "fixture.mjs": coverageVmFixture, From bc2247f2a7240d867a29a570897ca2e7094a4a38 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Wed, 21 Oct 2026 05:39:06 +0000 Subject: [PATCH 5/5] Use the WebKit that has the basic blocks for skipped code and for code after a yield or an await that is not resumed --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 4d93dfb02adb..1e352fc538bb 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "autobuild-preview-pr-851-b880fb21"; +export const WEBKIT_VERSION = "d0b16ad0ce2bac652f0f63cdcc642a7d3ba69631"; /** * WebKit (JavaScriptCore) — the JS engine.