Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 25 additions & 31 deletions src/runtime/cli/test/Scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,34 +161,6 @@ impl<'a> Scanner<'a> {
}
}

// you typed "." and we already scanned it
if !self.has_iterated {
if let EntriesOption::Entries(entries) = root {
// Collect first so `self.next(…)` doesn't overlap the
// `entries.data` borrow.
// this branch is taken when the resolver already has
// `path` cached (e.g. `run_env_loader`/`read_dir_info` read the
// cwd before the scanner runs), so `read_directory_with_iterator`
// returned the cached `EntryMap` without invoking `iterator.next`.
// Hash-map iteration order is not stable. Sort by (lowercased)
// base name so test-file discovery order is deterministic —
// regression/issue/26851 relies on `a_*.test` running before
// `b_*.test` under `--bail`.
let mut entry_ptrs: Vec<*mut fs::Entry> = entries.data.values().copied().collect();
index_sort::sort_slice_by(&mut entry_ptrs, |a, b| {
// SAFETY: `EntryMap` stores `*mut Entry` into the
// process-static `EntryStore`; valid for `'static`.
let (an, bn) = unsafe { ((**a).base_lowercase(), (**b).base_lowercase()) };
an.cmp(bn)
});
for entry_ptr in entry_ptrs {
// SAFETY: `EntryMap` stores `*mut Entry` into the
// process-static `EntryStore`; valid for `'static`.
self.next(unsafe { &mut *entry_ptr });
}
}
}

while let Some(entry) = self.dirs_to_scan.pop_front() {
let parts2: [&[u8]; 2] = [entry.dir_path, entry.name.slice()];
let Some(path2) = self.fs().abs_buf_checked(&parts2, &mut scan_dir_buf) else {
Expand Down Expand Up @@ -222,18 +194,40 @@ impl<'a> Scanner<'a> {
Ok(())
}

/// Passes every entry of `name` to `next`.
/// `handle` stays owned by the caller; the resolver caches the listing but not the fd.
fn read_dir_with_name(
&mut self,
name: &[u8],
handle: Option<Fd>,
) -> crate::Result<&'static mut EntriesOption> {
self.has_iterated = false;
let fs_ptr = self.fs;
let iter = ScannerDirIter(std::ptr::from_mut::<Scanner<'a>>(self));
// SAFETY: borrows only the `fs` field; re-entrant access is serialised by `RealFS.entries_mutex`.
unsafe { &mut (*fs_ptr).fs }
.read_directory_with_iterator(name, handle, 0, false, iter)
.map_err(Into::into)
let listing = unsafe { &mut (*fs_ptr).fs }
.read_directory_with_iterator(name, handle, 0, false, iter)?;

// On a cache hit the resolver returns the listing without calling the iterator.
if !self.has_iterated {
if let EntriesOption::Entries(entries) = &*listing {
// Sorted: hash-map order is not stable, and #26851 needs a deterministic order.
let mut entry_ptrs: Vec<*mut fs::Entry> = entries.data.values().copied().collect();
Comment thread
robobun marked this conversation as resolved.
index_sort::sort_slice_by(&mut entry_ptrs, |a, b| {
// SAFETY: `EntryMap` stores `*mut Entry` into the
// process-static `EntryStore`; valid for `'static`.
let (an, bn) = unsafe { ((**a).base_lowercase(), (**b).base_lowercase()) };
an.cmp(bn)
});
for entry_ptr in entry_ptrs {
// SAFETY: `EntryMap` stores `*mut Entry` into the
// process-static `EntryStore`; valid for `'static`.
self.next(unsafe { &mut *entry_ptr });
Comment thread
robobun marked this conversation as resolved.
}
}
}

Ok(listing)
}

pub(crate) fn could_be_test_file<const NEEDS_TEST_SUFFIX: bool>(&self, name: &[u8]) -> bool {
Expand Down
76 changes: 76 additions & 0 deletions test/cli/test/bun-test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1945,6 +1945,82 @@ describe.concurrent("test file discovery (scanner)", () => {
expect(exitCode).toBe(0);
});

// The resolver lists the cwd and its parents before the scanner runs, and it
// keeps the listing of every directory an earlier path argument walked. The
// scanner has to walk those listings too.
const ranTest = (name: string) =>
`import { test } from "bun:test"; test("${name}", () => { console.log("RAN ${name}"); });`;
async function discover(cwd: string, args: readonly string[]) {
await using proc = Bun.spawn({
cmd: [bunExe(), "test", ...args],
env: bunEnv,
cwd,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return {
ran: stdout
.split(/\r?\n/)
.filter(line => line.startsWith("RAN "))
.sort(),
summary: stderr.match(/^Ran \d+ tests? across \d+ files?\./m)?.[0] ?? stderr,
exitCode,
};
}

test.each(["./nested ./", "./ ./nested"])('path arguments "%s" run every test below both, once', async order => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
using dir = tempDir("scanner-two-paths", {
"root_only.test.ts": ranTest("root"),
"nested/inner.test.ts": ranTest("inner"),
"nested/deeper/most.test.ts": ranTest("most"),
"other/other.test.ts": ranTest("other"),
});

expect(await discover(String(dir), order.split(" "))).toEqual({
ran: ["RAN inner", "RAN most", "RAN other", "RAN root"],
summary: "Ran 4 tests across 4 files.",
exitCode: 0,
});
});

describe.each([
{ how: "a path argument", args: ["../../"], bunfig: {} },
{ how: "the bunfig test root", args: [], bunfig: { "pkg/app/bunfig.toml": `[test]\nroot = "../../"\n` } },
])("$how that is a parent of the cwd", ({ args, bunfig }) => {
test("runs the tests in every directory", async () => {
using dir = tempDir("scanner-parent-of-cwd", {
...bunfig,
"top.test.ts": ranTest("top"),
"sibling/sibling.test.ts": ranTest("sibling"),
"pkg/pkg.test.ts": ranTest("pkg"),
"pkg/lib/lib.test.ts": ranTest("lib"),
"pkg/app/app.test.ts": ranTest("app"),
"pkg/app/deeper/deeper.test.ts": ranTest("deeper"),
});

expect(await discover(join(String(dir), "pkg", "app"), args)).toEqual({
ran: ["RAN app", "RAN deeper", "RAN lib", "RAN pkg", "RAN sibling", "RAN top"],
summary: "Ran 6 tests across 6 files.",
exitCode: 0,
});
});

// Without the walk this run finds no test file and exits 1.
test("finds a test that is only below the cwd", async () => {
using dir = tempDir("scanner-parent-of-cwd-only", {
...bunfig,
"pkg/app/app.test.ts": ranTest("app"),
});

expect(await discover(join(String(dir), "pkg", "app"), args)).toEqual({
ran: ["RAN app"],
summary: "Ran 1 test across 1 file.",
exitCode: 0,
});
});
});

// The scanner builds every absolute path in a PathBuffer of MAX_PATH_BYTES:
// 4096 on Linux, 1024 on every other POSIX (src/bun_core/util.rs). On Windows
// it is 32767*3+1 bytes, more than a command line or an NT path can hold, so
Expand Down
Loading