use crate::runner::{Backend, Database, HealthCheck, Settings, Verbosity};
#[derive(Debug)]
pub enum CliOutcome {
Success(Settings),
Help(String),
ParseError(String),
}
pub fn apply_cli_args<I>(default_settings: impl FnOnce() -> Settings, args: I) -> CliOutcome
where
I: IntoIterator<Item = String>,
{
match try_apply_cli_args(default_settings, args) {
Ok(s) => CliOutcome::Success(s),
Err(CliError::Help(msg)) => CliOutcome::Help(msg),
Err(CliError::Parse(msg)) => CliOutcome::ParseError(format!("{}\n\n{}", msg, usage())),
}
}
#[derive(Debug)]
enum CliError {
Help(String),
Parse(String),
}
fn try_apply_cli_args<I>(
default_settings: impl FnOnce() -> Settings,
args: I,
) -> Result<Settings, CliError>
where
I: IntoIterator<Item = String>,
{
let mut iter = args.into_iter();
let _program = iter.next();
let mut args: Vec<String> = iter.collect();
if let Some(name) = extract_profile(&mut args)? {
set_default_profile(&name)?;
}
let mut settings = default_settings();
let mut i = 0;
while i < args.len() {
let arg = args[i].as_str();
match arg {
"--help" | "-h" => {
return Err(CliError::Help(usage()));
}
"--seed" => {
let value = next_value(&args, &mut i, "--seed")?;
if value == "none" {
settings = settings.seed(None);
} else {
let n: u64 = value.parse().map_err(|_| {
CliError::Parse(format!(
"--seed expects an integer or 'none', got {value:?}"
))
})?;
settings = settings.seed(Some(n));
}
}
"--verbosity" => {
let value = next_value(&args, &mut i, "--verbosity")?;
let v = parse_verbosity(&value)?;
settings = settings.verbosity(v);
}
"--derandomize" => {
let value = next_value(&args, &mut i, "--derandomize")?;
let b = parse_bool(&value, "--derandomize")?;
settings = settings.derandomize(b);
}
"--database" => {
let value = next_value(&args, &mut i, "--database")?;
match value.as_str() {
"disabled" => settings = settings.database(None),
"default" => settings.database = Database::Unset,
_ => settings = settings.database(Some(value)),
}
}
"--suppress-health-check" => {
let value = next_value(&args, &mut i, "--suppress-health-check")?;
let checks = parse_health_check(&value)?;
settings = settings.suppress_health_check(checks);
}
"--backend" => {
let value = next_value(&args, &mut i, "--backend")?;
let backend = parse_backend(&value)?;
settings = settings.backend(backend);
}
_ => {
return Err(CliError::Parse(format!("Unknown argument: {arg}")));
}
}
i += 1;
}
Ok(settings)
}
fn next_value(args: &[String], i: &mut usize, name: &str) -> Result<String, CliError> {
*i += 1;
args.get(*i)
.cloned()
.ok_or_else(|| CliError::Parse(format!("{name} requires a value")))
}
fn extract_profile(args: &mut Vec<String>) -> Result<Option<String>, CliError> {
let Some(pos) = args.iter().position(|a| a == "--profile") else {
return Ok(None);
};
args.remove(pos);
if pos >= args.len() {
return Err(CliError::Parse("--profile requires a value".to_string()));
}
let name = args.remove(pos);
if args.iter().any(|a| a == "--profile") {
return Err(CliError::Parse(
"--profile may be given at most once".to_string(),
));
}
Ok(Some(name))
}
fn set_default_profile(name: &str) -> Result<(), CliError> {
crate::ffi::set_default_profile(Some(name)).map_err(CliError::Parse)?;
if let Err(message) = crate::ffi::settings_from_profile(None) {
crate::ffi::set_default_profile(None).unwrap();
return Err(CliError::Parse(message));
}
Ok(())
}
fn parse_verbosity(s: &str) -> Result<Verbosity, CliError> {
match s {
"quiet" => Ok(Verbosity::Quiet),
"normal" => Ok(Verbosity::Normal),
"verbose" => Ok(Verbosity::Verbose),
"debug" => Ok(Verbosity::Debug),
other => Err(CliError::Parse(format!(
"--verbosity expects one of quiet|normal|verbose|debug, got {other:?}"
))),
}
}
fn parse_backend(s: &str) -> Result<Backend, CliError> {
match s {
"default" => Ok(Backend::Default),
"urandom" => Ok(Backend::Urandom),
other => Err(CliError::Parse(format!(
"--backend expects one of default|urandom, got {other:?}"
))),
}
}
fn parse_bool(s: &str, name: &str) -> Result<bool, CliError> {
crate::runner::parse_bool(s)
.ok_or_else(|| CliError::Parse(format!("{name} expects true|false, got {s:?}")))
}
fn parse_health_check(s: &str) -> Result<Vec<HealthCheck>, CliError> {
if s == "all" {
return Ok(HealthCheck::all().to_vec());
}
let mut checks = Vec::new();
for part in s.split(',') {
let part = part.trim();
let hc = match part {
"filter_too_much" => HealthCheck::FilterTooMuch,
"too_slow" => HealthCheck::TooSlow,
"test_cases_too_large" => HealthCheck::TestCasesTooLarge,
"large_initial_test_case" => HealthCheck::LargeInitialTestCase,
other => {
return Err(CliError::Parse(format!(
"--suppress-health-check does not recognise {other:?}. \
Known names: all, filter_too_much, too_slow, test_cases_too_large, large_initial_test_case"
)));
}
};
checks.push(hc);
}
Ok(checks)
}
fn usage() -> String {
let mut s = String::new();
s.push_str("Usage: <program> [options]\n");
s.push('\n');
s.push_str("Hegel property-based testing binary. Runs exactly one test case.\n");
s.push('\n');
s.push_str("Options:\n");
s.push_str(
" --profile <NAME> Set the default settings profile, like\n\
\x20 the HEGEL_DEFAULT_PROFILE environment\n\
\x20 variable; other flags apply on top\n",
);
s.push_str(
" --seed <N|none> Seed for randomisation ('none' for unset)\n",
);
s.push_str(" --verbosity <LEVEL> quiet | normal | verbose | debug\n");
s.push_str(" --derandomize <true|false> Use a deterministic derived seed\n");
s.push_str(
" --database <PATH|disabled|default> Database path for failing-example storage\n",
);
s.push_str(
" --suppress-health-check <NAMES> Comma-separated health check names, or 'all'\n",
);
s.push_str(
" --backend <default|urandom> Randomness source (urandom: the Antithesis fuzzer, else /dev/urandom)\n",
);
s.push_str(" -h, --help Show this help and exit\n");
s
}
#[cfg(test)]
#[path = "../tests/embedded/cli_tests.rs"]
mod tests;