use std::path::PathBuf;
use incurs::cli::Cli;
#[allow(dead_code)]
#[path = "../examples/todoapp.rs"]
mod todoapp;
async fn observe(argv: &[&str]) -> String {
let cli: Cli = todoapp::build_cli();
let argv: Vec<String> = argv.iter().map(|token| token.to_string()).collect();
let mut buffer = Vec::new();
let exit_code = cli
.serve_to(argv, &mut buffer, false)
.await
.expect("serve_to should not return Err");
let stdout = String::from_utf8(buffer).expect("output should be valid UTF-8");
render(exit_code, &strip_durations(&stdout))
}
fn render(exit_code: Option<i32>, stdout: &str) -> String {
let exit = match exit_code {
Some(code) => code.to_string(),
None => "none".to_string(),
};
format!("exit: {exit}\n--- stdout ---\n{stdout}")
}
fn strip_durations(text: &str) -> String {
let mut output = String::with_capacity(text.len());
let mut rest = text;
while let Some(start) = rest.find(|c: char| c.is_ascii_digit()) {
let (head, tail) = rest.split_at(start);
let digits: String = tail
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect();
let after = &tail[digits.len()..];
if let Some(remainder) = after.strip_prefix("ms") {
output.push_str(head);
output.push_str("<duration>ms");
rest = remainder;
} else {
output.push_str(head);
output.push_str(&digits);
rest = after;
}
}
output.push_str(rest);
output
}
fn golden_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("golden")
.join("cli_surface")
.join(format!("{}.txt", name.replace(' ', "-")))
}
fn assert_golden(name: &str, observed: &str) {
let path = golden_path(name);
if std::env::var_os("UPDATE_GOLDEN").is_some() {
std::fs::create_dir_all(path.parent().expect("golden directory"))
.expect("golden directory should be creatable");
std::fs::write(&path, observed).expect("golden should be writable");
return;
}
let expected = std::fs::read_to_string(&path).unwrap_or_else(|error| {
panic!(
"missing golden {}: {error}\nrerun with UPDATE_GOLDEN=1 to create it",
path.display()
)
});
assert_eq!(
observed, expected,
"the observable CLI surface changed for case `{name}`.\n\
If that is intended, rerun with UPDATE_GOLDEN=1 and review the diff."
);
}
macro_rules! surface_case {
($test:ident, $name:literal, $argv:expr) => {
#[tokio::test]
async fn $test() {
assert_golden($name, &observe(&$argv).await);
}
};
}
surface_case!(help, "help", ["--help"]);
surface_case!(version, "version", ["--version"]);
surface_case!(default_toon, "default-toon", ["stats"]);
surface_case!(json_shorthand, "json-shorthand", ["stats", "--json"]);
surface_case!(json_format, "json-format", ["list", "--format", "json"]);
surface_case!(option_default, "option-default", ["list", "--json"]);
surface_case!(
option_value,
"option-value",
["list", "--status", "pending", "--json"]
);
surface_case!(
option_alias,
"option-alias",
["list", "-s", "done", "--json"]
);
surface_case!(positional, "positional", ["get", "1", "--json"]);
surface_case!(
structured_error,
"structured-error",
["get", "99", "--json"]
);
surface_case!(
cta,
"cta",
["add", "test item", "--priority", "high", "--json"]
);
surface_case!(
filter,
"filter",
["stats", "--filter-output", "by_priority.high", "--json"]
);
surface_case!(
full_output,
"full-output",
["complete", "1", "--full-output", "--json"]
);
surface_case!(buffered_stream, "buffered-stream", ["stream", "--json"]);
surface_case!(
jsonl_stream,
"jsonl-stream",
["stream", "--format", "jsonl"]
);
#[cfg(feature = "yaml")]
surface_case!(yaml_format, "yaml-format", ["stats", "--format", "yaml"]);
surface_case!(schema_command, "schema-command", ["add", "--schema"]);
surface_case!(llms_index, "llms-index", ["--llms"]);
surface_case!(llms_full, "llms-full", ["--llms-full", "--format", "json"]);
#[tokio::test]
async fn harness_matches_a_real_process() {
let executable =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/debug/examples/todoapp");
if !executable.is_file() {
eprintln!("skipping: {} is not built", executable.display());
return;
}
for argv in [
vec!["stats", "--json"],
vec!["get", "99", "--json"],
vec!["--help"],
] {
let output = std::process::Command::new(&executable)
.args(&argv)
.output()
.expect("the example binary should run");
assert_eq!(
String::from_utf8_lossy(&output.stderr),
"",
"a piped run writes nothing to stderr, so the goldens lose no coverage by \
recording stdout alone (argv: {argv:?})"
);
let spawned = render(
output.status.code().filter(|code| *code != 0),
&strip_durations(&String::from_utf8_lossy(&output.stdout)),
);
assert_eq!(
spawned,
observe(&argv).await,
"the in-process harness diverged from a real process (argv: {argv:?})"
);
}
}