use autumn_web::sim::scenario::{apply_ops, ops_strategy};
use autumn_web::sim::sweep::{SweepOutcome, sweep_proptest};
const DEFAULT_SEED_COUNT: u64 = 256;
fn seed_range(start: Option<&str>, count: Option<&str>) -> std::ops::Range<u64> {
let start = start
.and_then(|value| value.trim().parse().ok())
.unwrap_or(0);
let count = count
.and_then(|value| value.trim().parse().ok())
.unwrap_or(DEFAULT_SEED_COUNT);
start..start.saturating_add(count)
}
fn replay_command(scenario: &str, seed: u64) -> String {
format!(
" replay: AUTUMN_SIM_SCENARIOS={scenario} AUTUMN_SIM_SEED_START={seed} AUTUMN_SIM_SEEDS=1 \
cargo run -p autumn-web --release --features \"sim-testing,sqlite\" --bin sim-sweep",
)
}
fn available_scenarios() -> Vec<&'static str> {
let scenarios = vec!["account"];
#[cfg(feature = "sqlite")]
let scenarios = [scenarios.as_slice(), autumn_web::sim::fleet::SCENARIOS].concat();
scenarios
}
fn select_scenarios(
raw: Option<&str>,
available: &[&'static str],
) -> Result<Vec<&'static str>, String> {
let Some(raw) = raw.map(str::trim).filter(|raw| !raw.is_empty()) else {
return Ok(available.to_vec());
};
raw.split(',')
.map(str::trim)
.map(|name| {
available
.iter()
.find(|known| **known == name)
.copied()
.ok_or_else(|| {
format!(
"unknown scenario `{name}`; this build has {available:?} \
(the fleet scenarios need --features sqlite)"
)
})
})
.collect()
}
#[cfg_attr(not(feature = "sqlite"), allow(dead_code))]
fn count(raw: Option<&str>, default: u64) -> u64 {
raw.and_then(|value| value.trim().parse().ok())
.unwrap_or(default)
}
fn main() {
let seeds = seed_range(
std::env::var("AUTUMN_SIM_SEED_START").ok().as_deref(),
std::env::var("AUTUMN_SIM_SEEDS").ok().as_deref(),
);
let scenarios = match select_scenarios(
std::env::var("AUTUMN_SIM_SCENARIOS").ok().as_deref(),
&available_scenarios(),
) {
Ok(scenarios) => scenarios,
Err(error) => {
eprintln!("sim-sweep: {error}");
std::process::exit(1);
}
};
if seeds.is_empty() {
eprintln!("sim-sweep: EMPTY — AUTUMN_SIM_SEEDS swept zero seeds; nothing was tested");
std::process::exit(1);
}
if scenarios.contains(&"account") {
#[cfg(feature = "sqlite")]
sweep_account(seeds.clone());
#[cfg(not(feature = "sqlite"))]
sweep_account(seeds);
}
#[cfg(feature = "sqlite")]
{
let fleet: Vec<&str> = scenarios
.iter()
.copied()
.filter(|scenario| *scenario != "account")
.collect();
if !fleet.is_empty() {
let checks = count(std::env::var("AUTUMN_SIM_TRACE_CHECKS").ok().as_deref(), 16);
sweep_fleet(seeds, &fleet, checks);
}
}
}
fn sweep_account(seeds: std::ops::Range<u64>) {
let count = seeds.end - seeds.start;
let strategy = ops_strategy();
println!(
"sim-sweep: sweeping {count} seed(s) ({}..{}) against the account demo scenario",
seeds.start, seeds.end,
);
match sweep_proptest(seeds, &strategy, |_sim, ops| apply_ops(ops)) {
SweepOutcome::Passed { seeds_run } => {
println!("sim-sweep: PASSED — {seeds_run} seed(s), non-vacuous");
}
SweepOutcome::Failed { seeds_run, failure } => {
eprintln!("sim-sweep: FAILED after {seeds_run} seed(s)");
eprintln!("{failure}");
eprintln!("{}", replay_command("account", failure.seed));
std::process::exit(1);
}
SweepOutcome::Vacuous {
seeds_run,
unsatisfied,
} => {
eprintln!(
"sim-sweep: VACUOUS — {seeds_run} seed(s) all passed, but sometimes! label(s) \
were observed and never satisfied across the whole sweep: {}",
unsatisfied.into_iter().collect::<Vec<_>>().join(", ")
);
std::process::exit(1);
}
SweepOutcome::Empty => {
eprintln!("sim-sweep: EMPTY — the account sweep ran zero seeds; nothing was tested");
std::process::exit(1);
}
}
}
#[cfg(feature = "sqlite")]
fn sweep_fleet(seeds: std::ops::Range<u64>, scenarios: &[&str], checks: u64) {
use autumn_web::sim::fleet::{FleetOutcome, sweep};
println!(
"sim-sweep: sweeping {} seed(s) ({}..{}) against {scenarios:?}; the first {checks} \
seed(s) run twice for the trace check",
seeds.end - seeds.start,
seeds.start,
seeds.end,
);
match sweep(seeds, scenarios, checks) {
FleetOutcome::Passed { runs } => {
println!("sim-sweep: PASSED — {runs} framework run(s), non-vacuous");
}
FleetOutcome::Failed {
scenario,
seed,
reason,
} => {
eprintln!("sim-sweep: FAILED — scenario `{scenario}`, seed {seed}");
eprintln!("{reason}");
eprintln!("{}", replay_command(&scenario, seed));
std::process::exit(1);
}
FleetOutcome::Nondeterministic {
scenario,
seed,
diff,
} => {
eprintln!(
"sim-sweep: NONDETERMINISTIC — scenario `{scenario}`, seed {seed} logged a \
different trace on its second run"
);
eprintln!("{diff}");
eprintln!("{}", replay_command(&scenario, seed));
std::process::exit(1);
}
FleetOutcome::Vacuous { runs, unsatisfied } => {
eprintln!(
"sim-sweep: VACUOUS — {runs} framework run(s) passed, but no run reached: {}",
unsatisfied.into_iter().collect::<Vec<_>>().join(", ")
);
std::process::exit(1);
}
other => {
eprintln!("sim-sweep: unexpected outcome {other:?}");
std::process::exit(1);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn env_value<'a>(command: &'a str, key: &str) -> &'a str {
command
.split_whitespace()
.find_map(|word| word.strip_prefix(key)?.strip_prefix('='))
.unwrap_or_else(|| panic!("replay command must set {key}: {command}"))
}
#[test]
fn replay_command_sweeps_exactly_the_failing_seed() {
let command = replay_command("account", 300);
let seeds = seed_range(
Some(env_value(&command, "AUTUMN_SIM_SEED_START")),
Some(env_value(&command, "AUTUMN_SIM_SEEDS")),
);
assert_eq!(seeds, 300..301, "{command}");
}
#[test]
fn replay_command_reaches_the_largest_sweepable_seed() {
let seed = u64::MAX - 1;
let command = replay_command("jobs", seed);
let seeds = seed_range(
Some(env_value(&command, "AUTUMN_SIM_SEED_START")),
Some(env_value(&command, "AUTUMN_SIM_SEEDS")),
);
assert_eq!(seeds, seed..u64::MAX, "{command}");
}
#[test]
fn seed_range_defaults_to_the_first_256_seeds() {
assert_eq!(seed_range(None, None), 0..DEFAULT_SEED_COUNT);
assert_eq!(
seed_range(Some("0x10"), Some("many")),
0..DEFAULT_SEED_COUNT,
"hex and garbage fall back to the defaults",
);
}
#[test]
fn replay_command_names_the_scenario() {
let command = replay_command("lock", 9);
assert_eq!(env_value(&command, "AUTUMN_SIM_SCENARIOS"), "lock");
assert!(command.contains("sqlite"), "{command}");
}
#[test]
fn scenarios_default_to_all_and_reject_unknown_names() {
let available = ["account", "jobs"];
assert_eq!(select_scenarios(None, &available).unwrap(), available);
assert_eq!(select_scenarios(Some(" "), &available).unwrap(), available);
assert_eq!(
select_scenarios(Some("jobs, account"), &available).unwrap(),
["jobs", "account"]
);
let error = select_scenarios(Some("jobs,nope"), &available).unwrap_err();
assert!(
error.contains("nope") && error.contains("sqlite"),
"{error}"
);
}
#[test]
fn counts_fall_back_to_the_default() {
assert_eq!(count(None, 16), 16);
assert_eq!(count(Some("x"), 16), 16);
assert_eq!(count(Some(" 3 "), 16), 3);
}
#[test]
fn seed_range_starts_where_asked() {
assert_eq!(seed_range(Some("40"), Some("8")), 40..48);
}
}