Repository navigation
Conversation
|
Status Reproduction on Bun 1.4.3 (Linux x64): mkdir t && cd t
echo 'import { test } from "bun:test"; test("s", () => {});' > s.test.js
ln -s . x && ln -s . y
bun test # never exits; memory and open fds growWith this branch the same tree prints This PR is a draft. Two items are open: the cost numbers in Downsides, and the question for a maintainer at the top of Notes. |
|
Updated 3:03 AM PT - Oct 3rd, 2026
❌ @robobun, your commit 46db8f9 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 44487That installs a local version of the PR into your bun-44487 --bun |
| /// The entry is itself a link (a POSIX symlink, a Windows reparse point). | ||
| /// `symlink` alone does not say so: the resolver also fills it for real directories. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// Where the link leads, with every link on the way resolved. Empty when | ||
| /// the entry is not a link or its target could not be named. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// [`Entry::kind`], plus whether the entry is a link and where it leads. | ||
| /// | ||
| /// # Safety | ||
| /// Same contract as [`Entry::kind`]. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| // A directory outside the walk's own tree can be one the resolver | ||
| // listed before discovery (a parent of the cwd). The read then | ||
| // returns that listing and calls nothing. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// Runs `next` over a listing the resolver returned from its cache without | ||
| /// calling the iterator (`run_env_loader`/`read_dir_info` read the cwd | ||
| /// before the scanner runs). |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| // 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`. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// A directory the walk does not enter: a dot-directory, `node_modules`, | ||
| /// an excluded name, or a path that an ignore pattern matches. `path` | ||
| /// joins to the directory's path and `name_lowercase` is its last part. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| // reshaped for borrowck — drop the &mut borrow from | ||
| // abs_buf and reborrow open_dir_buf immutably so &self methods | ||
| // can be called with the slice. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// Decides a directory entry that is a link, or that sits in a directory | ||
| /// the walk entered through a link, so that each directory is listed once. | ||
| /// A link to a directory the walk reaches by itself is not followed. Any | ||
| /// other directory is entered the first time only, under its real path. | ||
| /// Returns `true` when the caller queues the entry as a plain child. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// The walk lists `real` by itself: it is the scan root, or it sits below | ||
| /// the root and no directory on the way is pruned. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// The part of `path` below `root`, without a leading separator. Empty when | ||
| /// both name the same place. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
Problem
bun testnever exits when two directory symlinks lead back up the tree (ln -s . x && ln -s . y). Memory and open fds grow with no bound: 363 MB and 170,855 fds after 6 s.Scanner::next(src/runtime/cli/test/Scanner.rs) queues a symlinked directory like a real one, so the walk lists a directory once per path to it.Fix
Entry::link(src/resolver/fs.rs) returns the kind, a new link bit, and the real pathRealFS::kindalready reads.bun test latest/,latest -> packages/v2) matches nothing now.bun test ./lateststill works.directory linkstests intest/cli/test/bun-test.test.ts. Bun 1.4.3 fails 17.Background
Scanner::nextper entry.RealFS::kindstats an entry on first use and stores a symlink's real path.O_NOFOLLOW: a test that only a link leads to stops running. Considered a visited (device, inode) set (bun test: report directories the scanner cannot read, scan each directory once #41465): readdir order decides which path records a file.Downsides
Bun.FileSystemRouterhas the same walk (src/router/lib.rs). Two aliasing path roots still record a file twice (bun test: list a test file once when two path arguments select it #43361).Notes
Question for a maintainer. The walk records a directory that only a link leads to under its real path. A substring filter or a file ignore pattern that names only the link path stops matching. Is that acceptable? If not, a filter pass that runs after the walk, only on trees with links, can keep both names (about 55 more lines). The other way, one listing per link path, brings back the growth this PR removes.
The rule, in the directory arm of
Scanner::next, after the existing prunes.The resolver can hold a listing of a followed directory from before discovery (a parent of the cwd). The read then calls nothing, so the walk replays the cached listing, as it already does for the root.
Numbers: Bun 1.4.3 (367d939) against this branch, same machine. Each line is the
Ran N tests across M filesresult.x -> .andy -> .ulimit -n 64)examples/one/lib -> ../..andexamples/two/lib -> ../..a/loop -> ..andalias -> aa/loop -> ..sub/loop -> ..andsub/deeper/self -> .root = "tests"in bunfig.toml,tests/up -> ..a/loop -> ..with--isolateor--parallel=2shared -> ../outside(only route)pkg -> .store/pkg(only route)Filters and ignore patterns, tree
proj/shared -> ../outside, cwdproj.bun test shared/: 1.4.3 runs the test. This branch matches nothing and exits 1.bun test outside/: 1.4.3 matches nothing. This branch runs the test.bun test ./shared: both run the test.--path-ignore-patterns 'shared/**'and'shared': both prune the link.--path-ignore-patterns 'shared/*.test.js': 1.4.3 ignores the file. This branch runs it.--path-ignore-patterns '../outside/**': 1.4.3 runs the file. This branch ignores it.The 8 new tests that pass on 1.4.3 too. They pin what must not change: a test that only a link leads to still runs, a filter on the real path still matches, and a link that is the scan root still works. A walk that opens each directory with
O_NOFOLLOWcannot reach the directories of the first group.Why 1.4.0 ended. Before #40016 (in 1.4.1, not in 1.4.0) the walk held one fd for each directory, so it ended when no file descriptor was left.
What I ran. On a debug build of this diff on base bc7a813:
test/cli/test/bun-test.test.ts(123 pass, 6 todo, 0 fail),path-ignore-patterns.test.ts(10 pass),test-shard.test.ts(29 pass),pass-with-no-tests.test.ts(5 pass),test/regression/issue/26851.test.ts(2 pass).cargo checkforx86_64-pc-windows-msvcandaarch64-apple-darwin, andcargo clippyforbun_resolverandbun_runtime. The branch was then rebased on 519963e with no conflict. I did not run the Windows rows: I have no Windows machine. They use junctions.Known limits.
chmod 111): the walk skips the link as covered, and cannot list that directory by itself.get_fd_pathand the real path fromRealFS::kind. After it, the scanner's own lookup names every link target (open, fd path, close: 3 syscalls per directory link, 0 on a tree with no links), andreal_path_ofmoves tobun_sys::realpath.bust_dir_cache_recursive(src/runtime/api/filesystem_router.rs) andFrameworkRouter::scan_inner(src/runtime/bake/FrameworkRouter.rs).The reproductions come from #41465. That PR keeps only its other half, the report of a directory that cannot be read.