From a1bbae544cddf37ae862efc01588df37736a0048 Mon Sep 17 00:00:00 2001 From: Trey Turner Date: Mon, 23 Sep 2024 06:27:37 -0500 Subject: [PATCH] feat: support default test timeout in bunfig.toml --- docs/runtime/bunfig.md | 9 ++ src/bunfig.zig | 8 ++ test/cli/test/bun-test-bunfig.test.ts | 129 ++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 test/cli/test/bun-test-bunfig.test.ts diff --git a/docs/runtime/bunfig.md b/docs/runtime/bunfig.md index 4af518744556a2..7e22c6d492fb8a 100644 --- a/docs/runtime/bunfig.md +++ b/docs/runtime/bunfig.md @@ -144,6 +144,15 @@ Same as the top-level `smol` field, but only applies to `bun test`. smol = true ``` +### `test.timeout` + +A default timeout for each test in milliseconds. Default `5000`. Use `--timeout` to override. + +```toml +[test] +timeout = 15000 +``` + ### `test.coverage` Enables coverage reporting. Default `false`. Use `--coverage` to override. diff --git a/src/bunfig.zig b/src/bunfig.zig index ede8389cde3c3d..88ef8e21dbc88f 100644 --- a/src/bunfig.zig +++ b/src/bunfig.zig @@ -253,6 +253,14 @@ pub const Bunfig = struct { this.ctx.runtime_options.smol = expr.data.e_boolean.value; } + if (test_.get("timeout")) |expr| { + try this.expect(expr, .e_number); + // Is there a better way to only use bunfig value if --timeout wasn't passed as a CLI argument? + if (this.ctx.test_options.default_timeout_ms == (Command.TestOptions{}).default_timeout_ms) { + this.ctx.test_options.default_timeout_ms = expr.data.e_number.toU32(); + } + } + if (test_.get("coverage")) |expr| { try this.expect(expr, .e_boolean); this.ctx.test_options.coverage.enabled = expr.data.e_boolean.value; diff --git a/test/cli/test/bun-test-bunfig.test.ts b/test/cli/test/bun-test-bunfig.test.ts new file mode 100644 index 00000000000000..497a38f13dd2dd --- /dev/null +++ b/test/cli/test/bun-test-bunfig.test.ts @@ -0,0 +1,129 @@ +import { spawnSync } from "bun"; +import { afterEach, describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDirWithFiles } from "harness"; +import { rmSync } from "node:fs"; + +describe("bunfig test options", () => { + describe("timeout", () => { + let cwd: string; + + afterEach(() => { + if (cwd) rmSync(cwd, { recursive: true }); + }); + + test("bunfig timeout overrides default", async () => { + const bunfigTimeout = 500; + const bunfig = `[test]\ntimeout = ${bunfigTimeout}\n`; + const testFile = ` + import { test, expect } from "bun:test"; + test("takes 2 seconds", async () => { + const p = new Promise(r => setTimeout(r, 2000, true)); + expect(await p).toBeTrue(); + }); + `; + cwd = tempDirWithFiles("test.bunfig.timeout", { + "bunfig.toml": bunfig, + "bun-test-timeout.test.ts": testFile, + }); + + const result = spawnSync({ + cmd: [bunExe(), "-c=bunfig.toml", "test"], + env: bunEnv, + stderr: "pipe", + cwd, + }); + const stderr = result.stderr.toString().trim(); + + const errorPtn = /timed out after (\d+)ms/; + const errorMatch = stderr.match(errorPtn); + expect(errorMatch, "test didn't report timeout error to stderr").not.toBeNull(); + const errorTimeout = parseInt(errorMatch!.at(1)!); + expect(errorTimeout, "test timeout error doesn't reflect bunfig value").toEqual(bunfigTimeout); + + const durationPtn = /\(fail\) .* \[(\d+)(?:\.\d+)?ms\]/; + const durationMatch = stderr.match(durationPtn); + expect(durationMatch, "test didn't output failing result with actual duration to stderr").not.toBeNull(); + const duration = parseInt(durationMatch!.at(1)!); + expect(duration, "test timed out before bunfig timeout value").toBeGreaterThanOrEqual(bunfigTimeout); + expect(duration, "test didn't honor bunfig timeout value").toBeLessThanOrEqual(5000); + }); + + test("cli timeout overrides bunfig", async () => { + const cliTimeout = 500; + const bunfigTimeout = 1000; + const bunfig = `[test]\ntimeout = ${bunfigTimeout}\n`; + const testFile = ` + import { test, expect } from "bun:test"; + test("takes 2 seconds", async () => { + const p = new Promise(r => setTimeout(r, 2000, true)); + expect(await p).toBeTrue(); + }); + `; + cwd = tempDirWithFiles("test.cli.timeout.wins", { + "bunfig.toml": bunfig, + "bun-test-cli-timeout-wins.test.ts": testFile, + }); + + const result = spawnSync({ + cmd: [bunExe(), "-c=bunfig.toml", "test", "--timeout", `${cliTimeout}`], + env: bunEnv, + stderr: "pipe", + cwd, + }); + const stderr = result.stderr.toString().trim(); + + const errorPtn = /timed out after (\d+)ms/; + const errorMatch = stderr.match(errorPtn); + expect(errorMatch, "test didn't report timeout error to stderr").not.toBeNull(); + const errorTimeout = parseInt(errorMatch!.at(1)!); + expect(errorTimeout, "test timeout error doesn't reflect cli value").toEqual(cliTimeout); + + const durationPtn = /\(fail\) .* \[(\d+)(?:\.\d+)?ms\]/; + const durationMatch = stderr.match(durationPtn); + expect(durationMatch, "test didn't output failing result with actual duration to stderr").not.toBeNull(); + const duration = parseInt(durationMatch!.at(1)!); + expect(duration, "test timed out before cli value").toBeGreaterThanOrEqual(cliTimeout); + expect(duration, "test honored bunfig timeout instead of cli").toBeLessThan(bunfigTimeout); + }); + + test("default timeout specified via cli overrides bunfig", async () => { + // This addresses a corner case identified by a weak initial implementation. + // By necessity it requires the default test timeout to prove (5s) so it should + // be removed after passing in the PR. + const cliTimeout = 5000; + const bunfigTimeout = 500; + const bunfig = `[test]\ntimeout = ${bunfigTimeout}\n`; + const testFile = ` + import { test, expect } from "bun:test"; + test("takes 7 seconds", async () => { + const p = new Promise(r => setTimeout(r, 7000, true)); + expect(await p).toBeTrue(); + }); + `; + cwd = tempDirWithFiles("test.cli.timeout.wins.using.default", { + "bunfig.toml": bunfig, + "bun-test-cli-timeout-wins-using-default.test.ts": testFile, + }); + + const result = spawnSync({ + cmd: [bunExe(), "-c=bunfig.toml", "test", "--timeout", `${cliTimeout}`], + env: bunEnv, + stderr: "pipe", + cwd, + }); + const stderr = result.stderr.toString().trim(); + + const errorPtn = /timed out after (\d+)ms/; + const errorMatch = stderr.match(errorPtn); + expect(errorMatch, "test didn't report timeout error to stderr").not.toBeNull(); + const errorTimeout = parseInt(errorMatch!.at(1)!); + expect(errorTimeout, "test timeout error doesn't reflect cli value").toEqual(cliTimeout); + + const durationPtn = /\(fail\) .* \[(\d+)(?:\.\d+)?ms\]/; + const durationMatch = stderr.match(durationPtn); + expect(durationMatch, "test didn't output failing result with actual duration to stderr").not.toBeNull(); + const duration = parseInt(durationMatch!.at(1)!); + expect(duration, "test timed out before cli value").toBeGreaterThanOrEqual(cliTimeout); + }); + }); +});