use std::path::PathBuf;
use clap::Parser;
use crate::error::Error;
#[derive(Debug, Clone, Parser)]
#[command(
name = "cargo-lihaaf",
bin_name = "cargo lihaaf",
version,
about = "Fast, parallel, non-flaky proc-macro test harness",
long_about = "Fast, parallel, non-flaky proc-macro test harness for compile-fail \
and compile-pass fixtures. The consumer crate is built once as a \
Rust dynamic library at session startup; each fixture is a \
per-fixture rustc invocation that links the dylib via --extern. \
See `target/lihaaf/manifest.json` for the dylib metadata after \
the first run. Configuration: `[package.metadata.lihaaf]` in the \
consumer's Cargo.toml."
)]
pub struct Cli {
#[arg(long)]
pub bless: bool,
#[arg(long)]
pub filter: Vec<String>,
#[arg(short = 'j', long = "jobs", value_parser = parse_jobs)]
pub jobs: Option<u32>,
#[arg(long, value_name = "NAME")]
pub suite: Vec<String>,
#[arg(long)]
pub no_cache: bool,
#[arg(long, value_name = "PATH")]
pub manifest_path: Option<PathBuf>,
#[arg(long)]
pub list: bool,
#[arg(short = 'q', long)]
pub quiet: bool,
#[arg(short = 'v', long)]
pub verbose: bool,
#[arg(long)]
pub use_symlink: bool,
#[arg(long)]
pub keep_output: bool,
}
fn parse_jobs(s: &str) -> Result<u32, String> {
let n: u32 = s
.parse()
.map_err(|_| format!("`{s}` is not a non-negative integer"))?;
if n == 0 {
return Err(
"must be a positive integer (`-j 0` is rejected; omit `-j` to use the default)"
.to_string(),
);
}
Ok(n)
}
pub fn parse_from(argv: Vec<String>) -> Result<Cli, Error> {
use clap::error::ErrorKind;
match Cli::try_parse_from(argv) {
Ok(cli) => Ok(cli),
Err(e) => {
let kind = e.kind();
let exit_code = match kind {
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => 0,
_ => 2,
};
let message = e.to_string();
let _ = e.print();
Err(Error::Cli {
clap_exit_code: exit_code,
message,
})
}
}
}
impl Cli {
pub fn effective_bless(&self) -> bool {
if self.bless {
return true;
}
std::env::var("LIHAAF_OVERWRITE")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(args: &[&str]) -> Cli {
let argv: Vec<String> = std::iter::once("cargo-lihaaf".to_owned())
.chain(args.iter().map(|s| s.to_string()))
.collect();
parse_from(argv).expect("parse must succeed")
}
#[test]
fn defaults_are_safe_posture() {
let c = parse(&[]);
assert!(!c.bless);
assert!(c.filter.is_empty());
assert!(c.jobs.is_none());
assert!(!c.no_cache);
assert!(c.manifest_path.is_none());
assert!(!c.list);
assert!(!c.quiet);
assert!(!c.verbose);
assert!(!c.use_symlink);
assert!(!c.keep_output);
}
#[test]
fn filter_accumulates() {
let c = parse(&["--filter", "phase7", "--filter", "phase8"]);
assert_eq!(c.filter, vec!["phase7".to_string(), "phase8".to_string()]);
}
#[test]
fn jobs_short_long() {
assert_eq!(parse(&["-j", "4"]).jobs, Some(4));
assert_eq!(parse(&["--jobs", "8"]).jobs, Some(8));
}
#[test]
fn jobs_zero_is_rejected_per_spec_section_5_2() {
let argv: Vec<String> = ["cargo-lihaaf", "-j", "0"]
.iter()
.map(|s| s.to_string())
.collect();
let err = parse_from(argv).expect_err("`-j 0` must be rejected");
match err {
Error::Cli { message, .. } => {
assert!(
message.contains("positive integer"),
"diagnostic must explain the requirement: {message}"
);
}
other => panic!("expected Cli error, got {other:?}"),
}
}
#[test]
fn jobs_long_form_zero_also_rejected() {
let argv: Vec<String> = ["cargo-lihaaf", "--jobs", "0"]
.iter()
.map(|s| s.to_string())
.collect();
assert!(parse_from(argv).is_err());
}
#[test]
fn bless_via_env_when_flag_absent() {
let prev = std::env::var("LIHAAF_OVERWRITE").ok();
unsafe {
std::env::set_var("LIHAAF_OVERWRITE", "1");
}
let c = parse(&[]);
assert!(c.effective_bless());
unsafe {
match prev {
Some(v) => std::env::set_var("LIHAAF_OVERWRITE", v),
None => std::env::remove_var("LIHAAF_OVERWRITE"),
}
}
}
}