use std::path::PathBuf;
use std::process::{Command, Stdio};
fn stryke_binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_stryke"))
}
fn run_docs(args: &[&str]) -> Option<(i32, String, String)> {
let bin = stryke_binary();
let mut cmd = Command::new(&bin);
cmd.arg("docs").args(args).stdin(Stdio::null());
let out = cmd.output().ok()?;
Some((
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).to_string(),
String::from_utf8_lossy(&out.stderr).to_string(),
))
}
#[test]
fn docs_with_topic_dumps_and_exits_zero() {
let Some((rc, stdout, _stderr)) = run_docs(&["pmap"]) else {
eprintln!("skip: stryke binary not built");
return;
};
assert_eq!(rc, 0, "stryke docs pmap should exit 0, got {rc}");
let lower = stdout.to_lowercase();
assert!(
lower.contains("pmap") && (lower.contains("parallel") || lower.contains("rayon")),
"expected pmap doc content, got first 200 chars: {:?}",
stdout.chars().take(200).collect::<String>(),
);
}
#[test]
fn docs_with_page_number_dumps_and_exits_zero() {
let Some((rc, stdout, _)) = run_docs(&["1"]) else {
eprintln!("skip: stryke binary not built");
return;
};
assert_eq!(rc, 0);
assert!(!stdout.is_empty(), "page 1 should produce output");
}
#[test]
fn docs_with_unknown_topic_exits_one() {
let Some((rc, _stdout, stderr)) = run_docs(&["definitely_not_a_real_builtin_xyz"]) else {
eprintln!("skip: stryke binary not built");
return;
};
assert_eq!(rc, 1);
assert!(
stderr.contains("no documentation for")
|| stderr.contains("definitely_not_a_real_builtin_xyz"),
"expected unknown-topic hint in stderr, got: {stderr:?}",
);
}
fn strip_numbered_docs_list_line(line: &str) -> Option<String> {
let t = line.trim_start();
let dot = t.find(". ")?;
let num = t[..dot].trim();
if num.is_empty() || !num.chars().all(|c| c.is_ascii_digit()) {
return None;
}
let name = t[dot + 2..].trim();
if name.is_empty() {
return None;
}
Some(name.to_string())
}
#[test]
fn docs_list_covers_every_dispatch_primary() {
let Some((rc, stdout, _)) = run_docs(&["--list"]) else {
eprintln!("skip: stryke binary not built");
return;
};
assert_eq!(rc, 0);
let bin = stryke_binary();
let listed: std::collections::HashSet<String> = stdout
.lines()
.filter_map(strip_numbered_docs_list_line)
.collect();
let primaries_out = std::process::Command::new(&bin)
.args(["-e", r#"for (sort keys %b) { print "$_\n" }"#])
.output()
.expect("run %b dump");
assert!(
primaries_out.status.success(),
"%b dump failed: status={:?} stderr={}",
primaries_out.status.code(),
String::from_utf8_lossy(&primaries_out.stderr),
);
let primaries: std::collections::HashSet<String> =
String::from_utf8_lossy(&primaries_out.stdout)
.lines()
.map(|s| s.to_string())
.collect();
let missing: Vec<&String> = primaries.difference(&listed).collect();
assert!(
missing.is_empty(),
"%b has {} primaries missing from `s docs --list`: {:?}",
missing.len(),
missing.iter().take(5).collect::<Vec<_>>(),
);
}
#[test]
fn docs_list_flag_emits_numbered_topics() {
let Some((rc, stdout, _)) = run_docs(&["--list"]) else {
eprintln!("skip: stryke binary not built");
return;
};
assert_eq!(rc, 0);
let lines: Vec<&str> = stdout.lines().collect();
assert!(
lines.len() > 50,
"expected many topics, got {} lines",
lines.len()
);
let numbered = lines.iter().filter(|l| l.contains(". ")).count();
assert!(
numbered > 50,
"expected numbered topics, got {numbered} of {}",
lines.len()
);
}
#[test]
fn docs_search_flag_returns_matches() {
let Some((rc, stdout, _)) = run_docs(&["--search", "parallel"]) else {
eprintln!("skip: stryke binary not built");
return;
};
assert_eq!(rc, 0);
let lower = stdout.to_lowercase();
assert!(lower.contains("pmap"), "search 'parallel' should hit pmap");
assert!(
lower.contains("pgrep"),
"search 'parallel' should hit pgrep"
);
}
#[test]
fn docs_help_flag_prints_usage() {
let Some((rc, stdout, _)) = run_docs(&["--help"]) else {
eprintln!("skip: stryke binary not built");
return;
};
assert_eq!(rc, 0);
assert!(stdout.contains("USAGE") || stdout.to_lowercase().contains("usage"));
}
#[test]
fn docs_toc_flag_exits_zero() {
let Some((rc, stdout, _)) = run_docs(&["--toc"]) else {
eprintln!("skip: stryke binary not built");
return;
};
assert_eq!(rc, 0);
assert!(!stdout.is_empty(), "--toc should produce output");
}
#[test]
fn docs_no_args_with_piped_stdin_exits_zero() {
let Some((rc, stdout, _)) = run_docs(&[]) else {
eprintln!("skip: stryke binary not built");
return;
};
assert_eq!(rc, 0, "no-args + piped stdin should not enter TUI");
assert!(
stdout.contains("STRYKE ENCYCLOPEDIA")
|| stdout.contains("INTERACTIVE REFERENCE")
|| stdout.contains("Introduction"),
"expected intro-page banner, got first 200: {:?}",
stdout.chars().take(200).collect::<String>(),
);
}
#[test]
fn docs_with_stryke_no_tty_env_exits_zero() {
let bin = stryke_binary();
let out = Command::new(&bin)
.arg("docs")
.arg("pmap")
.env("STRYKE_NO_TTY", "1")
.stdin(Stdio::null())
.output()
.expect("run stryke");
assert_eq!(out.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.to_lowercase().contains("pmap"));
}
#[test]
fn docs_with_generic_no_tty_env_exits_zero() {
let bin = stryke_binary();
let out = Command::new(&bin)
.arg("docs")
.arg("pmap")
.env("NO_TTY", "1")
.stdin(Stdio::null())
.output()
.expect("run stryke");
assert_eq!(out.status.code(), Some(0));
}
#[test]
fn docs_pmap_pipes_to_head_cleanly() {
let bin = stryke_binary();
let out = Command::new("sh")
.arg("-c")
.arg(format!("{} docs pmap | head -3", bin.display()))
.output()
.expect("run pipe");
assert_eq!(
out.status.code(),
Some(0),
"pipe to head should exit 0 (was: {:?})",
out.status.code()
);
let stdout = String::from_utf8_lossy(&out.stdout);
let lines: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect();
assert!(!lines.is_empty(), "head -3 produced nothing");
}