Skip to content
Draft
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
15 changes: 15 additions & 0 deletions docs/test/discovery.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ By default, `bun test` ignores:
- Hidden directories (those starting with a period `.`)
- Files that don't have JavaScript-like extensions (based on available [loaders](/bundler/loaders))

## Symbolic Links

`bun test` follows symbolic links to directories, and it scans each directory once.

- `bun test` does not follow a link to a directory that the scan already covers. A link to a parent directory is one example.
- `bun test` scans a directory that only a link leads to under the real path of that directory.

Filters and [`pathIgnorePatterns`](/test/configuration#path-ignore-patterns) match the path that the scan recorded. For example, if `latest` is a link to `packages/v2`, then `bun test packages/v2` runs the tests in that directory, and `bun test latest` matches nothing.

To run the tests behind a link by the name of the link, pass the link as a path:

```bash terminal icon="terminal"
bun test ./latest
```

## Customizing Test Discovery

### Position Arguments as Filters
Expand Down
40 changes: 40 additions & 0 deletions src/resolver/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,26 @@ pub struct EntryCache {
/// don't make it bun.invalid_fd
pub fd: Fd,
pub(crate) kind: EntryKind,
/// 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.
Comment on lines +117 to +118

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

pub(crate) is_link: bool,
}

// `is_link` sits in what was padding after `kind`.
const _: () = assert!(
size_of::<EntryCache>()
== (size_of::<Interned>() + size_of::<Fd>() + size_of::<EntryKind>())
.next_multiple_of(align_of::<EntryCache>())
);

/// What a directory walker needs to know about an entry, from one read of its stat cache.
#[derive(Clone, Copy)]
pub struct EntryLink {
pub kind: EntryKind,
pub is_link: bool,
/// 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.
Comment on lines +134 to +135

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

pub real_path: &'static [u8],
}

// `cache` / `need_stat` are lazily populated by `Entry::kind` /
Expand Down Expand Up @@ -234,6 +254,25 @@ impl Entry {
self.cache().kind
}

/// [`Entry::kind`], plus whether the entry is a link and where it leads.
///
/// # Safety
/// Same contract as [`Entry::kind`].
Comment on lines +257 to +260

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

pub unsafe fn link<R: EntryKindResolver>(&self, fs: *mut R, store_fd: bool) -> EntryLink {
// SAFETY: forwarded; `kind` runs the stat-on-first-use protocol.
let _ = unsafe { self.kind(fs, store_fd) };
let cache = self.cache();
EntryLink {
kind: cache.kind,
is_link: cache.is_link,
real_path: if cache.is_link {
cache.symlink.as_bytes()
} else {
b""
},
}
}

///
/// # Safety
/// `fs` must point to a live `EntryKindResolver` (the process-global
Expand Down Expand Up @@ -537,6 +576,7 @@ impl DirEntry {
// store an arbitrary kind
kind: found_kind.unwrap_or(EntryKind::File),
fd: Fd::INVALID,
is_link: false,
}));
addr_of_mut!((*p).abs_path).write(Interned::EMPTY);
p
Expand Down
7 changes: 5 additions & 2 deletions src/resolver/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -710,8 +710,8 @@ pub mod fs {
// Canonical definitions live in `fs.rs` (mounted as `crate::fs_full`).
// Re-exported here so the public path `bun_resolver::fs::*` is preserved.
pub use crate::fs_full::{
DirEntry, DirEntryIterator, Entry, EntryCache, EntryKind, EntryKindResolver, EntryLookup,
FilenameStoreAppender, dir_entry,
DirEntry, DirEntryIterator, Entry, EntryCache, EntryKind, EntryKindResolver, EntryLink,
EntryLookup, FilenameStoreAppender, dir_entry,
};

use bun_core::Generation;
Expand Down Expand Up @@ -1377,6 +1377,7 @@ pub mod fs {
kind: EntryKind::File,
symlink: Interned::EMPTY,
fd: Fd::INVALID,
is_link: false,
};

let combo: [&[u8]; 2] = [dir_, base];
Expand Down Expand Up @@ -1414,6 +1415,7 @@ pub mod fs {
if !file.is_reparse_point {
return Ok(cache);
}
cache.is_link = true;

// For the realpath, open the path and let the kernel follow
// every hop, then `GetFinalPathNameByHandle` (same as libuv's
Expand Down Expand Up @@ -1538,6 +1540,7 @@ pub mod fs {
} else {
EntryKind::File
};
cache.is_link = is_symlink;
if !symlink.is_empty() {
cache.symlink =
Interned::from_static(FilenameStore::instance().append_slice(symlink)?);
Expand Down
Loading
Loading