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
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,96 @@ pub(crate) fn record() {
}

pub(crate) fn reset() {
#[cfg(unix)]
assert!(
is_isolated_process(),
"counter measurements must run in an isolated test process"
);
COUNT.store(0, Ordering::SeqCst);
}

pub(crate) fn count() -> usize {
COUNT.load(Ordering::SeqCst)
}

#[cfg(unix)]
const CHILD_TEST: &str = "BUZZ_LOGIN_SHELL_PROBE_TEST";

#[cfg(unix)]
fn is_isolated_process() -> bool {
std::thread::current()
.name()
.is_some_and(|name| std::env::var(CHILD_TEST).as_deref() == Ok(name))
}

/// Run a counter-owning test body alone in a fresh libtest process.
///
/// The counter deliberately stays process-wide: discovery's auth workers must
/// count too. A voluntary PATH lock cannot exclude unrelated, unlocked probe
/// callers in the full suite. Process isolation excludes those callers without
/// changing production code or requiring new workers to inherit test context.
/// Call while holding the PATH lock so the child inherits stable environment.
#[cfg(unix)]
pub(crate) fn run_in_isolated_process(test: impl FnOnce()) {
use std::process::Command;
use std::time::Duration;

let thread = std::thread::current();
let name = thread.name().expect("libtest names its test threads");
let completed = format!("completed isolated login-shell probe test: {name}");
if is_isolated_process() {
test();
// Receipt only after the assertion body returns, never on entry.
println!("{completed}");
return;
}

let mut command = Command::new(std::env::current_exe().expect("test executable"));
command
.args(["--exact", name, "--nocapture", "--test-threads=1"])
.env(CHILD_TEST, name);
let output = super::bounded_command::output_with_timeout(command, Duration::from_secs(300))
.expect("isolated probe test must finish within five minutes and the output cap");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success() && stdout.lines().any(|line| line.contains(&completed)),
"isolated probe test {name} failed or did not run: {}\n{stdout}\n{stderr}",
output.status
);
}

#[cfg(unix)]
#[test]
fn isolated_counter_excludes_parent_probes_but_counts_own_workers() {
let _guard = crate::managed_agents::lock_path_mutex();
let probe_on_worker = || {
std::thread::spawn(|| {
// A real production probe, independent of the shared PATH cache.
super::find_via_login_shell("buzz-absent-probe-isolation-xyzzy")
})
.join()
.expect("probe worker must finish")
};
if !is_isolated_process() {
assert!(probe_on_worker().is_none());
assert!(count() >= 1, "parent worker must reach the real probe");
}
run_in_isolated_process(|| {
// Before reset: unrelated parent probes must not enter this process.
assert_eq!(count(), 0, "the child must start with its own counter");
reset();
assert!(probe_on_worker().is_none());
});
if is_isolated_process() {
// Outside the closure so omitting its invocation cannot pass this test.
assert_eq!(count(), 1, "the child's assertion body and worker must run");
}
}

#[cfg(unix)]
#[test]
#[should_panic(expected = "counter measurements must run in an isolated test process")]
fn reset_rejects_unisolated_measurement() {
reset();
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,21 @@ fn forced_discovery_probes_auth_but_cheap_discovery_reuses_cached_status() {
#[cfg(unix)]
#[test]
fn cheap_discovery_reports_absent_before_any_forced_probe() {
let _path_guard = crate::managed_agents::lock_path_mutex();
super::super::login_shell_spawn_probe::run_in_isolated_process(
cheap_discovery_reports_absent_before_any_forced_probe_body,
);
}

#[cfg(unix)]
fn cheap_discovery_reports_absent_before_any_forced_probe_body() {
use crate::managed_agents::custom_harnesses::registry_test_lock;
use crate::managed_agents::discovery::{
clear_resolve_cache, discover_acp_runtimes_from, login_shell_spawn_probe,
};
use crate::managed_agents::{AcpAvailabilityStatus, AuthStatus};
use std::os::unix::fs::PermissionsExt;

let _path_guard = crate::managed_agents::lock_path_mutex();
let _registry_guard = registry_test_lock();

let dir = tempfile::tempdir().expect("tempdir");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,21 @@ fn resolve_command_prefers_buzz_managed_npm_shim_over_path() {
#[cfg(unix)]
#[test]
fn cheap_discovery_never_spawns_login_shell_even_when_cold() {
let _path_guard = crate::managed_agents::lock_path_mutex();
super::super::login_shell_spawn_probe::run_in_isolated_process(
cheap_discovery_never_spawns_login_shell_even_when_cold_body,
);
}

#[cfg(unix)]
fn cheap_discovery_never_spawns_login_shell_even_when_cold_body() {
use crate::managed_agents::custom_harnesses::registry_test_lock;
use crate::managed_agents::discovery::{
clear_resolve_cache, discover_acp_runtimes_from, login_shell_spawn_probe,
};
use std::fs;
use tempfile::tempdir;

// Serialize with every other test that spawns a login shell: the spawn
// counter and the PATH/login-shell caches are process-global.
let _path_guard = crate::managed_agents::lock_path_mutex();
let _registry = registry_test_lock();

// A custom harness whose command cannot resolve anywhere, so the resolver
Expand Down
Loading