use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicUsize, Ordering};
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
struct Fixture {
root: PathBuf,
}
impl Fixture {
fn new() -> Self {
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
let root = std::env::temp_dir().join(format!("asrch-test-{}-{id}", std::process::id()));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
Self { root }
}
fn write(&self, path: &str, content: &str) {
let path = self.root.join(path);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, content).unwrap();
}
fn run(&self, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_asrch"))
.args(args)
.current_dir(&self.root)
.output()
.unwrap()
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
fn stdout(output: &Output) -> String {
assert!(
output.status.success(),
"status={:?}\nstderr={}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout.clone()).unwrap()
}
fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
#[test]
fn survey_compares_terms_without_printing_matches() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "alpha alpha\nbeta\n");
fixture.write("src/b.rs", "alpha\n");
let text = stdout(&fixture.run(&["survey", "--term", "alpha", "--term", "beta", "."]));
assert!(text.contains("survey:"));
assert!(text.contains(" terms: 2"));
assert!(text.contains("alpha"));
assert!(text.contains("2"));
assert!(text.contains("beta"));
assert!(!text.contains("alpha alpha"));
}
#[test]
fn survey_accepts_short_term_option() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "alpha\nbeta\n");
let text = stdout(&fixture.run(&["survey", "-t", "alpha", "-t", "beta", "."]));
assert!(text.contains(" terms: 2"));
assert!(text.contains("alpha"));
assert!(text.contains("beta"));
}
#[test]
fn survey_identifier_mode_avoids_partial_matches() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "erf performed erfc\n");
let text = stdout(&fixture.run(&["survey", "--term", "erf", ".", "--identifier"]));
assert!(text.contains("erf"));
assert!(text.contains("1"));
assert!(!text.contains("3"));
}
#[test]
fn survey_reports_overall_and_by_path_for_multiple_paths() {
let fixture = Fixture::new();
fixture.write("crates/rschem/src/a.rs", "cam_coef\ncam_coef\nhyb_cam\n");
fixture.write("crates/libxc/src/b.rs", "hyb_cam\n");
fixture.write("docs/rsh.md", "cam_coef\n");
let text = stdout(&fixture.run(&[
"survey",
"--term",
"cam_coef",
"--term",
"hyb_cam",
"crates/rschem",
"crates/libxc",
"docs",
"--identifier",
]));
assert!(text.contains("survey:"));
assert!(text.contains(" terms: 2"));
assert!(text.contains(" paths: 3"));
assert!(text.contains(" mode: identifier"));
assert!(text.contains("overall[term,matches,files,dominant_path]:"));
assert!(text.contains("by_path:"));
assert!(text.contains("crates/rschem[term,matches,files,top_directory]:"));
assert!(text.contains("crates/libxc[term,matches,files,top_directory]:"));
assert!(text.contains("docs[term,matches,files,top_directory]:"));
assert!(text.contains("cam_coef,3,2,crates/rschem"));
assert!(text.contains("cam_coef,2,1,src"));
assert!(text.contains("next: choose one useful term and path"));
}
#[test]
fn survey_default_line_budget_allows_full_multi_path_summary() {
let fixture = Fixture::new();
let mut args = vec!["survey"];
for term in ["alpha", "beta", "gamma", "delta", "epsilon"] {
args.extend(["--term", term]);
}
for index in 0..8 {
let path = format!("path_{index}/src/a.rs");
fixture.write(&path, "alpha\nbeta\ngamma\ndelta\nepsilon\n");
args.push(Box::leak(format!("path_{index}").into_boxed_str()));
}
let text = stdout(&fixture.run(&args));
assert!(text.contains("path_7[term,matches,files,top_directory]:"));
assert!(text.contains(" epsilon,1,1,src"));
assert!(!text.contains("[output truncated"));
assert!(text.lines().count() > 40);
}
#[test]
fn survey_uses_relative_detail_paths_inside_absolute_path_sections() {
let fixture = Fixture::new();
fixture.write("crates/app/src/config.rs", "AuthConfig\n");
let base = fixture.root.join("crates");
let base_text = base.to_str().unwrap();
let text = stdout(&fixture.run(&["survey", "--term", "AuthConfig", base_text, "--identifier"]));
assert!(text.contains(&format!(" {base_text}[term,matches,files,top_directory]:")));
assert!(text.contains(" AuthConfig,1,1,app/src"));
assert!(!text.contains(&format!(" AuthConfig,1,1,{base_text}")));
}
#[test]
fn survey_omits_zero_rows_in_by_path_output() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "alpha\n");
fixture.write("docs/a.md", "beta\n");
let text =
stdout(&fixture.run(&["survey", "--term", "alpha", "--term", "beta", "src", "docs"]));
let docs_section = text
.split(" docs[term,matches,files,top_directory]:")
.nth(1)
.unwrap()
.split(" src[term,matches,files,top_directory]:")
.next()
.unwrap();
assert!(docs_section.contains("beta"));
assert!(!docs_section.contains("\"alpha\""));
let src_section = text
.split(" src[term,matches,files,top_directory]:")
.nth(1)
.unwrap();
assert!(src_section.contains("alpha"));
assert!(!src_section.contains("beta,"));
}
#[test]
fn survey_quotes_terms_only_when_needed_for_toon_rows() {
let fixture = Fixture::new();
fixture.write("docs/a.md", "range separated\ncam_coef\n");
let text = stdout(&fixture.run(&[
"survey",
"--term",
"range separated",
"--term",
"cam_coef",
"docs",
]));
assert!(text.contains("\"range separated\",1,1,docs"));
assert!(text.contains("cam_coef,1,1,docs"));
}
#[test]
fn survey_warns_when_one_term_dominates() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "alpha\nalpha\nalpha\nalpha\nalpha\nbeta\n");
let text = stdout(&fixture.run(&["survey", "--term", "alpha", "--term", "beta"]));
assert!(text.contains("dominates"));
}
#[test]
fn survey_warns_about_short_partial_match_terms() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "erf performed\n");
let text = stdout(&fixture.run(&["survey", "--term", "erf"]));
assert!(text.contains("short partial-match"));
assert!(text.contains("--identifier"));
}
#[test]
fn survey_limits_the_number_of_terms() {
let fixture = Fixture::new();
let mut args = vec!["survey"];
for _ in 0..13 {
args.extend(["--term", "needle"]);
}
let output = fixture.run(&args);
assert!(!output.status.success());
assert!(stderr(&output).contains("at most 12 terms"));
}
#[test]
fn survey_limits_the_number_of_paths() {
let fixture = Fixture::new();
let mut args = vec!["survey", "--term", "needle"];
for index in 0..9 {
let path = format!("dir{index}");
fs::create_dir_all(fixture.root.join(&path)).unwrap();
args.push(Box::leak(path.into_boxed_str()));
}
let output = fixture.run(&args);
assert!(!output.status.success());
assert!(stderr(&output).contains("at most 8 paths"));
}
#[test]
fn survey_reports_term_and_path_limit_violations_together() {
let fixture = Fixture::new();
let mut args = vec!["survey"];
for _ in 0..13 {
args.extend(["--term", "needle"]);
}
args.extend(std::iter::repeat_n(".", 9));
let output = fixture.run(&args);
assert!(!output.status.success());
let error = stderr(&output);
assert!(error.contains("at most 12 terms"));
assert!(error.contains("at most 8 paths"));
}
#[test]
fn survey_accepts_term_and_path_limits_exactly() {
let fixture = Fixture::new();
let mut args = vec!["survey"];
for index in 0..12 {
args.push("--term");
args.push(Box::leak(format!("term{index}").into_boxed_str()));
}
for index in 0..8 {
let path = format!("path{index}");
fs::create_dir_all(fixture.root.join(&path)).unwrap();
args.push(Box::leak(path.into_boxed_str()));
}
let output = fixture.run(&args);
assert!(output.status.success(), "{}", stderr(&output));
}
#[test]
fn scout_defaults_to_fixed_string_search() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "call(foo)\ncallXfoo\n");
let text = stdout(&fixture.run(&["scout", "call(foo)"]));
assert!(text.contains("scout:"));
assert!(text.contains(" matches: 1"));
assert!(text.contains(" files: 1"));
}
#[test]
fn scout_summarizes_directories_and_files() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "needle\nneedle\n");
fixture.write("tests/b.rs", "needle\n");
let text = stdout(&fixture.run(&["scout", "needle"]));
assert!(text.contains(" matches: 3"));
assert!(text.contains(" files: 2"));
assert!(text.contains("top_directories[path,matches]:"));
assert!(text.contains("top_files[path,matches]:"));
}
#[test]
fn scout_uses_relative_distribution_paths_for_absolute_search_path() {
let fixture = Fixture::new();
fixture.write("crates/app/src/config.rs", "AuthConfig\nAuthConfig\n");
fixture.write("crates/lib/src/auth.rs", "AuthConfig\n");
let base = fixture.root.join("crates");
let base_text = base.to_str().unwrap();
let text = stdout(&fixture.run(&["scout", "AuthConfig", base_text, "--identifier"]));
assert!(text.contains(&format!(" path: {base_text}")));
assert!(text.contains(" app/src,2"));
assert!(text.contains(" app/src/config.rs,2"));
assert!(text.contains(" lib/src,1"));
assert!(!text.contains(&format!("{base_text}/app/src,2")));
}
#[test]
fn regex_or_is_rejected_for_single_query_commands() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "alpha\nbeta\n");
let output = fixture.run(&["scout", "alpha|beta", "--regex"]);
assert!(!output.status.success());
assert!(stderr(&output).contains("survey"));
}
#[test]
fn sample_shows_deterministic_cluster_context() {
let fixture = Fixture::new();
fixture.write(
"src/a.rs",
"before\nneedle one\nneedle two\nafter\n\nfar before\nneedle far\nfar after\n\nlast before\nneedle last\nlast after\n",
);
let text = stdout(&fixture.run(&["sample", "needle", "src/a.rs", "--clusters", "2"]));
assert!(text.contains("clusters=3 page=1/2 selected=2"));
assert!(text.contains("clusters[index,range,hits,first,last]:"));
assert!(text.contains("1,2..3,2,src/a.rs:2:1,src/a.rs:3:1"));
assert!(text.contains("-- src/a.rs:2:"));
assert!(text.contains("needle one"));
assert!(text.contains("needle far"));
assert!(!text.contains("needle last"));
assert!(text.contains("next: use --page 2"));
assert!(text.contains("before"));
}
#[test]
fn sample_pages_through_match_clusters() {
let fixture = Fixture::new();
fixture.write(
"src/a.rs",
"before\nneedle one\nneedle two\nafter\n\nfar before\nneedle far\nfar after\n\nlast before\nneedle last\nlast after\n",
);
let page_two = stdout(&fixture.run(&[
"sample",
"needle",
"src/a.rs",
"--clusters",
"2",
"--page",
"2",
]));
assert!(page_two.contains("clusters=3 page=2/2 selected=1"));
assert!(page_two.contains("needle last"));
let empty = stdout(&fixture.run(&[
"sample",
"needle",
"src/a.rs",
"--clusters",
"2",
"--page",
"3",
]));
assert!(empty.contains("No clusters on this page."));
assert!(empty.contains("next: use --page 1..2"));
}
#[test]
fn sample_requires_a_file_and_limits_cluster_option_range() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "needle\n");
fs::create_dir_all(fixture.root.join("src/dir")).unwrap();
let directory = fixture.run(&["sample", "needle", "src"]);
assert!(!directory.status.success());
assert!(stderr(&directory).contains("file path"));
let too_many = fixture.run(&["sample", "needle", "src/a.rs", "--clusters", "6"]);
assert!(!too_many.status.success());
assert!(stderr(&too_many).contains("1..=5"));
let zero_page = fixture.run(&["sample", "needle", "src/a.rs", "--page", "0"]);
assert!(!zero_page.status.success());
assert!(stderr(&zero_page).contains("--page"));
}
#[test]
fn sample_validates_all_numeric_option_boundaries() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "needle\n");
for clusters in ["1", "5"] {
let output = fixture.run(&["sample", "needle", "src/a.rs", "--clusters", clusters]);
assert!(
output.status.success(),
"clusters={clusters}: {}",
stderr(&output)
);
}
for clusters in ["0", "6", "-1", "many"] {
let output = fixture.run(&["sample", "needle", "src/a.rs", "--clusters", clusters]);
assert!(!output.status.success(), "clusters={clusters}");
assert!(
stderr(&output).contains("--clusters"),
"clusters={clusters}"
);
}
let page_one = fixture.run(&["sample", "needle", "src/a.rs", "--page", "1"]);
assert!(page_one.status.success(), "{}", stderr(&page_one));
for page in ["0", "-1", "first"] {
let output = fixture.run(&["sample", "needle", "src/a.rs", "--page", page]);
assert!(!output.status.success(), "page={page}");
assert!(stderr(&output).contains("--page"), "page={page}");
}
}
#[test]
fn sample_rejects_output_limit_options() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "needle\n");
let output = fixture.run(&["sample", "needle", "src/a.rs", "--max-lines", "10"]);
assert!(!output.status.success());
assert!(stderr(&output).contains("unknown option: --max-lines"));
}
#[test]
fn sample_context_error_points_to_show() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "needle\n");
let output = fixture.run(&["sample", "needle", "src/a.rs", "--context", "3"]);
assert!(!output.status.success());
let error = stderr(&output);
assert!(error.contains("sample does not accept --context"));
assert!(error.contains("asrch show"));
}
#[test]
fn show_requires_a_file_and_rejects_many_matches() {
let fixture = Fixture::new();
fixture.write("src/a.rs", &"needle\n".repeat(25));
fs::create_dir_all(fixture.root.join("src/dir")).unwrap();
let directory = fixture.run(&["show", "needle", "src"]);
assert!(!directory.status.success());
assert!(stderr(&directory).contains("file path"));
let broad = fixture.run(&["show", "needle", "src/a.rs"]);
assert!(!broad.status.success());
assert!(stderr(&broad).contains("too many matches"));
}
#[test]
fn show_prints_small_focused_snippets() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "before\nneedle\nafter\n");
let text = stdout(&fixture.run(&["show", "needle", "src/a.rs", "--context", "1"]));
assert!(text.contains("-- src/a.rs:2:"));
assert!(text.contains("> 2 | needle"));
assert!(text.contains(" 1 | before"));
}
#[test]
fn show_line_prints_a_requested_line_even_for_broad_queries() {
let fixture = Fixture::new();
let content = (1..=25)
.map(|line| format!("needle line {line}"))
.collect::<Vec<_>>()
.join("\n");
fixture.write("src/a.rs", &(content + "\n"));
let text = stdout(&fixture.run(&[
"show",
"needle",
"src/a.rs",
"--line",
"13",
"--context",
"1",
]));
assert!(text.contains("-- src/a.rs:13:"));
assert!(text.contains("> 13 | needle line 13"));
assert!(text.contains(" 12 | needle line 12"));
assert!(text.contains(" 14 | needle line 14"));
}
#[test]
fn show_context_is_structurally_bounded() {
let fixture = Fixture::new();
fixture.write(
"src/a.rs",
"ctx1\nctx2\nctx3\nctx4\nctx5\nneedle\nctx7\nctx8\nctx9\nctx10\nctx11\n",
);
let text = stdout(&fixture.run(&["show", "needle", "src/a.rs", "--context", "5"]));
assert!(text.contains("ctx11"));
assert!(text.contains("> 6 | needle"));
}
#[test]
fn show_validates_all_numeric_option_boundaries() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "before\nneedle\nafter\n");
for context in ["0", "5"] {
let output = fixture.run(&["show", "needle", "src/a.rs", "--context", context]);
assert!(
output.status.success(),
"context={context}: {}",
stderr(&output)
);
}
for context in ["6", "-1", "many"] {
let output = fixture.run(&["show", "needle", "src/a.rs", "--context", context]);
assert!(!output.status.success(), "context={context}");
assert!(stderr(&output).contains("--context"), "context={context}");
}
let line_one = fixture.run(&["show", "needle", "src/a.rs", "--line", "1"]);
assert!(line_one.status.success(), "{}", stderr(&line_one));
for line in ["0", "-1", "first"] {
let output = fixture.run(&["show", "needle", "src/a.rs", "--line", line]);
assert!(!output.status.success(), "line={line}");
assert!(stderr(&output).contains("--line"), "line={line}");
}
let outside = fixture.run(&["show", "needle", "src/a.rs", "--line", "4"]);
assert!(!outside.status.success());
assert!(stderr(&outside).contains("line is outside file"));
}
#[test]
fn search_mode_options_are_mutually_exclusive_for_every_command() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "needle\n");
let cases: &[&[&str]] = &[
&["survey", "--term", "needle", "--identifier", "--word"],
&["scout", "needle", "--identifier", "--regex"],
&["sample", "needle", "src/a.rs", "--word", "--regex"],
&["show", "needle", "src/a.rs", "--identifier", "--word"],
];
for args in cases {
let output = fixture.run(args);
assert!(!output.status.success(), "args={args:?}");
assert!(
stderr(&output).contains("cannot be used with"),
"args={args:?}: {}",
stderr(&output)
);
}
}
#[test]
fn required_positional_arguments_are_enforced_for_every_command() {
let fixture = Fixture::new();
for args in [
&["survey"][..],
&["scout"][..],
&["sample", "needle"][..],
&["show", "needle"][..],
] {
let output = fixture.run(args);
assert!(!output.status.success(), "args={args:?}");
assert!(stderr(&output).contains("required"), "args={args:?}");
}
}
#[test]
fn unknown_commands_print_the_top_level_help() {
let fixture = Fixture::new();
for command in ["count", "terms"] {
let output = fixture.run(&[command, "needle"]);
assert!(!output.status.success());
let error = stderr(&output);
assert!(error.contains(&format!("unknown command: {command}")));
assert!(error.contains("Usage:"));
assert!(error.contains("survey"));
assert!(error.contains("show"));
assert!(!error.contains("--clusters"));
}
}
#[test]
fn unknown_options_print_the_relevant_help() {
let fixture = Fixture::new();
let command = fixture.run(&["scout", "needle", "--unknown"]);
assert!(!command.status.success());
let error = stderr(&command);
assert!(error.contains("unknown option: --unknown"));
assert!(error.contains("Usage: asrch scout [OPTIONS] <query> [path]"));
assert!(!error.contains("asrch sample <query>"));
let top_level = fixture.run(&["--unknown"]);
assert!(!top_level.status.success());
let error = stderr(&top_level);
assert!(error.contains("unknown option: --unknown"));
assert!(error.contains("survey"));
assert!(error.contains("show"));
assert!(!error.contains("--clusters"));
}
#[test]
fn unknown_option_detection_uses_clap_option_definitions() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "needle\n");
let survey = fixture.run(&["survey", "-tneedle", "."]);
assert!(survey.status.success(), "{}", stderr(&survey));
let sample = fixture.run(&["sample", "needle", "src/a.rs", "--clusters=1"]);
assert!(sample.status.success(), "{}", stderr(&sample));
let show = fixture.run(&["show", "needle", "src/a.rs", "--context=0"]);
assert!(show.status.success(), "{}", stderr(&show));
}
#[test]
fn version_options_are_handled_by_clap() {
let fixture = Fixture::new();
for option in ["-V", "--version"] {
let text = stdout(&fixture.run(&[option]));
assert!(text.starts_with("asrch "));
assert!(text.contains(env!("CARGO_PKG_VERSION")));
}
}
#[test]
fn empty_queries_are_rejected() {
let fixture = Fixture::new();
let output = fixture.run(&["scout", ""]);
assert!(!output.status.success());
assert!(stderr(&output).contains("must not be empty"));
}
#[test]
fn noisy_paths_and_extensions_are_excluded_by_default() {
let fixture = Fixture::new();
fixture.write("src/keep.rs", "needle\n");
fixture.write(".git/config", "needle\n");
fixture.write("target/output.txt", "needle\n");
fixture.write("scratch/note.txt", "needle\n");
fixture.write("run.log", "needle\n");
fixture.write("data.jsonl", "needle\n");
fixture.write("data.xml", "needle\n");
let text = stdout(&fixture.run(&["scout", "needle"]));
assert!(text.contains(" matches: 1"));
assert!(text.contains(" files: 1"));
assert!(text.contains("src/keep.rs"));
assert!(!text.contains("target"));
}
#[test]
fn output_lines_are_clipped_to_800_bytes() {
let fixture = Fixture::new();
fixture.write("src/a.rs", &format!("needle {}\n", "x".repeat(20_000)));
let text = stdout(&fixture.run(&["show", "needle", "src/a.rs"]));
for line in text.lines() {
assert!(line.len() <= 800, "{} bytes", line.len());
}
}
#[test]
fn output_limit_options_are_removed() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "needle\n");
let lines = fixture.run(&["show", "needle", "src/a.rs", "--max-lines", "10"]);
assert!(!lines.status.success());
assert!(stderr(&lines).contains("unknown option: --max-lines"));
let bytes = fixture.run(&["show", "needle", "src/a.rs", "--max-bytes", "100"]);
assert!(!bytes.status.success());
assert!(stderr(&bytes).contains("unknown option: --max-bytes"));
}
#[test]
fn broad_queries_request_narrowing_instead_of_dumping_matches() {
let fixture = Fixture::new();
fixture.write("src/a.rs", &"needle\n".repeat(1_001));
let text = stdout(&fixture.run(&["sample", "needle", "src/a.rs"]));
assert!(text.lines().nth(1).unwrap().contains("Query is broad"));
assert!(text.contains("needle"));
}
#[test]
fn error_output_is_bounded() {
let fixture = Fixture::new();
let long_invalid_regex = format!("({}", "x".repeat(20_000));
let output = fixture.run(&["scout", &long_invalid_regex, "--regex"]);
assert!(!output.status.success());
assert!(output.stderr.len() <= 801);
}
#[test]
fn top_level_help_uses_the_standard_clap_summary() {
let fixture = Fixture::new();
let text = stdout(&fixture.run(&["--help"]));
for command in ["survey", "scout", "sample", "show"] {
assert!(text.contains(command));
}
for option in ["-h, --help", "-V, --version"] {
assert!(text.contains(option), "missing {option}");
}
for command_option in [
"--term",
"--clusters",
"--page",
"--line",
"--context",
"--identifier",
"--word",
"--regex",
] {
assert!(
!text.contains(command_option),
"unexpected {command_option}"
);
}
assert!(!text.contains("asrch terms"));
assert!(!text.contains("Hard output limits"));
assert!(text.contains("Requires ripgrep (`rg`)"));
assert!(text.contains("matching lines"));
assert!(text.contains("cannot be disabled"));
assert!(text.contains("800 bytes"));
}
#[test]
fn command_help_is_available_with_short_and_long_options() {
let fixture = Fixture::new();
for (command, expected) in [
("survey", "Per-path rows with zero matches are omitted"),
("scout", "top 5 directories and top 5 files"),
("sample", "One line of context"),
("show", "internal scan limit"),
] {
for help in ["-h", "--help"] {
let text = stdout(&fixture.run(&[command, help]));
assert!(text.contains(&format!("asrch {command}")));
assert!(text.contains(expected), "{command} {help}");
}
}
}
#[test]
fn command_help_documents_numeric_constraints() {
let fixture = Fixture::new();
for (command, expected) in [
("survey", "Repeatable up to 12 times"),
("sample", "Range: 1..=5"),
("sample", "Minimum: 1"),
("show", "Range: 0..=5"),
("show", "Minimum: 1"),
] {
let text = stdout(&fixture.run(&[command, "--help"]));
assert!(text.contains(expected), "{command}: missing {expected}");
}
}
#[test]
fn command_help_does_not_repeat_clap_default_values() {
let fixture = Fixture::new();
for (command, option, value) in [
("survey", "[path]", "."),
("scout", "[path]", "."),
("sample", "--clusters <N>", "3"),
("sample", "--page <N>", "1"),
("show", "--context <N>", "2"),
] {
let text = stdout(&fixture.run(&[command, "--help"]));
assert!(text.contains(option), "{command}: missing {option}");
assert!(
text.contains(&format!("[default: {value}]")),
"{command}: missing default {value}"
);
assert!(
!text.contains(&format!("Default: {value} [default: {value}]")),
"{command}: repeated default {value}"
);
}
}
#[test]
fn show_rejects_context_above_the_documented_range() {
let fixture = Fixture::new();
fixture.write("src/a.rs", "needle\n");
let output = fixture.run(&["show", "needle", "src/a.rs", "--context", "6"]);
assert!(!output.status.success());
assert!(stderr(&output).contains("0..=5"));
}
#[test]
fn test_fixture_paths_are_valid() {
let fixture = Fixture::new();
assert!(Path::new(&fixture.root).is_dir());
}