use mock_upcloud::{Clock, Estate, Fault, Faults, Mock};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut port: u16 = 8099;
let mut speed: u64 = 1000;
let mut seed: Option<u64> = None;
let mut arm: Vec<Fault> = vec![];
let mut log = false;
let mut args = std::env::args().skip(1);
while let Some(a) = args.next() {
match a.as_str() {
"--port" => port = args.next().ok_or("--port wants a number")?.parse()?,
"--speed" => speed = args.next().ok_or("--speed wants a number")?.parse()?,
"--seed" => seed = Some(args.next().ok_or("--seed wants a number")?.parse()?),
"--arm" => {
let n = args.next().ok_or("--arm wants a fault name")?;
arm.push(Fault::parse(&n).ok_or_else(|| {
format!(
"no such fault: {n}\nthe faults are: {}",
Fault::ALL.iter().map(|f| f.name()).collect::<Vec<_>>().join(", ")
)
})?);
}
"--log" => log = true,
"--self-check" => {
let report = mock_upcloud::self_check();
print!("{}", report.text());
if !report.ok() {
std::process::exit(1);
}
return Ok(());
}
"--help" | "-h" => {
println!("{}", help());
return Ok(());
}
other => return Err(format!("unknown argument: {other}").into()),
}
}
let faults = match seed {
Some(s) => Faults::seeded(s),
None => Faults::none(),
};
for f in &arm {
faults.arm(*f);
}
let estate = Estate::new(Clock::new(speed), faults, seed.unwrap_or(0));
let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?;
rt.block_on(async move {
let mock = if log { mock_upcloud::http::Mock::logging(estate) } else { Mock::new(estate) };
let (bound, h) = mock_upcloud::serve(mock, &format!("127.0.0.1:{port}")).await?;
println!(
"mock-upcloud listening http://127.0.0.1:{bound} speed={speed}‰ seed={} armed={}",
seed.map(|s| s.to_string()).unwrap_or_else(|| "none".into()),
if arm.is_empty() {
"stale-vnc-port (the provider's normal)".to_string()
} else {
arm.iter().map(|f| f.name()).collect::<Vec<_>>().join(",")
}
);
h.await?;
Ok::<(), Box<dyn std::error::Error>>(())
})?;
Ok(())
}
fn help() -> String {
let mut out = String::from(HEAD);
let width = Fault::ALL.iter().map(|f| f.name().len()).max().unwrap_or(0);
for f in Fault::ALL {
let weather = if f.seeded_rate_per_mille() == 0 {
"by name".to_string()
} else {
format!("{}\u{2030}", f.seeded_rate_per_mille())
};
out.push_str(&format!(
" {:<width$} {:>8}{} {}\n",
f.name(),
weather,
if f.sticky() { " sticky" } else { " " },
f.summary(),
width = width,
));
}
out.push_str(TAIL);
out
}
const HEAD: &str = "\
mock-upcloud — the UpCloud API 1.3, faithfully wrong
--port <n> listen here (default 8099)
--speed <n> virtual ms per real ms, in thousandths: 1000 = faithful, 0 = instant
--seed <n> seeded fault weather; the run replays from this number alone
--arm <fault> arm one fault deterministically (repeatable)
--log print every call, in order, with each server's state, on stderr
--self-check drive every fault disarmed AND armed, print the table, exit non-zero if any
of them changes nothing a caller can see
faults — the rate column is seeded weather; `by name` means it never arrives as
weather and must be asked for with --arm, which is how this crate marks a
hypothesis or a guess rather than a measurement:
";
const TAIL: &str = "
`sticky` means the fault keeps firing once it has fired, until it is disarmed:
a retry loop must not be able to outwait it. That is what was measured.
";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_help_names_every_fault_and_invents_none() {
let h = help();
for f in Fault::ALL {
assert!(h.contains(f.name()), "{} is missing from --help:\n{h}", f.name());
assert!(h.contains(f.summary()), "{} has no line in --help", f.name());
}
for line in h.lines() {
if !line.starts_with(" ") || line.starts_with(" ") {
continue;
}
let Some(word) = line.split_whitespace().next() else { continue };
if word.starts_with("--") {
continue;
}
assert!(Fault::parse(word).is_some(), "--help advertises `{word}`, which --arm does not know");
}
assert!(!h.contains("grant-created-field"), "the fault that never existed is gone");
}
}