Repository navigation
Conversation
test.each`...`, it.each`...` and describe.each`...` call each(strings, ...values). fn_each read the first argument only, so the strings array became the table: one test per string segment, no values, and the title was not interpolated. fn_each now builds one row object per row when it gets a template strings array and values. The headings come from strings[0] with jest-each's grammar. The rows go in the existing table slot, so the title call runs the array-of-objects path. A malformed table does not throw from .each: the title call throws the error, with jest-each's wording. is_js_whitespace moves from node_assert.rs to bun_core::strings, and both users share it.
The heading row is one line of names with | between them. jest-each also takes an empty name, and a row that goes on after a | on the next line. Those are malformed here, with the same error. For every other table the headings are the ones jest-each reads. The row type of the template overloads defaults to any, so a callback can annotate its row parameter.
|
Status Reproduced on main (9f70da0) and on bun 1.4.3-canary with this file: import { test, expect } from "bun:test";
const price = { apple: 3, pear: 4 };
test.each`
fruit | expected
${"apple"} | ${999}
${"pear"} | ${999}
`("price of $fruit is $expected", ({ fruit, expected }) => {
expect(price[fruit]).toBe(expected);
});
With this PR, |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
WalkthroughBun now supports tagged-template tables with ChangesTagged-template test tables
Shared JavaScript whitespace handling
Suggested reviewers: Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline findings, I also checked two things that turned out fine: the is_js_whitespace move into bun_core::strings keeps the exact code point set node_assert used before (0x09..=0x0D, 0x2028/0x2029, 0xFEFF plus the Zs separators), so the assert diff trimEnd behavior is unchanged; and values.len() % headings.len() in template_table cannot divide by zero because template_headings returns None for an empty heading row.
Extended reasoning...
The change adds the tagged-template form of test.each/describe.each in src/runtime/test_runner/ScopeFunctions.rs, moves a whitespace predicate into bun_core, and ships matching .d.ts overloads, docs, and tests; it touches no security-sensitive surface. The template table and any malformed-table Error are built inside a MarkedArgumentBuffer closure and handed to create_bound, so no unrooted JSValue is held across JS-entering calls. Inline findings on title formatting, heading-row strictness, VS Code discovery, and test timeouts are already posted, which is why this run does not approve.
Additional findings (outside the current diff — GitHub can't attach inline comments there):
-
🟡
packages/bun-vscode/src/features/tests/bun-test-controller.ts— Users of the in-repo VS Code extension get no Test Explorer entry or run button for any test written with the newtest.each/describe.eachtemplate form. The discovery regex at packages/bun-vscode/src/features/tests/bun-test-controller.ts:343 only accepts.eachfollowed by(, sotest.each...("title", fn)never matches and the title call is skipped. Fix: let discovery accept.eachfollowed by a template literal (andexpandEachTestsat :453 fall back to one node for it) for every modifier chain the regex already covers, so the runtime surface this PR ships is visible in the extension. The PR notes this gap but leaves the only consumer in the repo unchanged.Why this was flagged
A test file contains a tagged-template table such as the one documented in docs/test/writing-tests.mdx, e.g. test.each
\n a | b\n ${1} | ${2}\n("add($a, $b)", fn). The extension reads the file through parseTestBlocks (packages/bun-vscode/src/features/tests/bun-test-controller.ts:334). Its testRegex at :343 requires the optional group(?:\.each\s*\([^)]*\))?and then\s*\(directly after the modifier chain; the characters aftertestare.eachfollowed by a backtick, so neither alternative matches and exec moves on. No TestNode is created, so the Test Explorer tree, gutter run buttons and per-test run/debug targets omit every row of that table. On the base branch the same source also does not match, but there the template form is not a supported, documented bun:test API; after this PR the extension in the same repository disagrees with the runtime it ships for. The array form keeps working because.each(still matches at :343 and :453.Verification: Triggering condition: a user of the in-repo VS Code extension writes a test with the newly supported
test.each\...`("title", fn)form. The only.eachalternative in the discovery regex at packages/bun-vscode/src/features/tests/bun-test-controller.ts:343 is.each\s*(`, so no match is produced. The base branch has the same regex, but the base had no working template form.
A `|` at the start of the text after the heading row made the table
malformed. jest-each reads more headings there only when a name follows
the `|`. Now a `|` with no name after it starts a row of values, so a
table with `| ${1} | ${2} |` rows runs as it does in Jest.
A `|` and a name at the start of the next line stay malformed, with or
without a newline after the name.
|
Reply to the review above. The details are in the three threads.
The two commits are not on this PR, so that the head under review (9bbaa04) does not move. They go on this PR if a maintainer asks for them. Otherwise they go into a follow-up PR after the merge. |
test/integration/bun-types/fixture/test.ts: both sides added cases at
the same place. Both stay.
The `each` overloads on main take `readonly T[]`, so a template with no
`${value}` type-checks as a table of strings. The `@ts-expect-error`
cases for it are gone from the fixture and from jest-each.test.ts. The
title call still throws for such a table.
main has `strings::is_js_whitespace` (#44805). The copy this branch
added to immutable.rs is gone, and node_assert.rs is as on main.
In a debug ASAN build the child takes 3.5 s to start when it collects at every third slow allocation. The three tests take 3 s to 7 s there, so the default timeout of 5 s is not enough.
|
Updated 7:11 AM PT - Oct 14th, 2026
❌ @robobun, your commit ad90f9a has 2 failures in
🧪 To try this PR locally: bunx bun-pr 44402That installs a local version of the PR into your bun-44402 --bun |
|
This branch had a conflict with main, so the head moved from 9bbaa04 to ad90f9a. What changed:
The diff against main is 6 files now. The PR body describes this state. |
Fixes #6364
Problem
test.eachordescribe.eachruns one test per string piece of the template. The callbacks get no row. A table that must fail prints5 pass, exit 0.fn_each(src/runtime/test_runner/ScopeFunctions.rs:120) read its first argument only. A tagged template callseach(strings, ...values), so the strings became the table.Fix
fn_eachbuilds one object per row from the template strings and values. The title call registers those rows as a table of objects.strings[0], one row of names. A malformed table throws at the title call, with jest-each's message.test/js/bun/test/jest-each.test.tsandjest-each-gc-root.test.ts(stock bun fails both). Alsobun-test.test.ts -t each, bun-types, clippy.$namefix for null cells (bun:test: format test.each titles with Jest's placeholder semantics #41565) and template mode for arrays with extra arguments.Background
.each(table)returns a function. The call with the title (the title call) registers one test per row.ScopeFunctionskeeps the table in one GC-visited slot.stringshave an ownrawarray..each(), Jest's failing-test entry, new rows per title call (Notes).Downsides
describeblock. Jest runs the rest.$namefor a null cell stays as text until bun:test: format test.each titles with Jest's placeholder semantics #41565.undefinedrow: +729 and 1 malloc. Binary: +6,400 bytes.Notes
Repro
price of $fruit is $expected,5 pass, exit 0.(fail) price of apple is 999,(fail) price of pear is 999, exit 1.What is in the diff
src/runtime/test_runner/ScopeFunctions.rs: the change. It usesstrings::is_js_whitespacefrombun_core(Addbun lintandbun format#44805).packages/bun-types/test.d.ts: one template overload on eacheachset. The row type defaults toany, so a callback can annotate its row parameter.test.each<Row>types it too.${value}type-checks as a table of strings, because the array overloads takereadonly T[]since Fix every type error intest/#44614. The title call throws for it.docs/test/writing-tests.mdx: a new section.Designs weighed
rawon every.each(array): about +660 instructions and 1 malloc for each call. A template with headings and no values becameRan 0 tests, exit 0..each(). It fires for a result that never gets a title. Jest and Vitest stay silent there..skip,.todo,.failingand-t, so a malformed table can hide. It also needs one more host function..each. No.eachcall site intest/(1,598 of them) can see the difference.Heading row
jest-each reads the headings with
/^\n\s*[^\s]+\s*(\|\s*[^\s]+\s*)*\n/, then removes the whitespace and splits on|.template_headingsreads one row: a newline, names with|between them, a newline.aand|, and two more alphabets with tab, carriage return, no-break space, U+3000 and non-ASCII letters.a||b), a row that goes on after a newline (a |thenb), and a|with a name at the start of the next line. They get the same error as any other malformed heading row.|, as in| ${1} | ${2} |. Only a|with a name after it counts as more headings.Differences from Jest that stay
.eachresult get the same row objects. jest-each builds new ones. Vitest and bun's array tables share them too.format_label, the same as for a table of objects.$namewith a null or undefined value and$#are in bun:test: format test.each titles with Jest's placeholder semantics #41565. A non-ASCII$nameprints broken bytes today: the owner of bun:test: format test.each titles with Jest's placeholder semantics #41565 has the report. Jest leaves the%specifiers literal for a template table. bun applies them to the row object, so%jthrows for a row that JSON cannot serialize.thisin the callback is the row, as for array tables.test.each([])registers nothing,test.each(123)throws from.each(), and an array with more arguments after it is a table.rawand values after it, and a strings array of several strings with no values, stay array tables..each(only.Measurements
Taken at 9bbaa04, before the merge with main. The code on the measured paths did not change after that.
template_headingschanged by one condition, and it runs only for a template table.Two release builds of the same tree, with
src/of the merge base and of this PR (x64 Linux). Instruction counts are gdbstepiover one call, inclusive, for 5 consecutive calls after a warm-up.callsarecallinstructions. mallocs and frees are entries into the mimalloc entry points..each(array):[[1,2,3]],["apple"],[{a:1}]test()registration[[1,2],[3,4]][[5,6]][{a:1}]["apple"][undefined].each(array): +0 GC cells (4 before and after: 2 Function, 1 string, 1 ScopeFunctions.bun:jscheapStats over 20,000 kept results).rawlookups on existing array tables: 0 infn_each, 0 at the title call, over 36.eachcalls and 35 title calls, 9 of them on a table of one row (temporary debug counter. main'sjest-each.test.tsand the.eachblock ofbun-test.test.ts)..text(size). Host functions: +0 (103__jsc_host_symbols in both). Generated classes: 0 lines of diff.catch syscall): +0 for each of the 36 syscalls with a stable count (openat 23, read 22, write 13, close 22 and 32 more). The counts of futex, epoll_pwait2, restart_syscall, madvise, getdents64, clone, mmap and prctl differ between two runs of one binary..eachof 2 rows and 2 columns: 7 GC cells for each call (the 4 above, 1 Array, 2 Object). A malformed template: 6 (the 4 above, 1 Error, 1 string).Self-review
Addressed: the heading matcher is now one row of names (it was an emulation of the regex). The row type defaults to
any. The docs and JSDoc sentences about the heading row and the value count are exact. The heading test has a string that changes the result for each of 14 mutants of a JavaScript model of the parser. This body lists the other differences from Jest and has no open question.After the review on this PR: a value row that starts with
|now runs, as in Jest. The three new GC tests keep their timeout, with the reason beside it: a debug ASAN build takes 3 s to 7 s for each, because the child also collects while it starts.Not taken:
$namefix for null and undefined cells into this PR. bun:test: format test.each titles with Jest's placeholder semantics #41565 ownsformat_label, and the fix changes the titles and snapshot keys of existing array tables.test.each([[1, 2]], extra)works today, and it stays a table.Earlier work
#34974 built the rows in
fn_eachand stored them as an ordinary array. This PR keeps that, and its docs section and child-run test. #30625 and #11358 implemented the same form before it.Suites run
On the merge with main (408c937):
bun bd testoftest/js/bun/test/jest-each.test.ts(also withBUN_JSC_validateExceptionChecks=1),test/js/bun/test/jest-each-gc-root.test.ts,test/regression/issue/11793.test.tsandtest/cli/test/bun-test.test.ts -t each(25 pass).bun testoftest/integration/bun-types/bun-types.test.tsandtest/internal/source-lints/.bun lint,bun fmt --checkon the changed files,cargo clippy -p bun_runtime.bun checkintest/reports no error in the two test files.With stock bun: 69 tests fail in
jest-each.test.ts, and the 3 new tests fail injest-each-gc-root.test.ts. The new tests that pass there pin behaviour that must not change (an array table with more arguments, a.skiprow, a malformed table with no title call).no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/test/jest-each-gc-root.test.ts