fusible 0.20.0

Electronic linter: a fuse that blows to block firmware code generation
use std::process::Command;
use std::time::{Duration, Instant};

use super::hub::ClientHub;
use super::runs::{Runs, Status};

use crate::test_shell::{shell, sleeping};

fn exit(code: i32) -> Command {
    shell(&format!("exit {code}"), &format!("exit {code}"))
}

fn wait_until_done(runs: &Runs) -> serde_json::Value {
    let deadline = Instant::now() + Duration::from_secs(5);
    loop {
        let snapshot: serde_json::Value = serde_json::from_str(&runs.snapshot()).unwrap();
        let running = snapshot
            .as_array()
            .unwrap()
            .iter()
            .any(|r| r["status"] == "running");
        if !running {
            return snapshot;
        }
        assert!(Instant::now() < deadline, "run did not finish");
        std::thread::sleep(Duration::from_millis(20));
    }
}

#[test]
fn run_collects_both_streams_and_maps_the_exit_code() {
    let runs = Runs::default();
    let hub = ClientHub::default();
    let rx = hub.subscribe(Vec::new);
    runs.start(
        &hub,
        "demo".into(),
        shell(
            "echo out; echo err >&2; exit 1",
            "echo out& >&2 echo err& exit 1",
        ),
    )
    .unwrap();
    let snapshot = wait_until_done(&runs);
    assert_eq!(snapshot[0]["status"], "fuse_blown");
    let mut lines: Vec<&str> = snapshot[0]["lines"]
        .as_array()
        .unwrap()
        .iter()
        .map(|l| l.as_str().unwrap())
        .collect();
    lines.sort_unstable();
    assert_eq!(lines, ["err", "out"]);
    let events: Vec<String> = rx
        .try_iter()
        .map(|m| String::from_utf8(m).unwrap())
        .collect();
    assert!(events
        .iter()
        .any(|e| e.contains(r#""event":"log""#) && e.contains("out")));
    assert!(events.last().unwrap().contains(r#""status":"fuse_blown""#));
}

#[test]
fn only_one_run_goes_at_a_time() {
    let runs = Runs::default();
    let hub = ClientHub::default();
    runs.start(&hub, "slow".into(), sleeping(1)).unwrap();
    assert!(runs.start(&hub, "second".into(), exit(0)).is_err());
    wait_until_done(&runs);
    assert!(runs.start(&hub, "third".into(), exit(0)).is_ok());
}

#[test]
fn unspawnable_command_is_an_error_run() {
    let runs = Runs::default();
    let hub = ClientHub::default();
    runs.start(&hub, "missing".into(), Command::new("/nonexistent/fusible"))
        .unwrap();
    let snapshot = wait_until_done(&runs);
    assert_eq!(snapshot[0]["status"], "error");
    assert!(snapshot[0]["lines"][0]
        .as_str()
        .unwrap()
        .contains("cannot start"));
}

#[test]
fn run_events_carry_the_label_when_the_run_starts_and_when_it_ends() {
    let runs = Runs::default();
    let hub = ClientHub::default();
    let rx = hub.subscribe(Vec::new);
    runs.start(&hub, "check main".into(), exit(0)).unwrap();
    wait_until_done(&runs);
    let events: Vec<String> = rx
        .try_iter()
        .map(|m| String::from_utf8(m).unwrap())
        .collect();
    let first = &events[0];
    assert!(first.contains(r#""event":"run""#) && first.contains(r#""label":"check main""#));
    assert!(first.contains(r#""status":"running""#));
    let last = events.last().unwrap();
    assert!(last.contains(r#""label":"check main""#) && last.contains(r#""status":"ok""#));
}

#[test]
fn a_run_that_hangs_is_killed_and_frees_the_slot() {
    let runs = Runs::default();
    let hub = ClientHub::default();
    runs.start_with(
        &hub,
        "hang".into(),
        sleeping(30),
        Duration::from_millis(200),
    )
    .unwrap();
    let snapshot = wait_until_done(&runs);
    assert_eq!(snapshot[0]["status"], "error");
    assert!(runs.start(&hub, "next".into(), exit(0)).is_ok());
}

#[test]
fn exit_codes_follow_the_fuse_contract() {
    assert_eq!(Status::from_code(Some(0)), Status::Ok);
    assert_eq!(Status::from_code(Some(1)), Status::FuseBlown);
    assert_eq!(Status::from_code(Some(2)), Status::Error);
    assert_eq!(Status::from_code(None), Status::Error);
}