mock-upcloud 0.1.3

A faithful fake of the UpCloud API 1.3 — the lies included — backed by real KVM guests
Documentation
//! `mock-upcloud` — run the fake provider.
//!
//!     mock-upcloud --port 8099                 # faithful timings: 98–105 s to create a server
//!     mock-upcloud --port 8099 --speed 0       # virtual time: the same order, no waiting
//!     mock-upcloud --port 8099 --seed 41       # seeded fault weather, replayable from 41 alone
//!     mock-upcloud --port 8099 --arm out-of-stock --arm price-transport-reset
//!     mock-upcloud --self-check                # calibrate: can every fault report the OPPOSITE?
//!
//! Then point the real plugin at it:
//!
//!     UPCLOUD_API_BASE=http://127.0.0.1:8099 UPCLOUD_TOKEN=ucat_mock monetize-server-vetra …
//!
//! `--speed` is in thousandths (`1000` = real time) so the knob is an integer
//! and two mocks with the same knob are the same mock.

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(", ")
                    )
                })?);
            }
            // **The calibration, and it exits non-zero.** Every variant in
            // `Fault::ALL`, driven twice — disarmed and armed — with the two
            // observations printed under each other. A fault whose two runs
            // observe the SAME thing is named and fails the process: it arms,
            // it parses, it fires, and nothing a caller can see changes, which
            // is a coverage claim the crate cannot honour. Nothing is spawned
            // and no socket is bound; see `mock_upcloud::self_check`.
            // Every call, in order, on stderr. See `Mock::log`.
            "--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?;
        // One line, and it names the seed, because a run that cannot be replayed
        // is a run that found nothing.
        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(())
}

/// **The help text, GENERATED from [`Fault::ALL`].**
///
/// It was a hand-written list, and a hand-written list of a thing that grows is
/// a list that lies. This one named `grant-created-field`, a fault that has
/// never existed under that name — the real one is `withhold-created-field` and
/// it does the OPPOSITE, so a person who read the help and typed what it said
/// got `no such fault` if they were lucky and the wrong behaviour if they
/// weren't — and it listed 9 of the 16 faults there were. There are 20 now.
///
/// So the faults are walked, not written: each line is `Fault::name`,
/// `Fault::seeded_rate_per_mille` (or `by name` at rate 0) and
/// `Fault::summary`, whose match is exhaustive. A new variant appears here the
/// moment it compiles, and there is no second place to forget.
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::*;

    /// **The help names every fault, and it cannot go stale.** This is the
    /// regression for the `grant-created-field` line: the text used to name a
    /// fault that did not exist and to omit seven that did.
    #[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());
        }
        // Every wire name in the faults block parses back to a fault: nothing
        // is advertised that `--arm` would refuse.
        for line in h.lines() {
            // Exactly two spaces: that is the indent an entry has, and a
            // wrapped continuation line has more.
            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");
    }
}