Skip to content
Merged
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
61 changes: 25 additions & 36 deletions crates/forkd-controller/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ pub struct AppState {
/// associated snapshot files are unrecoverable.
#[cfg(target_os = "linux")]
pub live_in_flight: Mutex<HashMap<String, LiveBranchHandle>>,
/// Test-only owner of the scratch `TempDir` backing `snapshot_root`,
/// the registry file and `prewarm_scratch_dir`. Tying the directory's
/// lifetime to the state means dropping the last `Arc` reaps it; the
/// unit tests used to `std::mem::forget` the `TempDir` instead, which
/// orphaned one directory per construction on every `cargo test` run
/// (issue #273). Declared last so it drops after `registry`.
#[cfg(test)]
pub _tempdir: Option<tempfile::TempDir>,
}

/// Phase 6.4: handle for a background bulk-copy thread driving the
Expand Down Expand Up @@ -2784,25 +2792,34 @@ mod tests {
use axum::http::{Request, StatusCode};
use tower::ServiceExt;

fn test_state() -> SharedState {
/// `AppState` on a private scratch `TempDir` that the state itself owns,
/// so the directory is reaped when the last `Arc` goes away. Every path
/// the daemon writes to — registry file, `snapshot_root`, prewarm
/// scratch — lives inside it, so a test run leaves nothing behind in
/// `$TMPDIR` (issue #273).
fn test_state_with_cap(cap: usize) -> SharedState {
let td = tempfile::TempDir::new().unwrap();
let path = td.path().join("state.json");
let snapshot_root = td.path().join("snapshots");
// Leak the TempDir so it survives the test (Drop deletes the dir).
std::mem::forget(td);
let prewarm_scratch_dir = td.path().join("prewarm");
Arc::new(AppState {
registry: Registry::load_or_init(path).unwrap(),
live_vms: Mutex::new(HashMap::new()),
snapshot_root,
branch_in_flight: Mutex::new(HashSet::new()),
branch_sem: Arc::new(Semaphore::new(DEFAULT_BRANCH_CONCURRENCY)),
branch_concurrency_cap: DEFAULT_BRANCH_CONCURRENCY,
prewarm_scratch_dir: std::env::temp_dir().join("forkd-test-prewarm"),
branch_sem: Arc::new(Semaphore::new(cap)),
branch_concurrency_cap: cap,
prewarm_scratch_dir,
#[cfg(target_os = "linux")]
live_in_flight: Mutex::new(HashMap::new()),
_tempdir: Some(td),
})
}

fn test_state() -> SharedState {
test_state_with_cap(DEFAULT_BRANCH_CONCURRENCY)
}

#[test]
fn new_sandbox_id_uses_fixed_width_counter_suffix() {
let id = new_sandbox_id();
Expand Down Expand Up @@ -3500,21 +3517,7 @@ mod tests {
#[test]
fn branch_slot_global_cap_blocks() {
// Cap = 2 so the test stays deterministic. Reaches the 503 path.
let td = tempfile::TempDir::new().unwrap();
let path = td.path().join("state.json");
let snapshot_root = td.path().join("snapshots");
std::mem::forget(td);
let s = Arc::new(AppState {
registry: Registry::load_or_init(path).unwrap(),
live_vms: Mutex::new(HashMap::new()),
snapshot_root,
branch_in_flight: Mutex::new(HashSet::new()),
branch_sem: Arc::new(Semaphore::new(2)),
branch_concurrency_cap: 2,
prewarm_scratch_dir: std::env::temp_dir().join("forkd-test-prewarm"),
#[cfg(target_os = "linux")]
live_in_flight: Mutex::new(HashMap::new()),
});
let s = test_state_with_cap(2);
let _a = s.try_acquire_branch_slot("t1").unwrap();
let _b = s.try_acquire_branch_slot("t2").unwrap();
let err = s
Expand All @@ -3525,21 +3528,7 @@ mod tests {

#[test]
fn branch_slot_capacity_recovers_on_drop() {
let td = tempfile::TempDir::new().unwrap();
let path = td.path().join("state.json");
let snapshot_root = td.path().join("snapshots");
std::mem::forget(td);
let s = Arc::new(AppState {
registry: Registry::load_or_init(path).unwrap(),
live_vms: Mutex::new(HashMap::new()),
snapshot_root,
branch_in_flight: Mutex::new(HashSet::new()),
branch_sem: Arc::new(Semaphore::new(1)),
branch_concurrency_cap: 1,
prewarm_scratch_dir: std::env::temp_dir().join("forkd-test-prewarm"),
#[cfg(target_os = "linux")]
live_in_flight: Mutex::new(HashMap::new()),
});
let s = test_state_with_cap(1);
let a = s.try_acquire_branch_slot("t1").unwrap();
assert!(s.try_acquire_branch_slot("t2").is_err());
drop(a);
Expand Down
2 changes: 2 additions & 0 deletions crates/forkd-controller/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ pub async fn run_daemon(cfg: DaemonConfig) -> Result<()> {
prewarm_scratch_dir: cfg.prewarm_scratch_dir.clone(),
#[cfg(target_os = "linux")]
live_in_flight: Mutex::new(HashMap::new()),
#[cfg(test)]
_tempdir: None,
});

let auth_layer_cfg = auth_cfg.clone();
Expand Down
Loading