Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
9e9213a
fix(fspy): replace quiescence locking with crash-tolerant frame publi…
wan9chi Aug 14, 2026
2f5d389
docs: add changelog entry for crash-tolerant file-access tracking
wan9chi Aug 14, 2026
d2fa1de
temp(fspy): benchmark phase instrumentation
wan9chi Aug 14, 2026
ab2749f
Revert "temp(fspy): benchmark phase instrumentation"
wan9chi Aug 14, 2026
b1babb7
perf(fspy): keep first-touch allocation and unmapping off the launch …
wan9chi Aug 14, 2026
3a84bc0
perf(fspy): make frame claims wait-free and channel close write-free
wan9chi Aug 14, 2026
b53a3fb
perf(fspy): restore the header pre-fault on Linux only
wan9chi Aug 14, 2026
528d1d1
perf(fspy): preallocate first-touch blocks instead of pre-faulting on…
wan9chi Aug 14, 2026
3e5bea8
Revert "perf(fspy): preallocate first-touch blocks instead of pre-fau…
wan9chi Aug 14, 2026
c4ba8dd
perf(fspy): warm the first payload page alongside the header
wan9chi Aug 14, 2026
916e292
Revert "perf(fspy): warm the first payload page alongside the header"
wan9chi Aug 14, 2026
a5c8ec3
refactor(fspy): borrow committed frames from the mapping instead of c…
wan9chi Aug 15, 2026
d91b2fb
refactor(fspy): rename CollectedAccesses and drop its async wrapper
wan9chi Aug 15, 2026
0a80d2b
docs(fspy-shm): add a protocol README to shm_io
wan9chi Aug 15, 2026
3336edf
docs(fspy-shm): keep shm_io usage-agnostic
wan9chi Aug 15, 2026
2d6aafa
docs(fspy-shm): say u64 and AtomicU64 instead of word
wan9chi Aug 15, 2026
f2ac098
refactor(fspy-shm): fix the channel layout at compile time
wan9chi Aug 15, 2026
7a4c950
refactor(fspy-shm): dynamic sizing with typed region views
wan9chi Aug 15, 2026
907a55d
docs(fspy-shm): update the README for dynamic typed-view sizing
wan9chi Aug 15, 2026
c07dea9
docs(fspy-shm): plainer wording in the README
wan9chi Aug 15, 2026
1497e51
refactor(fspy-shm): derive completeness from counter overshoot
wan9chi Aug 15, 2026
47ce474
refactor(fspy): one skip-on-failure send where the usage lives
wan9chi Aug 15, 2026
46871b9
refactor(fspy-shm): consolidate to three files
wan9chi Aug 15, 2026
fad9c0b
refactor(fspy-shm): report lost records with a header flag
wan9chi Aug 15, 2026
f063237
refactor(fspy-shm): derive the header size from the header struct
wan9chi Aug 15, 2026
db554cd
refactor(fspy-shm): drop code the simplifications left behind
wan9chi Aug 15, 2026
df8b8a7
refactor(fspy-shm): decode descriptors straight to validated spans
wan9chi Aug 15, 2026
9d5e745
refactor(fspy-shm): wrap the receiver end in a type like the writer's
wan9chi Aug 15, 2026
8090249
refactor(fspy-shm): fold shared.rs into mod.rs
wan9chi Aug 15, 2026
b17bad9
refactor(fspy-shm): move each side's operations into that side
wan9chi Aug 15, 2026
a6f4ddd
refactor(fspy-shm): merge the receiver and Frames into ShmReader
wan9chi Aug 15, 2026
fa17277
refactor(fspy-shm): make the CLOSED gate the loss report
wan9chi Aug 15, 2026
268ffe9
refactor(fspy-shm): iterate the frozen table instead of snapshotting …
wan9chi Aug 15, 2026
080f40f
refactor(fspy-shm): build the region views once, at attach
wan9chi Aug 15, 2026
effa74e
refactor(fspy-shm): split the sides apart over a shared layout
wan9chi Aug 15, 2026
8e18c62
refactor(fspy-shm): drop the stored mapping length
wan9chi Aug 15, 2026
d449bc5
refactor(fspy-shm): support only regular region lengths
wan9chi Aug 15, 2026
1702a53
refactor(fspy-shm): payload-relative descriptors; unshare the codec h…
wan9chi Aug 15, 2026
f1127ed
refactor(fspy-shm): rename the reader's constructor to seal
wan9chi Aug 15, 2026
cd41498
refactor(fspy-shm): fix the table length at compile time
wan9chi Aug 15, 2026
91e4d7b
refactor(fspy-shm): claim exact lengths; drop the alignment fossil
wan9chi Aug 15, 2026
c0a6ab9
refactor(fspy-shm): capacity is the region length again
wan9chi Aug 15, 2026
612e93e
refactor(fspy-shm): flatten the counters into Meta; drop the reserve
wan9chi Aug 15, 2026
2ce915c
refactor(fspy-shm): parse descriptor fields, don't validate them
wan9chi Aug 15, 2026
21ac689
refactor(fspy-shm): give the length field its full 32 bits
wan9chi Aug 15, 2026
d5aaec3
refactor(fspy-shm): declare the views before the region that owns them
wan9chi Aug 15, 2026
4cdb7e6
refactor(fspy-shm): validate inside the constructors
wan9chi Aug 15, 2026
99f5093
refactor(fspy-shm): underscore the owner fields; drop into_memory
wan9chi Aug 15, 2026
27a1cfe
refactor(fspy-shm): parse slots into SlotState
wan9chi Aug 15, 2026
20522b9
refactor(fspy-shm): net-zero refusals make the gate bit unreachable
wan9chi Aug 16, 2026
70eb9c4
docs(fspy-shm): tighten the comments
wan9chi Aug 16, 2026
eafdf80
refactor(fspy-shm): exact width conversions
wan9chi Aug 16, 2026
199dc44
refactor(fspy-shm): one bounds check, one cast
wan9chi Aug 16, 2026
9578fad
refactor(fspy-shm): seal without walking the table
wan9chi Aug 16, 2026
f0a19e2
fix(fspy): restore the SHM_CAPACITY import on Windows
wan9chi Aug 16, 2026
268c6e8
refactor(fspy-shm): name the payload pointer, drop the dead parameters
wan9chi Aug 16, 2026
4480826
refactor(fspy-shm): the reader keeps no layout and no SLOTS
wan9chi Aug 16, 2026
9911f84
refactor(fspy-shm): mark the frame iterator fused
wan9chi Aug 16, 2026
beb784b
fix(fspy): drop the unused wincode dependency
wan9chi Aug 16, 2026
ca73296
refactor(fspy-shm): counters give up rather than wrap
wan9chi Aug 16, 2026
7968ab4
fix(fspy): update Cargo.lock for the dropped dependency
wan9chi Aug 16, 2026
b84ea5c
Revert "refactor(fspy-shm): counters give up rather than wrap"
wan9chi Aug 16, 2026
43e894f
fix(fspy-shm): report the losses only the writer can see
wan9chi Aug 17, 2026
2d57f12
fix(fspy-shm): build the round-trip test's paths per platform
wan9chi Aug 17, 2026
954c991
perf(fspy-shm): drop the pre-fault thread
wan9chi Aug 17, 2026
1ffd6e2
refactor(fspy-shm): seal with one swap
wan9chi Aug 17, 2026
3b7a543
docs(fspy-shm): cut the protocol docs down
wan9chi Aug 17, 2026
6b61b84
docs(fspy-shm): stop hard-wrapping the README
wan9chi Aug 17, 2026
e0894be
docs(fspy-shm): say why the seal clamps its slot count
wan9chi Aug 17, 2026
f3db8e8
docs(fspy-shm): note why an overshot count is not an error
wan9chi Aug 17, 2026
96abd84
refactor(fspy-shm): split what the channel reports from what it panic…
wan9chi Aug 17, 2026
30588aa
refactor(fspy-shm): state the failures as expects
wan9chi Aug 17, 2026
af3d07e
perf(fspy-shm): report a loss with a store
wan9chi Aug 17, 2026
08150ee
refactor(fspy-shm): let the table's own bounds decide
wan9chi Aug 17, 2026
5ca10c9
feat(fspy): let the caller size the tracking channel
wan9chi Aug 17, 2026
a5e27a9
refactor(fspy-shm): choose the slot count at run time
wan9chi Aug 17, 2026
25dd95a
Merge branch 'claude/fspy-shm-capacity-env' into shm publication design
wan9chi Aug 17, 2026
a267ab1
Merge branch 'claude/fspy-shm-capacity-env' into shm publication design
wan9chi Aug 17, 2026
b5eb0eb
Merge branch 'claude/fspy-shm-capacity-env' into shm publication design
wan9chi Aug 17, 2026
836bdcb
Merge branch 'claude/fspy-shm-capacity-env' into shm publication design
wan9chi Aug 17, 2026
88216bc
Merge branch 'claude/fspy-shm-capacity-env', and make record loss an …
wan9chi Aug 17, 2026
fe70860
docs: write the tracking changelog entries for users
wan9chi Aug 17, 2026
b17256b
refactor(fspy): let the caller decide what an attach failure means
wan9chi Aug 17, 2026
aa075b2
refactor(fspy-shm): state the table's alignment instead of implying it
wan9chi Aug 17, 2026
eb5e2ec
refactor(fspy): let sender say whether there is a sender
wan9chi Aug 17, 2026
97a6c40
test(fspy): pin the check that lets the rest of the channel assume a …
wan9chi Aug 17, 2026
a9a30a4
refactor(vt): check the tracking error where the accesses are taken
wan9chi Aug 17, 2026
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Changelog

