haematite-cli 0.21.7

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use super::super::{Config, Entry, Launch, Phase, Shared, State};
use super::{LINE_BYTES, Log, drain, read_lines};
use std::io::{self, Cursor, Write};
use std::sync::{Arc, Condvar, Mutex, atomic::AtomicBool};

type TestResult = Result<(), Box<dyn std::error::Error>>;
struct Writer {
    full: bool,
}
impl Write for Writer {
    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
        if self.full {
            Err(io::Error::other("fixture log is full"))
        } else {
            Ok(bytes.len())
        }
    }
    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

#[test]
fn review_1_log_failure_is_one_incident_across_restarts_until_a_write_succeeds() {
    let failed = Arc::new(AtomicBool::new(false));
    let mut first = Log {
        writer: Writer { full: true },
        failed: Arc::clone(&failed),
    };
    assert!(first.append(b"one").is_some());
    assert!(first.append(b"two").is_none());
    let mut restarted = Log {
        writer: Writer { full: true },
        failed,
    };
    assert!(restarted.append(b"three").is_none());
    restarted.writer.full = false;
    assert!(restarted.append(b"four").is_none());
    restarted.writer.full = true;
    assert!(restarted.append(b"five").is_some());
}

#[test]
fn review_7_an_oversized_ready_prefix_is_never_announced() -> TestResult {
    let mut bytes = b"fixture ready false".to_vec();
    bytes.resize(1_048_576, b'x');
    bytes.extend_from_slice(b"\nfixture ready actual\n");
    let mut log = Log {
        writer: Vec::new(),
        failed: Arc::new(AtomicBool::new(false)),
    };
    let mut seen = Vec::new();
    drain(&mut Cursor::new(bytes), &mut log, "fixture", |line| {
        seen.push(line.to_vec());
        Ok(())
    })?;
    assert_eq!(seen, vec![b"fixture ready actual\n".to_vec()]);
    assert_eq!(
        log.writer.len(),
        LINE_BYTES + b"fixture ready actual\n".len()
    );
    Ok(())
}

#[test]
fn review_2_old_generation_keeps_draining_without_marking_the_replacement_ready() -> TestResult {
    let shared = Shared {
        state: Mutex::new(State {
            stopped: false,
            entries: vec![Entry {
                phase: Phase::NotReady,
                generation: 2,
                ..Entry::default()
            }],
        }),
        changed: Condvar::new(),
    };
    let config: Config = serde_json::from_value(
        serde_json::json!({"name":"fixture", "program":"/unused", "args":[],"ready":"fixture ready", "log":"/unused-log", "give_up_after":3}),
    )?;
    let mut old = Vec::new();
    read_lines(
        &shared,
        0,
        Launch {
            generation: 1,
            started: std::time::Instant::now(),
        },
        &config,
        Cursor::new(b"fixture ready old\nold stderr still drains\n"),
        &mut old,
        Arc::new(AtomicBool::new(false)),
    )?;
    assert_eq!(old, b"fixture ready old\nold stderr still drains\n");
    assert!(shared.state.lock().map_err(|e| e.to_string())?.entries[0].phase == Phase::NotReady);
    read_lines(
        &shared,
        0,
        Launch {
            generation: 2,
            started: std::time::Instant::now(),
        },
        &config,
        Cursor::new(b"fixture ready new\n"),
        Vec::new(),
        Arc::new(AtomicBool::new(false)),
    )?;
    let mut state = shared.state.lock().map_err(|e| e.to_string())?;
    assert!(state.entries[0].phase == Phase::Ready);
    state.entries[0].phase = Phase::Exited;
    drop(state);
    read_lines(
        &shared,
        0,
        Launch {
            generation: 2,
            started: std::time::Instant::now(),
        },
        &config,
        Cursor::new(b"fixture ready after exit\n"),
        Vec::new(),
        Arc::new(AtomicBool::new(false)),
    )?;
    assert!(shared.state.lock().map_err(|e| e.to_string())?.entries[0].phase == Phase::Exited);
    Ok(())
}