diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bfa3590a..6872de5b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 off-loop fsync agent). ### Fixed +- **Test-harness hygiene sweep: hardcoded `target/release/moon` paths and + unguarded child spawns across `tests/`.** 33 integration suites resolved + the moon server binary via a bare `./target/release/moon` default, a + `CARGO_MANIFEST_DIR`-relative guess, or a local `find_moon_binary()` copy + that never checked `CARGO_BIN_EXE_moon` — a stale binary of unknown + provenance on a shared checkout could silently run instead of the one + cargo actually built for the test; migrated to `common::find_moon_binary()` + (5 suites with a documented "skip gracefully when unbuilt" contract keep + their own `Option` resolver, with the same `CARGO_BIN_EXE_moon` + tier added ahead of the stale-path fallback). Separately, 6 suites + (`console_gateway_test`, `scan_fanout_multishard`, + `allocator_mimalloc_smoke`, `perf_v0112_arenas_cap`, + `replication_readonly_eval`, `replication_readonly_ws_mq`) held a bare + `Child` across many `assert!`/`.expect()` calls with cleanup only at the + end of the function — a mid-test panic orphaned the server, the same + shape behind issue #366's 667%-CPU incident — and now use the + kill-on-drop `MoonGuard` pattern from `tests/bgsave_startup_race.rs`. + Complex multi-restart harnesses (crash-recovery kill-9 cycles, Jepsen, + instance-lock, SIGTERM) were left untouched by design. - **`tests/bgsave_startup_race.rs` can no longer orphan its server on a mid-test panic** — the spawned child is now held by a kill-on-drop guard (the pattern from `tests/dir_deleted_degraded.rs`). An orphan from this diff --git a/tests/admin_auth_cors_ratelimit.rs b/tests/admin_auth_cors_ratelimit.rs index ac033c98a..995f73b96 100644 --- a/tests/admin_auth_cors_ratelimit.rs +++ b/tests/admin_auth_cors_ratelimit.rs @@ -43,7 +43,12 @@ impl Drop for Moon { } fn bin_path() -> std::path::PathBuf { - std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("target/release/moon") + // MOON_BIN -> CARGO_BIN_EXE_moon (cargo guarantees this exists for any + // test binary that references it) -> target/{release,debug} — see + // `common::find_moon_binary`. The bare `target/release/moon` guess this + // used to return unconditionally could silently pick a stale binary of + // unknown provenance on a shared checkout. + common::find_moon_binary() } /// GET `url` with the given headers, returning the raw response even for diff --git a/tests/allocator_mimalloc_smoke.rs b/tests/allocator_mimalloc_smoke.rs index aaa06443f..7f34ff111 100644 --- a/tests/allocator_mimalloc_smoke.rs +++ b/tests/allocator_mimalloc_smoke.rs @@ -62,6 +62,21 @@ fn wait_ready(port: u16) { panic!("moon did not become ready on port {port}"); } +/// Kill-on-drop guard: `wait_ready` and the redis-cli `.expect()` calls below +/// can all panic before the old manual `child.kill()` ran, orphaning the +/// server (task: test/harness-hygiene-sweep). See +/// tests/bgsave_startup_race.rs for the same pattern. +struct MoonGuard(Option); + +impl Drop for MoonGuard { + fn drop(&mut self) { + if let Some(mut child) = self.0.take() { + let _ = child.kill(); + let _ = child.wait(); + } + } +} + #[test] fn mimalloc_set_get_roundtrip() { if !redis_cli_available() { @@ -69,7 +84,8 @@ fn mimalloc_set_get_roundtrip() { return; } let port = 16401; - let mut child = spawn_moon(port); + let child = spawn_moon(port); + let child = MoonGuard(Some(child)); wait_ready(port); let set_out = Command::new("redis-cli") @@ -84,8 +100,7 @@ fn mimalloc_set_get_roundtrip() { .expect("GET failed to spawn"); let get_stdout = String::from_utf8_lossy(&get_out.stdout); - let _ = child.kill(); - let _ = child.wait(); + drop(child); // MoonGuard SIGKILLs + reaps assert!( set_stdout.contains("OK"), diff --git a/tests/aof_fsync_err_subscribe_ordering.rs b/tests/aof_fsync_err_subscribe_ordering.rs index f0d0ad30a..3767efd61 100644 --- a/tests/aof_fsync_err_subscribe_ordering.rs +++ b/tests/aof_fsync_err_subscribe_ordering.rs @@ -22,6 +22,8 @@ //! Requires the release binary at ./target/release/moon. //! The MOON_TEST_AOF_FSYNC_FAIL=1 env var is passed to the child process. +mod common; + use std::io::{BufRead, BufReader, Write}; use std::net::TcpStream; use std::path::PathBuf; @@ -46,7 +48,7 @@ fn spawn_moon_with_fsync_fail(port: u16, shards: u16) -> (Child, PathBuf) { let stderr_log = dir.join("moon.stderr.log"); let stdout_log = dir.join("moon.stdout.log"); - let child = Command::new("./target/release/moon") + let child = Command::new(common::find_moon_binary()) .args([ "--port", &port.to_string(), diff --git a/tests/aof_multidb_kill9.rs b/tests/aof_multidb_kill9.rs index 79d60c2af..ad0b62a1f 100644 --- a/tests/aof_multidb_kill9.rs +++ b/tests/aof_multidb_kill9.rs @@ -22,13 +22,20 @@ #![cfg(any(feature = "runtime-monoio", feature = "runtime-tokio"))] +mod common; + use std::io::{BufRead, BufReader, Write}; use std::net::TcpStream; use std::process::{Child, Command, Stdio}; use std::time::Duration; -fn moon_bin() -> String { - std::env::var("MOON_BIN").unwrap_or_else(|_| "./target/release/moon".to_string()) +fn moon_bin() -> std::path::PathBuf { + // Migrated off the bare `./target/release/moon` default (task: + // test/harness-hygiene-sweep) — a stale target/release/moon of unknown + // provenance would silently run the wrong binary. See + // `common::find_moon_binary` for the MOON_BIN -> CARGO_BIN_EXE_moon -> + // target/{release,debug} precedence. + common::find_moon_binary() } fn start_moon(port: u16, dir: &str, shards: usize) -> Child { diff --git a/tests/aof_toplevel_multishard_refusal.rs b/tests/aof_toplevel_multishard_refusal.rs index daaf9204b..45da4dfe6 100644 --- a/tests/aof_toplevel_multishard_refusal.rs +++ b/tests/aof_toplevel_multishard_refusal.rs @@ -13,6 +13,8 @@ //! //! Requires the release binary at ./target/release/moon. +mod common; + use std::fs; use std::io::Read as _; use std::path::PathBuf; @@ -70,7 +72,7 @@ fn toplevel_manifest_with_multishard_exits_2_and_prints_refusing_to_start() { let stderr_log = dir.join("moon.stderr.log"); let stdout_log = dir.join("moon.stdout.log"); - let mut child = Command::new("./target/release/moon") + let mut child = Command::new(common::find_moon_binary()) .args([ "--port", "17399", // high port unlikely to clash @@ -156,7 +158,7 @@ fn toplevel_manifest_with_single_shard_is_allowed() { let stderr_log = dir.join("moon.stderr.log"); let stdout_log = dir.join("moon.stdout.log"); - let mut child = Command::new("./target/release/moon") + let mut child = Command::new(common::find_moon_binary()) .args([ "--port", "17400", diff --git a/tests/bgsave_startup_race.rs b/tests/bgsave_startup_race.rs index a0298860f..2c9ecae71 100644 --- a/tests/bgsave_startup_race.rs +++ b/tests/bgsave_startup_race.rs @@ -94,7 +94,7 @@ fn bgsave_issued_during_shard_startup_still_completes() { // guard instead of orphaning the server. An orphan from THIS suite is // what produced the 667%-CPU incident behind issue #366 (leaked child, // tmpdir later cleaned, pre-fix binary error-looping its tick). - let mut child = MoonGuard(Some(child)); + let child = MoonGuard(Some(child)); let mut c = Conn::open(port); // PING to confirm command plane is up, then fire BGSAVE immediately — diff --git a/tests/cold_shadow_overwrite_resurrection.rs b/tests/cold_shadow_overwrite_resurrection.rs index b466e5f39..5819c2b38 100644 --- a/tests/cold_shadow_overwrite_resurrection.rs +++ b/tests/cold_shadow_overwrite_resurrection.rs @@ -46,6 +46,8 @@ #![cfg(any(feature = "runtime-monoio", feature = "runtime-tokio"))] +mod common; + use std::io::Write; use std::process::{Child, Command, Stdio}; use std::time::Duration; @@ -95,7 +97,7 @@ fn unique_dir(suffix: &str) -> std::path::PathBuf { fn start_moon(port: u16, dir: &std::path::Path) -> Child { let off_dir = dir.join("off"); std::fs::create_dir_all(&off_dir).expect("create off dir"); - Command::new("./target/release/moon") + Command::new(common::find_moon_binary()) .args([ "--port", &port.to_string(), diff --git a/tests/console_gateway_test.rs b/tests/console_gateway_test.rs index c40a732c9..e67ae2b42 100644 --- a/tests/console_gateway_test.rs +++ b/tests/console_gateway_test.rs @@ -7,11 +7,13 @@ #![cfg(feature = "console")] +mod common; + use std::process::{Child, Command}; use std::time::Duration; fn start_server() -> Child { - Command::new("./target/release/moon") + Command::new(common::find_moon_binary()) .args(["--port", "16399", "--admin-port", "16400", "--shards", "2"]) .spawn() .expect("Failed to start moon server (build with --release --features console,graph first)") @@ -44,10 +46,26 @@ fn curl_json(method: &str, url: &str, body: Option<&str>) -> (u16, String) { (status, resp_body) } +/// Kill-on-drop guard: every assert! below panics through the guard instead +/// of orphaning the server (task: test/harness-hygiene-sweep — a leaked +/// child from a suite exactly this shape produced the 667%-CPU incident +/// behind issue #366). See tests/bgsave_startup_race.rs for the same +/// pattern. +struct MoonGuard(Option); + +impl Drop for MoonGuard { + fn drop(&mut self) { + if let Some(mut child) = self.0.take() { + common::sigkill(&mut child); + } + } +} + #[test] #[ignore] // Requires a built release binary; run explicitly fn test_rest_api_endpoints() { - let mut server = start_server(); + let server = start_server(); + let server = MoonGuard(Some(server)); wait_for_server(16400); // 1. POST /api/v1/command -- SET a key @@ -135,7 +153,7 @@ fn test_rest_api_endpoints() { body ); - // Cleanup - server.kill().ok(); - server.wait().ok(); + // MoonGuard SIGKILLs + reaps on drop (end of scope, or on an earlier + // panic from any assert! above). + drop(server); } diff --git a/tests/crash_matrix_per_shard_aof.rs b/tests/crash_matrix_per_shard_aof.rs index 5e5127c33..e31f1b60b 100644 --- a/tests/crash_matrix_per_shard_aof.rs +++ b/tests/crash_matrix_per_shard_aof.rs @@ -33,6 +33,8 @@ #![cfg(any(feature = "runtime-monoio", feature = "runtime-tokio"))] +mod common; + use std::process::{Child, Command, Stdio}; use std::time::Duration; @@ -85,7 +87,7 @@ fn start_moon(port: u16, dir: &std::path::Path) -> Child { } fn start_moon_with_fsync(port: u16, dir: &std::path::Path, fsync: &str) -> Child { - Command::new("./target/release/moon") + Command::new(common::find_moon_binary()) .args([ "--port", &port.to_string(), diff --git a/tests/crash_matrix_per_shard_bgrewriteaof.rs b/tests/crash_matrix_per_shard_bgrewriteaof.rs index 863b371f4..31dbd2d48 100644 --- a/tests/crash_matrix_per_shard_bgrewriteaof.rs +++ b/tests/crash_matrix_per_shard_bgrewriteaof.rs @@ -26,6 +26,8 @@ #![cfg(any(feature = "runtime-monoio", feature = "runtime-tokio"))] +mod common; + use std::process::{Child, Command, Stdio}; use std::time::Duration; @@ -51,7 +53,7 @@ fn unique_dir(suffix: &str) -> std::path::PathBuf { } fn start_moon(port: u16, dir: &std::path::Path) -> Child { - Command::new("./target/release/moon") + Command::new(common::find_moon_binary()) .args([ "--port", &port.to_string(), diff --git a/tests/crash_recovery_cold_del_resurrection.rs b/tests/crash_recovery_cold_del_resurrection.rs index 362b233cc..3c88b859c 100644 --- a/tests/crash_recovery_cold_del_resurrection.rs +++ b/tests/crash_recovery_cold_del_resurrection.rs @@ -37,6 +37,8 @@ #![cfg(any(feature = "runtime-monoio", feature = "runtime-tokio"))] +mod common; + use std::io::Write; use std::process::{Child, Command, Stdio}; use std::time::Duration; @@ -83,7 +85,7 @@ fn unique_dir(suffix: &str) -> std::path::PathBuf { fn start_moon(port: u16, dir: &std::path::Path) -> Child { let off_dir = dir.join("off"); std::fs::create_dir_all(&off_dir).expect("create off dir"); - Command::new("./target/release/moon") + Command::new(common::find_moon_binary()) .args([ "--port", &port.to_string(), diff --git a/tests/crash_recovery_disk_offload_no_aof.rs b/tests/crash_recovery_disk_offload_no_aof.rs index e36af0157..8b0ebe338 100644 --- a/tests/crash_recovery_disk_offload_no_aof.rs +++ b/tests/crash_recovery_disk_offload_no_aof.rs @@ -83,6 +83,8 @@ #![cfg(any(feature = "runtime-monoio", feature = "runtime-tokio"))] +mod common; + use std::collections::HashSet; use std::io::Write; use std::process::{Child, Command, Stdio}; @@ -134,7 +136,7 @@ fn unique_dir(suffix: &str) -> std::path::PathBuf { fn start_moon(port: u16, dir: &std::path::Path) -> Child { let off_dir = dir.join("off"); std::fs::create_dir_all(&off_dir).expect("create off dir"); - Command::new("./target/release/moon") + Command::new(common::find_moon_binary()) .args([ "--port", &port.to_string(), diff --git a/tests/crash_recovery_graph_durability.rs b/tests/crash_recovery_graph_durability.rs index c978edaec..c1c989932 100644 --- a/tests/crash_recovery_graph_durability.rs +++ b/tests/crash_recovery_graph_durability.rs @@ -36,6 +36,8 @@ #![cfg(any(feature = "runtime-monoio", feature = "runtime-tokio"))] #![allow(clippy::unwrap_used)] +mod common; + use std::collections::BTreeSet; use std::io::{BufReader, Read, Write}; use std::net::{TcpStream, ToSocketAddrs}; @@ -44,29 +46,14 @@ use std::process::{Child, Command}; use std::time::{Duration, Instant}; // --------------------------------------------------------------------------- -// Binary resolution (pattern: tests/crash_recovery_vector_durability.rs) +// Binary resolution — delegates to common::find_moon_binary (task: +// test/harness-hygiene-sweep). The old local copy stopped at +// target/{release,debug}/moon and never checked CARGO_BIN_EXE_moon, the +// binary cargo actually built for THIS test run. // --------------------------------------------------------------------------- fn find_moon_binary() -> PathBuf { - if let Ok(bin) = std::env::var("MOON_BIN") { - let p = PathBuf::from(bin); - if p.exists() { - return p; - } - } - let manifest = env!("CARGO_MANIFEST_DIR"); - let release = PathBuf::from(format!("{manifest}/target/release/moon")); - if release.exists() { - return release; - } - let debug = PathBuf::from(format!("{manifest}/target/debug/moon")); - if debug.exists() { - return debug; - } - panic!( - "No moon binary found. Build with `cargo build --release` or set \ - MOON_BIN=/path/to/moon." - ); + common::find_moon_binary() } fn unique_port() -> u16 { diff --git a/tests/crash_recovery_orphan_sweep_readiness.rs b/tests/crash_recovery_orphan_sweep_readiness.rs index ad142958b..384c51fe0 100644 --- a/tests/crash_recovery_orphan_sweep_readiness.rs +++ b/tests/crash_recovery_orphan_sweep_readiness.rs @@ -38,6 +38,8 @@ #![cfg(any(feature = "runtime-monoio", feature = "runtime-tokio"))] +mod common; + use std::process::{Child, Command, Stdio}; use std::time::{Duration, Instant}; @@ -78,7 +80,7 @@ fn unique_dir(suffix: &str) -> std::path::PathBuf { } fn start_moon(port: u16, dir: &std::path::Path, off_dir: &std::path::Path) -> Child { - Command::new("./target/release/moon") + Command::new(common::find_moon_binary()) .args([ "--port", &port.to_string(), diff --git a/tests/crash_recovery_wal_recycle_legacy.rs b/tests/crash_recovery_wal_recycle_legacy.rs index eec621439..43aba65ab 100644 --- a/tests/crash_recovery_wal_recycle_legacy.rs +++ b/tests/crash_recovery_wal_recycle_legacy.rs @@ -51,6 +51,8 @@ #![allow(clippy::unwrap_used)] +mod common; + use std::io::{BufReader, Read, Write}; use std::net::{TcpStream, ToSocketAddrs}; use std::path::{Path, PathBuf}; @@ -58,29 +60,14 @@ use std::process::{Child, Command}; use std::time::{Duration, Instant}; // --------------------------------------------------------------------------- -// Binary resolution (pattern: crash_recovery_graph_durability.rs) +// Binary resolution — delegates to common::find_moon_binary (task: +// test/harness-hygiene-sweep). The old local copy stopped at +// target/{release,debug}/moon and never checked CARGO_BIN_EXE_moon, the +// binary cargo actually built for THIS test run. // --------------------------------------------------------------------------- fn find_moon_binary() -> PathBuf { - if let Ok(bin) = std::env::var("MOON_BIN") { - let p = PathBuf::from(bin); - if p.exists() { - return p; - } - } - let manifest = env!("CARGO_MANIFEST_DIR"); - let release = PathBuf::from(format!("{manifest}/target/release/moon")); - if release.exists() { - return release; - } - let debug = PathBuf::from(format!("{manifest}/target/debug/moon")); - if debug.exists() { - return debug; - } - panic!( - "No moon binary found. Build with `cargo build --release` or set \ - MOON_BIN=/path/to/moon." - ); + common::find_moon_binary() } fn unique_port() -> u16 { diff --git a/tests/flush_cross_shard_scatter.rs b/tests/flush_cross_shard_scatter.rs index 8576bc079..7e73c088b 100644 --- a/tests/flush_cross_shard_scatter.rs +++ b/tests/flush_cross_shard_scatter.rs @@ -28,11 +28,7 @@ fn redis_cli_available() -> bool { } fn release_binary() -> std::path::PathBuf { - // MOON_BIN pin wins (VM-local target dirs); fall back to target/release. - if let Ok(p) = std::env::var("MOON_BIN") { - return std::path::PathBuf::from(p); - } - std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("target/release/moon") + common::find_moon_binary() } struct Moon { diff --git a/tests/info_memory_allocator_pagecache.rs b/tests/info_memory_allocator_pagecache.rs index fa13b5174..af8db4f08 100644 --- a/tests/info_memory_allocator_pagecache.rs +++ b/tests/info_memory_allocator_pagecache.rs @@ -29,12 +29,7 @@ fn redis_cli_available() -> bool { /// Resolve the release binary to spawn. Honors `MOON_BIN` when set (VM / /// worktree parity -- see `gotcha_orbstack_macho_binary_trap`). fn release_binary() -> std::path::PathBuf { - if let Ok(bin) = std::env::var("MOON_BIN") - && !bin.trim().is_empty() - { - return std::path::PathBuf::from(bin); - } - std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("target/release/moon") + common::find_moon_binary() } /// Running moon instance. Auto-killed on drop. diff --git a/tests/memory_prometheus_kinds.rs b/tests/memory_prometheus_kinds.rs index a1f286211..cc266daac 100644 --- a/tests/memory_prometheus_kinds.rs +++ b/tests/memory_prometheus_kinds.rs @@ -34,12 +34,7 @@ fn redis_cli_available() -> bool { /// 30s accept timeout with no obvious cause). See /// `gotcha_orbstack_macho_binary_trap`. fn release_binary() -> std::path::PathBuf { - if let Ok(bin) = std::env::var("MOON_BIN") - && !bin.trim().is_empty() - { - return std::path::PathBuf::from(bin); - } - std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("target/release/moon") + common::find_moon_binary() } /// Running moon instance. Auto-killed on drop. diff --git a/tests/perf_v0112_arenas_cap.rs b/tests/perf_v0112_arenas_cap.rs index df2af4288..8bb6089ab 100644 --- a/tests/perf_v0112_arenas_cap.rs +++ b/tests/perf_v0112_arenas_cap.rs @@ -78,6 +78,21 @@ fn run_memory_doctor(port: u16) -> String { String::from_utf8_lossy(&out.stdout).into_owned() } +/// Kill-on-drop guard: `wait_ready` and `run_memory_doctor`'s panics below +/// can leak the server before the old manual `child.kill()` ran (task: +/// test/harness-hygiene-sweep). See tests/bgsave_startup_race.rs for the +/// same pattern. +struct MoonGuard(Option); + +impl Drop for MoonGuard { + fn drop(&mut self) { + if let Some(mut child) = self.0.take() { + let _ = child.kill(); + let _ = child.wait(); + } + } +} + #[test] fn default_arenas_is_eight() { if !redis_cli_available() { @@ -85,11 +100,11 @@ fn default_arenas_is_eight() { return; } let port = 16399; - let mut child = spawn_moon(&[], port); + let child = spawn_moon(&[], port); + let child = MoonGuard(Some(child)); wait_ready(port); let output = run_memory_doctor(port); - let _ = child.kill(); - let _ = child.wait(); + drop(child); // MoonGuard SIGKILLs + reaps let arenas = parse_arenas(&output).unwrap_or_else(|| { panic!( @@ -110,11 +125,11 @@ fn override_via_cli_flag() { return; } let port = 16400; - let mut child = spawn_moon(&["--memory-arenas-cap", "4"], port); + let child = spawn_moon(&["--memory-arenas-cap", "4"], port); + let child = MoonGuard(Some(child)); wait_ready(port); let output = run_memory_doctor(port); - let _ = child.kill(); - let _ = child.wait(); + drop(child); // MoonGuard SIGKILLs + reaps let arenas = parse_arenas(&output).expect("MEMORY DOCTOR missing Arenas line"); assert_eq!( diff --git a/tests/pubsub_kv_ordering.rs b/tests/pubsub_kv_ordering.rs index 75318f763..5e0263125 100644 --- a/tests/pubsub_kv_ordering.rs +++ b/tests/pubsub_kv_ordering.rs @@ -26,6 +26,15 @@ fn moon_binary() -> Option { if let Ok(p) = std::env::var("MOON_BIN") { return Some(std::path::PathBuf::from(p)); } + // CARGO_BIN_EXE_moon is the binary cargo built for THIS test run (right + // profile, right CARGO_TARGET_DIR); cargo guarantees it exists whenever + // this env!() macro is referenced, so check it before falling back to a + // bare target/{release,debug}/moon guess that risks a stale binary of + // unknown provenance on a shared checkout (task: harness-hygiene-sweep). + let cargo_bin = std::path::PathBuf::from(env!("CARGO_BIN_EXE_moon")); + if cargo_bin.exists() { + return Some(cargo_bin); + } let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); for rel in ["target/release/moon", "target/debug/moon"] { let p = root.join(rel); diff --git a/tests/pubsub_multi_channel_acl.rs b/tests/pubsub_multi_channel_acl.rs index 466c3cc7c..830053823 100644 --- a/tests/pubsub_multi_channel_acl.rs +++ b/tests/pubsub_multi_channel_acl.rs @@ -23,6 +23,15 @@ fn moon_binary() -> Option { if let Ok(p) = std::env::var("MOON_BIN") { return Some(std::path::PathBuf::from(p)); } + // CARGO_BIN_EXE_moon is the binary cargo built for THIS test run (right + // profile, right CARGO_TARGET_DIR); cargo guarantees it exists whenever + // this env!() macro is referenced, so check it before falling back to a + // bare target/{release,debug}/moon guess that risks a stale binary of + // unknown provenance on a shared checkout (task: harness-hygiene-sweep). + let cargo_bin = std::path::PathBuf::from(env!("CARGO_BIN_EXE_moon")); + if cargo_bin.exists() { + return Some(cargo_bin); + } let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); for rel in ["target/release/moon", "target/debug/moon"] { let p = root.join(rel); diff --git a/tests/replication_graph.rs b/tests/replication_graph.rs index fc0c1ac62..0c52151eb 100644 --- a/tests/replication_graph.rs +++ b/tests/replication_graph.rs @@ -10,13 +10,15 @@ //! //! Run: `MOON_BIN=./target/release/moon cargo test --test replication_graph -- --ignored` +mod common; + use std::io::{Read, Write}; use std::net::TcpStream; use std::process::{Child, Command, Stdio}; use std::time::{Duration, Instant}; -fn moon_bin() -> String { - std::env::var("MOON_BIN").unwrap_or_else(|_| "./target/release/moon".to_string()) +fn moon_bin() -> std::path::PathBuf { + common::find_moon_binary() } fn start_moon(port: u16, dir: &str) -> Child { diff --git a/tests/replication_hardening.rs b/tests/replication_hardening.rs index 9be5ef879..8badb86dc 100644 --- a/tests/replication_hardening.rs +++ b/tests/replication_hardening.rs @@ -8,14 +8,16 @@ //! ./target/release/moon (⚠ on a shared macOS/Linux checkout the default may //! be the other platform's binary; always pin MOON_BIN, repo harness rule). +mod common; + use std::io::{BufRead, BufReader, Write}; use std::net::TcpStream; use std::process::{Command, Stdio}; use std::thread; use std::time::Duration; -fn moon_bin() -> String { - std::env::var("MOON_BIN").unwrap_or_else(|_| "./target/release/moon".to_string()) +fn moon_bin() -> std::path::PathBuf { + common::find_moon_binary() } fn start_moon(port: u16, dir: &str, extra: &[&str]) -> Guard { diff --git a/tests/replication_mq.rs b/tests/replication_mq.rs index 652c66364..962934144 100644 --- a/tests/replication_mq.rs +++ b/tests/replication_mq.rs @@ -15,13 +15,15 @@ //! //! Run: `MOON_BIN=./target/release/moon cargo test --test replication_mq -- --ignored` +mod common; + use std::io::{Read, Write}; use std::net::TcpStream; use std::process::{Child, Command, Stdio}; use std::time::{Duration, Instant}; -fn moon_bin() -> String { - std::env::var("MOON_BIN").unwrap_or_else(|_| "./target/release/moon".to_string()) +fn moon_bin() -> std::path::PathBuf { + common::find_moon_binary() } fn start_moon(port: u16, dir: &str, shards: usize) -> Child { diff --git a/tests/replication_multishard.rs b/tests/replication_multishard.rs index 97276356c..7cd017644 100644 --- a/tests/replication_multishard.rs +++ b/tests/replication_multishard.rs @@ -13,14 +13,16 @@ //! cargo test --test replication_multishard -- --ignored --nocapture //! ``` +mod common; + use std::io::{BufRead, BufReader, Read, Write}; use std::net::TcpStream; use std::process::{Child, Command, Stdio}; use std::thread; use std::time::Duration; -fn moon_bin() -> String { - std::env::var("MOON_BIN").unwrap_or_else(|_| "./target/release/moon".to_string()) +fn moon_bin() -> std::path::PathBuf { + common::find_moon_binary() } fn start_moon_shards(port: u16, dir: &str, shards: usize) -> Child { diff --git a/tests/replication_planes.rs b/tests/replication_planes.rs index f630b71a6..71b0f7a37 100644 --- a/tests/replication_planes.rs +++ b/tests/replication_planes.rs @@ -34,8 +34,8 @@ use std::process::{Child, Command, Stdio}; use std::thread; use std::time::Duration; -fn moon_bin() -> String { - std::env::var("MOON_BIN").unwrap_or_else(|_| "./target/release/moon".to_string()) +fn moon_bin() -> std::path::PathBuf { + common::find_moon_binary() } /// Spawn moon with explicit `--shards` plus arbitrary extra CLI args (mirrors diff --git a/tests/replication_readonly_eval.rs b/tests/replication_readonly_eval.rs index 156212ee8..5cc1332e9 100644 --- a/tests/replication_readonly_eval.rs +++ b/tests/replication_readonly_eval.rs @@ -29,8 +29,8 @@ mod common; use std::process::{Child, Command, Stdio}; use std::time::Duration; -fn moon_bin() -> String { - std::env::var("MOON_BIN").unwrap_or_else(|_| "./target/release/moon".to_string()) +fn moon_bin() -> std::path::PathBuf { + common::find_moon_binary() } fn start_moon(port: u16, dir: &str) -> Child { @@ -77,12 +77,28 @@ fn assert_readonly(result: redis::RedisResult, label: &st ); } +/// Kill-on-drop guard: the many `assert_eq!`/`.expect()` calls below used to +/// run entirely before the manual `child.kill()` at the end of the test, so +/// any one of them panicking orphaned the server (task: +/// test/harness-hygiene-sweep). See tests/bgsave_startup_race.rs for the +/// same pattern. +struct MoonGuard(Option); + +impl Drop for MoonGuard { + fn drop(&mut self) { + if let Some(mut child) = self.0.take() { + let _ = child.kill(); + let _ = child.wait(); + } + } +} + #[tokio::test] #[ignore = "spawns a real moon binary — set MOON_BIN, see module docs"] async fn test_readonly_replica_blocks_writing_eval_and_evalsha() { let dir = tempfile::tempdir().unwrap(); - let (mut child, port) = - common::spawn_listening(|p| start_moon(p, dir.path().to_str().unwrap())); + let (child, port) = common::spawn_listening(|p| start_moon(p, dir.path().to_str().unwrap())); + let child = MoonGuard(Some(child)); let mut con = connect(port).await; // --- Master: a writing EVAL succeeds, proving the harness/script path @@ -180,6 +196,5 @@ async fn test_readonly_replica_blocks_writing_eval_and_evalsha() { "read-only EVAL must still be served on a read-only replica, got: {eval_read_on_replica:?}" ); - let _ = child.kill(); - let _ = child.wait(); + drop(child); // MoonGuard SIGKILLs + reaps } diff --git a/tests/replication_readonly_ws_mq.rs b/tests/replication_readonly_ws_mq.rs index ad4f6b48d..907ea59ce 100644 --- a/tests/replication_readonly_ws_mq.rs +++ b/tests/replication_readonly_ws_mq.rs @@ -35,8 +35,8 @@ mod common; use std::process::{Child, Command, Stdio}; use std::time::Duration; -fn moon_bin() -> String { - std::env::var("MOON_BIN").unwrap_or_else(|_| "./target/release/moon".to_string()) +fn moon_bin() -> std::path::PathBuf { + common::find_moon_binary() } /// Spawn moon single-shard, AOF disabled, matching the repo harness rule of @@ -92,12 +92,28 @@ fn assert_readonly(result: redis::RedisResult, label: &st ); } +/// Kill-on-drop guard: the many `assert_eq!`/`.expect()` calls below used to +/// run entirely before the manual `child.kill()` at the end of the test, so +/// any one of them panicking orphaned the server (task: +/// test/harness-hygiene-sweep). See tests/bgsave_startup_race.rs for the +/// same pattern. +struct MoonGuard(Option); + +impl Drop for MoonGuard { + fn drop(&mut self) { + if let Some(mut child) = self.0.take() { + let _ = child.kill(); + let _ = child.wait(); + } + } +} + #[tokio::test] #[ignore = "spawns a real moon binary — set MOON_BIN, see module docs"] async fn test_readonly_replica_blocks_ws_mq_temporal_writes() { let dir = tempfile::tempdir().unwrap(); - let (mut child, port) = - common::spawn_listening(|p| start_moon(p, dir.path().to_str().unwrap())); + let (child, port) = common::spawn_listening(|p| start_moon(p, dir.path().to_str().unwrap())); + let child = MoonGuard(Some(child)); let mut con = connect(port).await; // --- Master: mutating subcommands succeed, so the replica assertions @@ -194,6 +210,5 @@ async fn test_readonly_replica_blocks_ws_mq_temporal_writes() { "MQ DLQLEN must still be served on a read-only replica, got: {dlqlen_on_replica:?}" ); - let _ = child.kill(); - let _ = child.wait(); + drop(child); // MoonGuard SIGKILLs + reaps } diff --git a/tests/replication_streaming.rs b/tests/replication_streaming.rs index a3a5487e1..c2858ee47 100644 --- a/tests/replication_streaming.rs +++ b/tests/replication_streaming.rs @@ -14,14 +14,16 @@ //! stream (`buf.clear()`), so a freshly-attached replica reported `DBSIZE 0`. //! These tests lock in the fix. +mod common; + use std::io::{BufRead, BufReader, Read, Write}; use std::net::TcpStream; use std::process::{Child, Command, Stdio}; use std::thread; use std::time::Duration; -fn moon_bin() -> String { - std::env::var("MOON_BIN").unwrap_or_else(|_| "./target/release/moon".to_string()) +fn moon_bin() -> std::path::PathBuf { + common::find_moon_binary() } fn start_moon(port: u16, dir: &str) -> Child { diff --git a/tests/replication_ttl_semantics.rs b/tests/replication_ttl_semantics.rs index 49958dcdd..3ca7b4ccf 100644 --- a/tests/replication_ttl_semantics.rs +++ b/tests/replication_ttl_semantics.rs @@ -17,14 +17,16 @@ //! With the master-side rewrite to `PEXPIREAT k `, both sides carry the //! **identical** absolute deadline — asserted here by exact equality. +mod common; + use std::io::{BufReader, Read, Write}; use std::net::TcpStream; use std::process::{Child, Command, Stdio}; use std::thread; use std::time::Duration; -fn moon_bin() -> String { - std::env::var("MOON_BIN").unwrap_or_else(|_| "./target/release/moon".to_string()) +fn moon_bin() -> std::path::PathBuf { + common::find_moon_binary() } fn start_moon(port: u16, dir: &str) -> Child { diff --git a/tests/replication_ws.rs b/tests/replication_ws.rs index eae647994..f1f46d173 100644 --- a/tests/replication_ws.rs +++ b/tests/replication_ws.rs @@ -9,13 +9,15 @@ //! //! Run: `MOON_BIN=./target/release/moon cargo test --test replication_ws -- --ignored` +mod common; + use std::io::{Read, Write}; use std::net::TcpStream; use std::process::{Child, Command, Stdio}; use std::time::{Duration, Instant}; -fn moon_bin() -> String { - std::env::var("MOON_BIN").unwrap_or_else(|_| "./target/release/moon".to_string()) +fn moon_bin() -> std::path::PathBuf { + common::find_moon_binary() } fn start_moon(port: u16, dir: &str) -> Child { diff --git a/tests/scan_fanout_multishard.rs b/tests/scan_fanout_multishard.rs index 11411296a..bc9a5d856 100644 --- a/tests/scan_fanout_multishard.rs +++ b/tests/scan_fanout_multishard.rs @@ -10,6 +10,8 @@ #![cfg(feature = "console")] +mod common; + use std::collections::HashSet; use std::process::{Child, Command}; use std::time::Duration; @@ -19,7 +21,7 @@ const ADMIN_PORT: u16 = 16500; const KEY_COUNT: usize = 100; fn start_server() -> Child { - Command::new("./target/release/moon") + Command::new(common::find_moon_binary()) .args([ "--port", &RESP_PORT.to_string(), @@ -67,10 +69,26 @@ fn curl_get(url: &str) -> (u16, String) { (status, body) } +/// Kill-on-drop guard: every assert!/expect! below panics through the guard +/// instead of orphaning the server (task: test/harness-hygiene-sweep; the +/// SCAN pagination loop below has several assert/expect points that used to +/// run BEFORE the manual `server.kill()` at the end). See +/// tests/bgsave_startup_race.rs for the same pattern. +struct MoonGuard(Option); + +impl Drop for MoonGuard { + fn drop(&mut self) { + if let Some(mut child) = self.0.take() { + common::sigkill(&mut child); + } + } +} + #[test] #[ignore] // Requires built release binary + redis-cli; run explicitly fn scan_fanout_returns_all_keys_across_shards() { - let mut server = start_server(); + let server = start_server(); + let server = MoonGuard(Some(server)); wait_for_port(ADMIN_PORT); wait_for_port(RESP_PORT); @@ -134,9 +152,9 @@ fn scan_fanout_returns_all_keys_across_shards() { } } - // Cleanup BEFORE asserting so we don't leak the server on failure. - server.kill().ok(); - server.wait().ok(); + // MoonGuard SIGKILLs + reaps on drop; explicit drop here keeps the old + // "cleanup before asserting" ordering for the assertions below. + drop(server); let missing: Vec<&String> = inserted.difference(&found).collect(); assert!( diff --git a/tests/sharded_multi_exec_durability.rs b/tests/sharded_multi_exec_durability.rs index 2d56cc0fa..4590cec63 100644 --- a/tests/sharded_multi_exec_durability.rs +++ b/tests/sharded_multi_exec_durability.rs @@ -33,6 +33,15 @@ fn moon_binary() -> Option { if let Ok(p) = std::env::var("MOON_BIN") { return Some(std::path::PathBuf::from(p)); } + // CARGO_BIN_EXE_moon is the binary cargo built for THIS test run (right + // profile, right CARGO_TARGET_DIR); cargo guarantees it exists whenever + // this env!() macro is referenced, so check it before falling back to a + // bare target/{release,debug}/moon guess that risks a stale binary of + // unknown provenance on a shared checkout (task: harness-hygiene-sweep). + let cargo_bin = std::path::PathBuf::from(env!("CARGO_BIN_EXE_moon")); + if cargo_bin.exists() { + return Some(cargo_bin); + } let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); for rel in ["target/release/moon", "target/debug/moon"] { let p = root.join(rel); diff --git a/tests/sharded_multi_exec_locality.rs b/tests/sharded_multi_exec_locality.rs index f9a2c2283..25c2f81ff 100644 --- a/tests/sharded_multi_exec_locality.rs +++ b/tests/sharded_multi_exec_locality.rs @@ -34,6 +34,15 @@ fn moon_binary() -> Option { if let Ok(p) = std::env::var("MOON_BIN") { return Some(std::path::PathBuf::from(p)); } + // CARGO_BIN_EXE_moon is the binary cargo built for THIS test run (right + // profile, right CARGO_TARGET_DIR); cargo guarantees it exists whenever + // this env!() macro is referenced, so check it before falling back to a + // bare target/{release,debug}/moon guess that risks a stale binary of + // unknown provenance on a shared checkout (task: harness-hygiene-sweep). + let cargo_bin = std::path::PathBuf::from(env!("CARGO_BIN_EXE_moon")); + if cargo_bin.exists() { + return Some(cargo_bin); + } let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); for rel in ["target/release/moon", "target/debug/moon"] { let p = root.join(rel); diff --git a/tests/sharded_multi_exec_routing.rs b/tests/sharded_multi_exec_routing.rs index 0ffff6c56..ed183e508 100644 --- a/tests/sharded_multi_exec_routing.rs +++ b/tests/sharded_multi_exec_routing.rs @@ -28,6 +28,15 @@ fn moon_binary() -> Option { if let Ok(p) = std::env::var("MOON_BIN") { return Some(std::path::PathBuf::from(p)); } + // CARGO_BIN_EXE_moon is the binary cargo built for THIS test run (right + // profile, right CARGO_TARGET_DIR); cargo guarantees it exists whenever + // this env!() macro is referenced, so check it before falling back to a + // bare target/{release,debug}/moon guess that risks a stale binary of + // unknown provenance on a shared checkout (task: harness-hygiene-sweep). + let cargo_bin = std::path::PathBuf::from(env!("CARGO_BIN_EXE_moon")); + if cargo_bin.exists() { + return Some(cargo_bin); + } let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); for rel in ["target/release/moon", "target/debug/moon"] { let p = root.join(rel); diff --git a/tests/wal_group_commit.rs b/tests/wal_group_commit.rs index df3408453..8de2125c1 100644 --- a/tests/wal_group_commit.rs +++ b/tests/wal_group_commit.rs @@ -21,6 +21,8 @@ //! //! Running: cargo test --test wal_group_commit +mod common; + use bytes::Bytes; use moon::persistence::aof::group_commit::{ AOF_GROUP_COMMIT_MAX_BATCH, AOF_GROUP_COMMIT_MAX_BYTES, CommitOutcome, GroupCommitBatch, @@ -412,7 +414,7 @@ mod integration { } fn start_moon(port: u16, dir: &std::path::Path, shards: u16, fsync: &str) -> Child { - Command::new("./target/release/moon") + Command::new(super::common::find_moon_binary()) .args([ "--port", &port.to_string(),