use std::path::{Path, PathBuf};
use std::process::{Command, Output};
struct TempProject {
path: PathBuf,
}
impl TempProject {
fn new(tag: &str) -> Self {
let mut path = std::env::temp_dir();
path.push(format!("runner-quiet-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path).expect("create temp project dir");
Self { path }
}
fn file(self, name: &str, contents: &str) -> Self {
std::fs::write(self.path.join(name), contents).expect("write project file");
self
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TempProject {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
fn run_binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_run"))
}
fn tool_available(bin: &str) -> bool {
Command::new(bin)
.arg("--version")
.output()
.is_ok_and(|o| o.status.success())
}
fn run_in(dir: &Path, extra_env: &[(&str, &str)], args: &[&str]) -> Output {
let mut cmd = Command::new(run_binary());
for (key, _) in std::env::vars_os() {
if key
.to_string_lossy()
.to_ascii_uppercase()
.starts_with("RUNNER_")
{
cmd.env_remove(&key);
}
}
for (key, value) in extra_env {
cmd.env(key, value);
}
cmd.arg("--dir")
.arg(dir)
.args(args)
.output()
.expect("run should execute")
}
fn make_project(tag: &str) -> TempProject {
TempProject::new(tag).file("Makefile", "greet:\n\t@true\n")
}
fn npm_project(tag: &str) -> TempProject {
TempProject::new(tag)
.file("package.json", "{ \"scripts\": { \"greet\": \"true\" } }\n")
.file("package-lock.json", "{}\n")
}
#[test]
fn quiet_flag_suppresses_dispatch_arrow() {
if !tool_available("make") {
eprintln!("skipping: `make` not found on PATH");
return;
}
let proj = make_project("flag");
let output = run_in(proj.path(), &[], &["--quiet", "greet"]);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"run --quiet greet should succeed. status: {:?}, stderr: {stderr}",
output.status,
);
assert!(
!stderr.contains('→'),
"dispatch arrow must be suppressed with --quiet. stderr: {stderr}",
);
}
#[test]
fn runner_quiet_env_suppresses_dispatch_arrow() {
if !tool_available("make") {
eprintln!("skipping: `make` not found on PATH");
return;
}
let proj = make_project("env");
let output = run_in(proj.path(), &[("RUNNER_QUIET", "1")], &["greet"]);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"run with RUNNER_QUIET=1 should succeed. status: {:?}, stderr: {stderr}",
output.status,
);
assert!(
!stderr.contains('→'),
"dispatch arrow must be suppressed with RUNNER_QUIET=1. stderr: {stderr}",
);
}
#[test]
fn dispatch_arrow_prints_without_quiet() {
if !tool_available("make") {
eprintln!("skipping: `make` not found on PATH");
return;
}
let proj = make_project("plain");
let output = run_in(proj.path(), &[], &["greet"]);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"run greet should succeed. status: {:?}, stderr: {stderr}",
output.status,
);
assert!(
stderr.contains('→'),
"dispatch arrow expected without --quiet. stderr: {stderr}",
);
}
#[test]
fn quiet_suppresses_explain_trace() {
if !tool_available("npm") {
eprintln!("skipping: `npm` not found on PATH");
return;
}
let shown_proj = npm_project("explain-on");
let shown = run_in(shown_proj.path(), &[], &["--explain", "greet"]);
let shown_err = String::from_utf8_lossy(&shown.stderr);
assert!(
shown.status.success(),
"run --explain greet should succeed. status: {:?}, stderr: {shown_err}",
shown.status,
);
assert!(
shown_err.contains("resolved:"),
"--explain should emit a resolution trace to suppress. stderr: {shown_err}",
);
let hidden_proj = npm_project("explain-off");
let hidden = run_in(hidden_proj.path(), &[], &["--quiet", "--explain", "greet"]);
let hidden_err = String::from_utf8_lossy(&hidden.stderr);
assert!(
hidden.status.success(),
"run --quiet --explain greet should succeed. status: {:?}, stderr: {hidden_err}",
hidden.status,
);
assert!(
!hidden_err.contains('→') && !hidden_err.contains("resolved:"),
"--quiet must suppress the arrow and the explain trace. stderr: {hidden_err}",
);
}