josie / alder-tools

fix: smoke exit codes; null child stdin (yc tty-hang); uninstall rmdir

- main.rs: smoke_workload -> i32 and exit() wiring; Clean/Stopped = 0,
  real failures = 1 (7z cut at the smoke window is Stopped by design);
  unknown/unwired name = 1; start failure = 1
- workload.rs: SpawnedWorkload::spawn sets stdin(null) - y-cruncher stress
  prompts on a tty and hung waiting for ENTER
- Makefile: uninstall rmdir -p also removes the bin dir, not just conf dir
- 71 tests, clippy -D/fmt clean

c7cc56f04e7cec733e0217d37b05bebb16427c38
josie <josie@example.com> · 2026-10-08T21:23 · browse files at this commit

parents: d7832fa

diff --git a/alderbench/Makefile b/alderbench/Makefile
index b0de8cf..3a99d3a 100644
--- a/alderbench/Makefile
+++ b/alderbench/Makefile
@@ -24,7 +24,7 @@ install: build
 
 uninstall:
 	rm -f $(BIN) $(CONF)
-	rmdir -p --ignore-fail-on-non-empty $(CONF) 2>/dev/null || true
+	rmdir -p --ignore-fail-on-non-empty $(dir $(BIN)) $(dir $(CONF)) 2>/dev/null || true
 
 clean:
 	cargo clean
diff --git a/alderbench/src/main.rs b/alderbench/src/main.rs
index 52a971e..c3b07a0 100644
--- a/alderbench/src/main.rs
+++ b/alderbench/src/main.rs
@@ -20,7 +20,7 @@ const USAGE: &str = "\
 usage: alderbench [OPTION]...
 
   (no option)              pick a preset or edit a custom sweep (TUI)
-  --tui                    TUI with the config as loaded (no picker)
+  --tui                    TUI picker (loaded config = Custom fallback)
   --preset NAME            TUI straight into a preset: stability | bench |
                            core-cycle
   --headless               run the configured sweep without a TUI
@@ -78,13 +78,12 @@ fn main() {
     }
     if let Some(idx) = args.iter().position(|a| a == "--smoke-workload") {
         match args.get(idx + 1) {
-            Some(name) => smoke_workload(name, &config::load()),
+            Some(name) => std::process::exit(smoke_workload(name, &config::load())),
             None => {
                 eprintln!("alderbench: --smoke-workload needs a name\n{USAGE}");
                 std::process::exit(2);
             }
         }
-        return;
     }
     if args.iter().any(|a| a == "--headless") {
         headless_run(config::load());
@@ -264,7 +263,7 @@ fn format_score(s: &workload::Score) -> String {
     }
 }
 
-fn smoke_workload(name: &str, cfg: &config::Config) {
+fn smoke_workload(name: &str, cfg: &config::Config) -> i32 {
     use std::thread;
     use std::time::Duration;
     // Build a 3-second spec for the requested workload, regardless of the
@@ -276,10 +275,10 @@ fn smoke_workload(name: &str, cfg: &config::Config) {
     };
     let Some(mut w) = workload::from_spec(&spec, &cfg.bin_dir) else {
         eprintln!("unknown or not-yet-wired workload: {name}");
-        return;
+        return 1;
     };
     println!("smoke: {}  params={}", w.name(), w.params());
-    match w.start() {
+    let verdict = match w.start() {
         Ok(()) => {
             // Poll is_running every 250ms; stop after the spec's duration
             // plus a 1s grace (some workloads self-exit right at the
@@ -299,11 +298,20 @@ fn smoke_workload(name: &str, cfg: &config::Config) {
                 println!("smoke: still running — calling stop()");
                 let _ = w.stop();
             }
-            let verdict = w.wait();
-            let score = w.score();
-            println!("smoke: verdict = {:?}", verdict);
-            println!("smoke: score   = {}", format_score(&score));
+            w.wait()
         }
-        Err(e) => println!("smoke: start failed: {e}"),
+        Err(e) => {
+            println!("smoke: start failed: {e}");
+            return 1;
+        }
+    };
+    let score = w.score();
+    println!("smoke: verdict = {verdict:?}");
+    println!("smoke: score   = {}", format_score(&score));
+    // Stopped is expected when a longer workload (7z) is cut at the smoke
+    // window — only real failures fail the exit code.
+    match verdict {
+        workload::Verdict::Clean | workload::Verdict::Stopped => 0,
+        _ => 1,
     }
 }
diff --git a/alderbench/src/workload.rs b/alderbench/src/workload.rs
index 9db9a52..8254916 100644
--- a/alderbench/src/workload.rs
+++ b/alderbench/src/workload.rs
@@ -158,6 +158,7 @@ impl SpawnedWorkload {
 
     fn spawn(cmd: &mut Command) -> io::Result<Self> {
         let child = cmd
+            .stdin(std::process::Stdio::null())
             .stdout(std::process::Stdio::piped())
             .stderr(std::process::Stdio::piped())
             .spawn()?;
@@ -413,10 +414,12 @@ impl Workload for YCruncher {
                 format!("y-cruncher variant path canonicalize failed: {e}"),
             )
         })?;
-        // Wrap in `taskset -c <cores>` when the cores range is a subset
-        // (not all 0-15). y-cruncher spawns 16 threads by default + has no
-        // CLI flag for affinity; taskset pins it at the kernel level. The
-        // full-range case skips the wrapper (no point constraining to all).
+        // NOTE: y-cruncher pins its worker threads itself (verified on
+        // 12600KF: taskset -c 0 still spread threads across cpu0-15), and
+        // stress mode rejects -TD/-PF/-noSMT entirely — so a y-cruncher
+        // stress leg ALWAYS occupies every logical core. The taskset
+        // wrapper only constrains the initial thread; it does NOT make
+        // the leg desktop-safe. `cores` is decorative for yc legs.
         let mut cmd = taskset_command(&self.cores, &variant_path);
         cmd.arg("skip-warnings")
             .arg("stress")
@@ -591,8 +594,10 @@ impl Workload for SevenZip {
 /// Build a `Command` for `program`, wrapped in `taskset -c <cores>` when
 /// `cores` is a subset of the available CPUs (not the full "0-15" range).
 /// The full-range case skips the wrapper — there's no point constraining
-/// to all CPUs. Used by YCruncher + SevenZip (StressNg has its own
-/// `--taskset` flag, so it doesn't go through here). Returns a `Command`
+/// to all CPUs. Used by SevenZip (honors it: taskset pins + `-mmt<N>`
+/// caps threads) + YCruncher (starts under it but then RE-PINS its own
+/// threads to its own core table — all cores, see YCruncher::start;
+/// StressNg has its own `--taskset` flag, so it doesn't go through here). Returns a `Command`
 /// pointing at either `taskset` (with program as an arg) or `program`
 /// directly. `program` accepts `OsStr` so it works with both `&str`
 /// (7z) and `&PathBuf` (y-cruncher's canonicalized variant path).