use beam::actor::Addr;
use beam::message::Message;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct WireFixture {
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub category: String,
pub input: String,
pub expected: Expected,
#[serde(default)]
pub allow_public_space: bool,
}
#[derive(Debug, Deserialize)]
pub struct Expected {
pub parses: bool,
#[serde(default)]
pub kind: Option<String>,
#[serde(default)]
pub souls: Vec<String>,
#[serde(default)]
pub fields: BTreeMap<String, Vec<String>>,
#[serde(default)]
pub values: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
#[serde(default)]
pub timestamps: BTreeMap<String, BTreeMap<String, f64>>,
#[serde(default)]
pub error: Option<String>,
}
pub fn load_fixtures(base: &Path) -> Vec<WireFixture> {
let mut fixtures = Vec::new();
collect_fixtures(base, &mut fixtures);
fixtures.sort_by(|a, b| a.name.cmp(&b.name));
fixtures
}
fn collect_fixtures(dir: &Path, out: &mut Vec<WireFixture>) {
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(e) => panic!("failed to read fixture dir {}: {e}", dir.display()),
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_fixtures(&path, out);
} else if path.extension().is_some_and(|ext| ext == "json") {
let content = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
let fixture: WireFixture = serde_json::from_str(&content)
.unwrap_or_else(|e| panic!("failed to parse fixture {}: {e}", path.display()));
out.push(fixture);
}
}
}
pub fn run_fixture(fixture: &WireFixture, from: Addr, allow_public_space: bool) {
let result = Message::try_from(&fixture.input, from, allow_public_space);
if !fixture.expected.parses {
assert!(
result.is_err(),
"[{}] expected error but parsed successfully",
fixture.name
);
if let Some(expected_err) = &fixture.expected.error {
let actual = result.unwrap_err();
assert!(
actual.contains(expected_err.as_str()),
"[{}] expected error containing {:?}, got {:?}",
fixture.name,
expected_err,
actual
);
}
return;
}
let messages = result.unwrap_or_else(|e| {
panic!(
"[{}] expected parse success but got error: {e}",
fixture.name
)
});
let has_assertions = fixture.expected.kind.is_some()
|| !fixture.expected.souls.is_empty()
|| !fixture.expected.fields.is_empty()
|| !fixture.expected.values.is_empty()
|| !fixture.expected.timestamps.is_empty();
if messages.is_empty() {
assert!(
!has_assertions,
"[{}] expected at least one message but got empty vec",
fixture.name
);
return;
}
let msg = &messages[0];
if let Some(expected_kind) = &fixture.expected.kind {
let actual_kind = match msg {
Message::Put(_) => "Put",
Message::Get(_) => "Get",
Message::Hi { .. } => "Hi",
Message::RtcSignal(_) => "RtcSignal",
Message::BatchPut(_) => "BatchPut",
Message::Flush(_) => "Flush",
_ => "Other",
};
assert_eq!(
actual_kind, expected_kind,
"[{}] message kind mismatch",
fixture.name
);
}
if let Message::Put(put) = msg {
let actual_souls: Vec<&String> = put.updated_nodes.keys().collect();
if !fixture.expected.souls.is_empty() {
assert_eq!(
actual_souls.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
fixture
.expected
.souls
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>(),
"[{}] souls mismatch",
fixture.name
);
}
for (soul, expected_fields) in &fixture.expected.fields {
let children = put
.updated_nodes
.get(soul)
.unwrap_or_else(|| panic!("[{}] soul {soul} not in parsed put", fixture.name));
let actual_fields: Vec<&String> = children.keys().collect();
assert_eq!(
actual_fields.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
expected_fields
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>(),
"[{}] fields mismatch for soul {soul}",
fixture.name
);
}
for (soul, expected_values) in &fixture.expected.values {
let children = put
.updated_nodes
.get(soul)
.unwrap_or_else(|| panic!("[{}] soul {soul} not in parsed put", fixture.name));
for (key, expected_val) in expected_values {
let node_data = children
.get(key)
.unwrap_or_else(|| panic!("[{}] key {key} not in soul {soul}", fixture.name));
let actual_json: serde_json::Value = node_data.value.clone().into();
assert_eq!(
actual_json, *expected_val,
"[{}] value mismatch for {soul}.{key}",
fixture.name
);
}
}
for (soul, expected_ts) in &fixture.expected.timestamps {
let children = put
.updated_nodes
.get(soul)
.unwrap_or_else(|| panic!("[{}] soul {soul} not in parsed put", fixture.name));
for (key, expected_ts_val) in expected_ts {
let node_data = children
.get(key)
.unwrap_or_else(|| panic!("[{}] key {key} not in soul {soul}", fixture.name));
assert_eq!(
node_data.updated_at, *expected_ts_val,
"[{}] timestamp mismatch for {soul}.{key}",
fixture.name
);
}
}
}
if let Message::Get(get) = msg {
if !fixture.expected.souls.is_empty() {
assert_eq!(
get.node_id, fixture.expected.souls[0],
"[{}] get node_id mismatch",
fixture.name
);
}
}
if let Message::Hi { peer_id, .. } = msg {
if !fixture.expected.souls.is_empty() {
assert_eq!(
peer_id, &fixture.expected.souls[0],
"[{}] Hi peer_id mismatch",
fixture.name
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_wire_fixtures() {
let base = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("wire")
.join("fixtures");
let fixtures = load_fixtures(&base);
assert!(
!fixtures.is_empty(),
"no wire fixtures found in {} — did you create fixture files?",
base.display()
);
for fixture in &fixtures {
run_fixture(fixture, Addr::noop(), fixture.allow_public_space);
}
}
}