- **Fixed** `vp run` no longer hangs or fails when a task leaves a process running behind it, such as a dev server or a background helper, or when one of a task's processes is killed. The run finishes as soon as the task itself does, and the files the task used are still recorded ([#544](https://github.com/voidzero-dev/vite-task/issues/544), [#675](https://github.com/voidzero-dev/vite-task/pull/675)).
- **Fixed** A task that reads or writes an unusually large number of files now runs to the end instead of being killed partway through. Vite+ reports the run as not cached, because it could not record every file the task used ([#533](https://github.com/voidzero-dev/vite-task/issues/533), [#675](https://github.com/voidzero-dev/vite-task/pull/675)).
- **Fixed** Vite+ diagnostics now display individual paths and working directories without Rust debug formatting such as quoted paths or escaped Windows backslashes ([#534](https://github.com/voidzero-dev/vite-task/pull/534)).
- **Fixed** Automatic file-access tracking now works inside the default Codex CLI and Claude Code sandboxes ([#562](https://github.com/voidzero-dev/vite-task/issues/562), [#563](https://github.com/voidzero-dev/vite-task/issues/563), [#576](https://github.com/voidzero-dev/vite-task/pull/576), [#569](https://github.com/voidzero-dev/vite-task/pull/569)).
- **Fixed** Broad workspace globs no longer discover and run package scripts inside `node_modules` ([#539](https://github.com/voidzero-dev/vite-task/pull/539)).
Expand Down
2 changes: 0 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion crates/fspy/examples/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ async fn main() -> anyhow::Result<()> {

let mut csv_writer = csv_async::AsyncWriter::from_writer(out_file);

for acc in termination.path_accesses.iter() {
for acc in termination
.path_accesses
.expect("the tracking region holds every record this run makes")
.iter()
{
path_count += 1;
let path_str = format!("{:?}", acc.path);
let mode_str = format!("{:?}", acc.mode);
Expand Down
12 changes: 12 additions & 0 deletions crates/fspy/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,15 @@ pub enum SpawnError {
#[error("underlying os error: {0}")]
OsSpawn(std::io::Error),
}

/// A tracked process could not record a file access it went on to perform,
/// so the accesses collected for the run are a subset of what it really
/// touched.
///
/// The run itself is unaffected: recording must never stop the program
/// doing the work. What cannot be done is anything that needs every
/// access, caching above all, which has to treat the run as untracked
/// rather than as having touched only the paths that fit.
#[derive(thiserror::Error, Clone, Copy, PartialEq, Eq, Debug)]
#[error("the file-access records did not fit in the tracking channel")]
pub struct TrackingIncomplete;
63 changes: 37 additions & 26 deletions crates/fspy/src/ipc.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use std::io;

use fspy_shared::ipc::{
PathAccess,
channel::{Receiver, ReceiverLockGuard},
ChannelSize, PathAccess,
channel::{FrameReader, Receiver},
};
use tokio::task::spawn_blocking;

use crate::error::TrackingIncomplete;

/// Shared memory for one tracked run's file-access records.
///
Expand All @@ -18,43 +17,55 @@ const DEFAULT_SHM_CAPACITY: usize = 4 * 1024 * 1024 * 1024;
/// nothing outside this repository should set it.
const SHM_CAPACITY_ENV: &str = "VP_RUN_INTERNAL_FSPY_SHM_CAPACITY";

/// How much shared memory to give the next tracked run.
/// How much shared memory to give the next tracked run, split at one
/// record per 64 bytes: 8 for its descriptor and 56 for its payload.
/// Records run a few hundred bytes each, so payload space runs out well
/// before slots do.
///
/// # Panics
///
/// When the override is set to something that is not a byte count. It is
/// ours to set, so a value we cannot read is a mistake worth stopping for
/// rather than quietly ignoring.
pub fn shm_capacity() -> usize {
std::env::var_os(SHM_CAPACITY_ENV).map_or(DEFAULT_SHM_CAPACITY, |value| {
pub fn shm_size() -> ChannelSize {
let capacity = std::env::var_os(SHM_CAPACITY_ENV).map_or(DEFAULT_SHM_CAPACITY, |value| {
value.to_str().and_then(|value| value.parse().ok()).unwrap_or_else(|| {
panic!("{SHM_CAPACITY_ENV} is not a byte count: {}", value.display())
})
})
});
ChannelSize { capacity, slots: capacity / 64 }
}

#[ouroboros::self_referencing]
pub struct OwnedReceiverLockGuard {
/// Owns the shared memory
receiver: Receiver,
/// Borrows the shared memory and owns the file lock
#[borrows(receiver)]
#[covariant]
lock_guard: ReceiverLockGuard<'this>,
/// The path accesses a run reported through the IPC channel.
pub struct ChannelAccesses {
frames: FrameReader,
}

impl OwnedReceiverLockGuard {
pub fn lock(receiver: Receiver) -> io::Result<Self> {
Self::try_new(receiver, fspy_shared::ipc::channel::Receiver::lock)
}
impl TryFrom<Receiver> for ChannelAccesses {
type Error = TrackingIncomplete;

pub async fn lock_async(receiver: Receiver) -> io::Result<Self> {
spawn_blocking(move || Self::lock(receiver)).await.expect("lock task panicked")
/// Closes the channel and takes every record it collected.
///
/// Never waits for tracked processes: closing reads one counter and
/// shuts the channel's gate (see
/// [`fspy_shared::ipc::channel::Receiver::close`]), so it runs inline
/// however many records were reported.
///
/// # Errors
///
/// [`TrackingIncomplete`] when a tracked process could not record
/// something it went on to do. What did arrive is then a subset of
/// what the run really touched, so none of it is handed back.
fn try_from(receiver: Receiver) -> Result<Self, TrackingIncomplete> {
Ok(Self { frames: receiver.close().map_err(|_| TrackingIncomplete)? })
}
}

impl ChannelAccesses {
pub fn iter_path_accesses(&self) -> impl Iterator<Item = PathAccess<'_>> {
self.borrow_lock_guard()
.iter_frames()
.map(|frame| wincode::deserialize_exact(frame).unwrap())
self.frames.iter().map(|frame| {
wincode::deserialize_exact(frame)
.expect("committed frames are complete under the channel protocol")
})
}
}
6 changes: 4 additions & 2 deletions crates/fspy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod command;
use std::{env::temp_dir, fs::create_dir, io, process::ExitStatus, sync::LazyLock};

pub use command::Command;
pub use error::TrackingIncomplete;
pub use fspy_shared::ipc::{AccessMode, PathAccess};
use futures_util::future::BoxFuture;
pub use os_impl::PathAccessIterable;
Expand All @@ -30,8 +31,9 @@ use tokio::process::{ChildStderr, ChildStdin, ChildStdout};
pub struct ChildTermination {
/// The exit status of the child process.
pub status: ExitStatus,
/// The path accesses captured from the child process.
pub path_accesses: PathAccessIterable,
/// The path accesses captured from the child process, or the reason
/// they cannot be trusted to be all of them.
pub path_accesses: Result<PathAccessIterable, TrackingIncomplete>,
}

pub struct TrackedChild {
Expand Down
24 changes: 11 additions & 13 deletions crates/fspy/src/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use tokio::task::spawn_blocking;
use tokio_util::sync::CancellationToken;

#[cfg(not(target_env = "musl"))]
use crate::ipc::OwnedReceiverLockGuard;
use crate::ipc::ChannelAccesses;
use crate::{ChildTermination, Command, TrackedChild, arena::PathAccessArena, error::SpawnError};

#[derive(Debug)]
Expand Down Expand Up @@ -80,7 +80,7 @@ impl SpyImpl {

#[cfg(not(target_env = "musl"))]
let (ipc_channel_conf, ipc_receiver) =
channel(crate::ipc::shm_capacity()).map_err(SpawnError::ChannelCreation)?;
channel(crate::ipc::shm_size()).map_err(SpawnError::ChannelCreation)?;

let payload = Payload {
#[cfg(not(target_env = "musl"))]
Expand Down Expand Up @@ -137,7 +137,7 @@ impl SpyImpl {
stdout: child.stdout.take(),
stderr: child.stderr.take(),
// Keep polling for the child to exit in the background even if `wait_handle` is not awaited,
// because we need to stop the supervisor and lock the channel as soon as the child exits.
// because we need to stop the supervisor and close the channel as soon as the child exits.
wait_handle: tokio::spawn(async move {
let status = tokio::select! {
status = child.wait() => status?,
Expand All @@ -159,16 +159,14 @@ impl SpyImpl {
);
let arenas = arenas.collect::<Vec<_>>();

// Lock the ipc channel after the child has exited.
// Close the ipc channel after the child has exited.
// We are not interested in path accesses from descendants after the main child has exited.
#[cfg(not(target_env = "musl"))]
let ipc_receiver_lock_guard =
OwnedReceiverLockGuard::lock_async(ipc_receiver).await?;
let path_accesses = PathAccessIterable {
arenas,
#[cfg(not(target_env = "musl"))]
ipc_receiver_lock_guard,
};
#[cfg(not(target_env = "musl"))]
let path_accesses = ChannelAccesses::try_from(ipc_receiver)
.map(|ipc_accesses| PathAccessIterable { arenas, ipc_accesses });
#[cfg(target_env = "musl")]
let path_accesses = Ok(PathAccessIterable { arenas });

io::Result::Ok(ChildTermination { status, path_accesses })
})
Expand All @@ -181,7 +179,7 @@ impl SpyImpl {
pub struct PathAccessIterable {
arenas: Vec<PathAccessArena>,
#[cfg(not(target_env = "musl"))]
ipc_receiver_lock_guard: OwnedReceiverLockGuard,
ipc_accesses: ChannelAccesses,
}

impl PathAccessIterable {
Expand All @@ -191,7 +189,7 @@ impl PathAccessIterable {

#[cfg(not(target_env = "musl"))]
{
let accesses_in_shm = self.ipc_receiver_lock_guard.iter_path_accesses();
let accesses_in_shm = self.ipc_accesses.iter_path_accesses();
accesses_in_shm.chain(accesses_in_arena)
}
#[cfg(target_env = "musl")]
Expand Down
17 changes: 8 additions & 9 deletions crates/fspy/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,19 @@ use winapi::{
use winsafe::co::{CP, WC};

use crate::{
ChildTermination, TrackedChild, command::Command, error::SpawnError,
ipc::OwnedReceiverLockGuard,
ChildTermination, TrackedChild, command::Command, error::SpawnError, ipc::ChannelAccesses,
};

const INTERPOSE_CDYLIB: Artifact =
artifact!("fspy_preload", "CARGO_CDYLIB_FILE_FSPY_PRELOAD_WINDOWS");

pub struct PathAccessIterable {
ipc_receiver_lock_guard: OwnedReceiverLockGuard,
ipc_accesses: ChannelAccesses,
}

impl PathAccessIterable {
pub fn iter(&self) -> impl Iterator<Item = PathAccess<'_>> {
self.ipc_receiver_lock_guard.iter_path_accesses()
self.ipc_accesses.iter_path_accesses()
}
}

Expand Down Expand Up @@ -85,7 +84,7 @@ impl SpyImpl {
command.creation_flags(CREATE_SUSPENDED);

let (channel_conf, receiver) =
channel(crate::ipc::shm_capacity()).map_err(SpawnError::ChannelCreation)?;
channel(crate::ipc::shm_size()).map_err(SpawnError::ChannelCreation)?;

let mut spawn_success = false;
let spawn_success = &mut spawn_success;
Expand Down Expand Up @@ -156,7 +155,7 @@ impl SpyImpl {
stderr: child.stderr.take(),
process_handle,
// Keep polling for the child to exit in the background even if `wait_handle` is not awaited,
// because we need to stop the supervisor and lock the channel as soon as the child exits.
// because we need to stop the supervisor and close the channel as soon as the child exits.
wait_handle: tokio::spawn(async move {
let status = tokio::select! {
status = child.wait() => status?,
Expand All @@ -165,10 +164,10 @@ impl SpyImpl {
child.wait().await?
}
};
// Lock the ipc channel after the child has exited.
// Close the ipc channel after the child has exited.
// We are not interested in path accesses from descendants after the main child has exited.
let ipc_receiver_lock_guard = OwnedReceiverLockGuard::lock_async(receiver).await?;
let path_accesses = PathAccessIterable { ipc_receiver_lock_guard };
let path_accesses = ChannelAccesses::try_from(receiver)
.map(|ipc_accesses| PathAccessIterable { ipc_accesses });

io::Result::Ok(ChildTermination { status, path_accesses })
})
Expand Down
4 changes: 3 additions & 1 deletion crates/fspy/tests/node_fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ fn track_script(
let child = command.spawn(tokio_util::sync::CancellationToken::new()).await?;
let termination = child.wait_handle.await?;
assert!(termination.status.success());
Ok(termination.path_accesses)
Ok(termination
.path_accesses
.expect("the tracking region holds every record this run makes"))
})
}

Expand Down
2 changes: 1 addition & 1 deletion crates/fspy/tests/oxlint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ async fn track_oxlint(dir: &std::path::Path, args: &[&str]) -> anyhow::Result<Pa
let child = command.spawn(tokio_util::sync::CancellationToken::new()).await?;
let termination = child.wait_handle.await?;
// oxlint may return non-zero if it finds lint errors, that's OK
Ok(termination.path_accesses)
Ok(termination.path_accesses.expect("the test region holds every record"))
}

#[test(tokio::test)]
Expand Down
2 changes: 1 addition & 1 deletion crates/fspy/tests/static_executable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async fn track_test_bin(args: &[&str], cwd: Option<&str>) -> PathAccessIterable
let termination = tracked_child.wait_handle.await.unwrap();
assert!(termination.status.success());

termination.path_accesses
termination.path_accesses.expect("the tracking region holds every record this run makes")
}

#[test(tokio::test)]
Expand Down
2 changes: 1 addition & 1 deletion crates/fspy/tests/test_utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,5 @@ pub async fn spawn_command(cmd: subprocess_test::Command) -> anyhow::Result<Path
.wait_handle
.await?;
assert!(termination.status.success());
Ok(termination.path_accesses)
Ok(termination.path_accesses.expect("the test region holds every record"))
}
28 changes: 26 additions & 2 deletions crates/fspy_benchmark_launcher/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

use std::{env, ffi::OsString, process::Stdio, time::Instant};

use fspy::Command;
use fspy::{Command, PathAccessIterable};
use tokio::{io::AsyncReadExt as _, process::ChildStdout, runtime::Builder};
use tokio_util::sync::CancellationToken;

Expand Down Expand Up @@ -140,6 +140,30 @@ async fn report(mut launch: Launch) {
/// In relative mode the captured path must come out identical — the tracker
/// resolves the root working directory and joins the bare name back into
/// [`MISSING_PATH`] — so the assertion below covers both modes.
/// Takes the accesses out of a [`fspy::ChildTermination`], whichever shape
/// that field has.
///
/// The benchmark compiles this one source file against two revisions of
/// `fspy` — the pull request's and its merge base's — so that identical
/// launcher code times both arms. A revision that changes the field's type
/// would otherwise stop the other arm from building. Exactly one of these
/// impls applies per build; the other is inert.
trait TrackedAccesses {
fn tracked(self) -> PathAccessIterable;
}

impl TrackedAccesses for PathAccessIterable {
fn tracked(self) -> PathAccessIterable {
self
}
}

impl<E: std::fmt::Debug> TrackedAccesses for Result<PathAccessIterable, E> {
fn tracked(self) -> PathAccessIterable {
self.expect("the tracking region holds every record this run makes")
}
}

async fn validate(target: &OsString, target_args: &[OsString], relative: bool) {
let mut command = Command::new(target);
command
Expand All @@ -159,7 +183,7 @@ async fn validate(target: &OsString, target_args: &[OsString], relative: bool) {
.await
.expect("failed to wait for tracked target");
assert!(termination.status.success(), "benchmark target failed: {}", termination.status);
let captured_missing_access = termination.path_accesses.iter().any(|access| {
let captured_missing_access = termination.path_accesses.tracked().iter().any(|access| {
access.path.strip_path_prefix(MISSING_PATH, |result| {
result.is_ok_and(|path| path.as_os_str().is_empty())
})
Expand Down
1 change: 0 additions & 1 deletion crates/fspy_client_unix/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ libc = { workspace = true }
nix = { workspace = true, features = ["fs"] }
fspy_nostd = { workspace = true }
fspy_nostd_alloc = { workspace = true }
wincode = { workspace = true }

[target.'cfg(all(target_os = "linux", not(target_env = "musl")))'.dependencies]
itoa = { workspace = true }
Expand Down
Loading
Loading