use std::collections::BTreeMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FixtureDecl {
#[serde(rename = "testID")]
pub test_id: String,
pub signal: SignalMatcher,
#[serde(rename = "timeoutMs")]
pub timeout_ms: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignalMatcher {
pub regex: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub level: Option<String>,
}
#[derive(Debug, Deserialize)]
struct RegistryFile {
#[serde(default)]
#[allow(dead_code)]
version: u32,
fixtures: BTreeMap<String, FixtureDecl>,
}
#[derive(Clone, Debug)]
pub struct FixtureRegistry {
fixtures: BTreeMap<String, FixtureDecl>,
}
#[derive(Debug, Error)]
pub enum RegistryError {
#[error("read {path}: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("parse {path}: {detail}")]
Malformed { path: String, detail: String },
#[error("fixture not found: {id} (registered: {known:?})")]
NotFound { id: String, known: Vec<String> },
}
impl FixtureRegistry {
pub fn load(path: &Path) -> Result<Self, RegistryError> {
let text = std::fs::read_to_string(path).map_err(|source| RegistryError::Io {
path: path.display().to_string(),
source,
})?;
let file: RegistryFile =
serde_json::from_str(&text).map_err(|e| RegistryError::Malformed {
path: path.display().to_string(),
detail: e.to_string(),
})?;
Ok(Self {
fixtures: file.fixtures,
})
}
pub fn get(&self, id: &str) -> Option<&FixtureDecl> {
self.fixtures.get(id)
}
pub fn require(&self, id: &str) -> Result<&FixtureDecl, RegistryError> {
self.get(id).ok_or_else(|| RegistryError::NotFound {
id: id.to_string(),
known: self.fixtures.keys().cloned().collect(),
})
}
pub fn ids(&self) -> impl Iterator<Item = &String> {
self.fixtures.keys()
}
pub fn from_map(fixtures: BTreeMap<String, FixtureDecl>) -> Self {
Self { fixtures }
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn write(tmp: &TempDir, name: &str, content: &str) -> std::path::PathBuf {
let p = tmp.path().join(name);
std::fs::write(&p, content).unwrap();
p
}
#[test]
fn load_single_fixture() {
let tmp = TempDir::new().unwrap();
let path = write(
&tmp,
"reg.json",
r#"{
"version": 1,
"fixtures": {
"prime-search-history": {
"testID": "qa-chip-prime-search-history",
"signal": {
"regex": "\\[fixture\\] prime-search-history: seeded (\\d+) rows",
"level": "log"
},
"timeoutMs": 8000
}
}
}"#,
);
let reg = FixtureRegistry::load(&path).unwrap();
let f = reg.require("prime-search-history").unwrap();
assert_eq!(f.test_id, "qa-chip-prime-search-history");
assert_eq!(f.timeout_ms, 8000);
assert_eq!(f.signal.level.as_deref(), Some("log"));
}
#[test]
fn not_found_lists_available() {
let tmp = TempDir::new().unwrap();
let path = write(
&tmp,
"reg.json",
r#"{"fixtures":{
"a":{"testID":"t-a","signal":{"regex":"a"},"timeoutMs":100},
"b":{"testID":"t-b","signal":{"regex":"b"},"timeoutMs":100}
}}"#,
);
let reg = FixtureRegistry::load(&path).unwrap();
let err = reg.require("nope").unwrap_err();
match err {
RegistryError::NotFound { known, .. } => {
assert_eq!(known, vec!["a".to_string(), "b".to_string()]);
}
other => panic!("expected NotFound, got {other:?}"),
}
}
#[test]
fn malformed_json_surfaces_error() {
let tmp = TempDir::new().unwrap();
let path = write(&tmp, "reg.json", "not json");
let err = FixtureRegistry::load(&path).unwrap_err();
matches!(err, RegistryError::Malformed { .. });
}
#[test]
fn multi_fixture_ids_iterate_deterministic() {
let tmp = TempDir::new().unwrap();
let path = write(
&tmp,
"reg.json",
r#"{"fixtures":{
"z":{"testID":"z","signal":{"regex":"."},"timeoutMs":100},
"a":{"testID":"a","signal":{"regex":"."},"timeoutMs":100},
"m":{"testID":"m","signal":{"regex":"."},"timeoutMs":100}
}}"#,
);
let reg = FixtureRegistry::load(&path).unwrap();
let ids: Vec<&String> = reg.ids().collect();
assert_eq!(ids, vec!["a", "m", "z"]);
}
}