lernie 0.1.6

lernie: the operator seat — the window and wire client for a yog server
Documentation
//! **Reading the corpus** — the one walk over `corpus/`, shared by the two
//! replays that judge this seat's codec (`src/reply/tests/corpus.rs` for what
//! it reads, `src/verbs/tests/corpus.rs` for what it writes).
//!
//! It is here rather than beside either of them because the corpus is one
//! artifact and a second walk over it is a second opinion about what is in it.
//!
//! **Two provenances, told apart by the bytes and not by a list.** A file
//! either carries yog's own fixture envelope — `direction`, `shape`,
//! `protocol` and N `frames` — which makes it **vendored**, generated by the
//! boundary that is the protocol authority and copied in unmodified by
//! `scripts/refresh-corpus.sh`; or it is one bare frame this repository wrote,
//! which is how a frame captured off a live engine drops in and how the
//! malformed shapes upstream cannot generate are held. Neither form needs a
//! manifest to say which it is.
//!
//! **The protocol stamp is checked here, once, on every file read.** A shape
//! stamped past what [`PROTOCOL`] speaks is a corpus this build cannot be
//! judged by, and the refusal names both numbers — the same sentence the
//! version preface owes an operator, for the same reason.

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use serde_json::Value;

use crate::channel::hello::PROTOCOL;

/// The three assertion directories, which are the three outcomes a reply
/// frame can have. `corpus/README.md` is the contract they carry.
pub(crate) const CLASSES: [&str; 3] = ["answers", "refusals", "unreadable"];

/// The upstream envelope's discriminant: present exactly on a vendored file.
const DIRECTION: &str = "direction";

/// The corpus root, resolved off the manifest rather than the working
/// directory: `cargo test` runs from the crate root today and a harness that
/// changed that would silently empty the corpus.
pub(crate) fn root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("corpus")
}

/// Every `*.json` file in one corpus directory, by name.
///
/// **A directory that enumerates nothing fails here**, which is the second
/// direction every gate in this repo holds: a broken walk must not pass as a
/// clean corpus.
pub(crate) fn files(dir: &str) -> Vec<PathBuf> {
    let mut found: Vec<PathBuf> = std::fs::read_dir(root().join(dir))
        .unwrap_or_else(|e| panic!("corpus/{dir}: {e}"))
        .flatten()
        .map(|entry| entry.path())
        .filter(|path| path.extension().is_some_and(|ext| ext == "json"))
        .collect();
    found.sort();
    assert!(
        !found.is_empty(),
        "corpus/{dir} holds no file — the walk is broken, not the corpus"
    );
    found
}

/// One corpus file: where it came from, and the frames it carries.
pub(crate) struct Fixture {
    /// The file's own path, for a failure that has to name it.
    pub(crate) path: PathBuf,
    /// The upstream shape this file is the fixture of, or `None` for a frame
    /// this repository wrote. The file stem equals the shape when it is one,
    /// which is what lets `scripts/refresh-corpus.sh` find it by name.
    pub(crate) shape: Option<String>,
    /// The frames themselves — one for a bare file, N for a vendored one.
    pub(crate) frames: Vec<Value>,
}

/// Read one file into the frames it carries, checking its stamp on the way.
pub(crate) fn fixture(path: &Path) -> Fixture {
    let text = std::fs::read_to_string(path).expect("a corpus file");
    let value: Value = serde_json::from_str(&text)
        .unwrap_or_else(|e| panic!("{}: not JSON — {e}", path.display()));
    let Some(obj) = value.as_object().filter(|o| o.contains_key(DIRECTION)) else {
        return Fixture {
            path: path.to_owned(),
            shape: None,
            frames: vec![value],
        };
    };
    let stamp = obj["protocol"].as_u64().expect("a stamped fixture");
    assert!(
        stamp <= u64::from(PROTOCOL),
        "{} is stamped protocol {stamp} and this seat speaks {PROTOCOL} — \
         the corpus is ahead of this build; upgrade the seat",
        path.display()
    );
    let frames = obj["frames"].as_array().expect("frames").clone();
    assert!(
        !frames.is_empty(),
        "{} carries no frame — a fixture that replays nothing passes forever",
        path.display()
    );
    Fixture {
        path: path.to_owned(),
        shape: Some(obj["shape"].as_str().expect("a named shape").to_owned()),
        frames,
    }
}

/// **The standing shape record** — `corpus/shapes.json`, vendored whole: every
/// shape upstream has, with the field signature each last moved at.
pub(crate) struct Record {
    /// The version the corpus as a whole is for.
    pub(crate) protocol: u64,
    /// Each `<direction>/<shape>` key, and the signature under it.
    pub(crate) shapes: BTreeMap<String, Vec<String>>,
}

/// Read it. The corpus-wide protocol is checked by the caller that can say
/// what a mismatch means; the per-shape stamps are [`fixture`]'s.
pub(crate) fn record() -> Record {
    let text = std::fs::read_to_string(root().join("shapes.json")).expect("corpus/shapes.json");
    let value: Value = serde_json::from_str(&text).expect("shapes.json is JSON");
    Record {
        protocol: value["protocol"].as_u64().expect("a corpus protocol"),
        shapes: value["shapes"]
            .as_object()
            .expect("the shape record")
            .iter()
            .map(|(key, shape)| {
                let signature = shape["signature"]
                    .as_array()
                    .expect("a signature")
                    .iter()
                    .map(|field| field.as_str().expect("a field").to_owned())
                    .collect();
                (key.clone(), signature)
            })
            .collect(),
    }
}