Separate workspace crate at tooling/test-client/ importing bridge types from server crate. CLI: --connect, --replay, --text, --json, --quiet, --golden, --ticks. Exit codes: 0=success, 1=golden mismatch, 2=connection error. Golden file comparison with recursive JSON diff. JSONL replay loader for tick-scheduled input sending. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
105 lines
3.6 KiB
Rust
105 lines
3.6 KiB
Rust
// Golden file comparison for test-client.
|
|
// Compares the final ObserverSnapshot (as JSON) against a golden file.
|
|
// Reports field-by-field differences with JSON paths.
|
|
|
|
use serde_json::Value;
|
|
use settled_reach_server::bridge::types::ObserverSnapshot;
|
|
use std::path::Path;
|
|
|
|
/// Compare an ObserverSnapshot against a golden JSON file.
|
|
/// Returns a list of difference descriptions (empty = match).
|
|
pub fn compare_golden(
|
|
golden_path: &Path,
|
|
snapshot: &ObserverSnapshot,
|
|
) -> Result<Vec<String>, String> {
|
|
let golden_str = std::fs::read_to_string(golden_path).map_err(|e| {
|
|
format!(
|
|
"failed to read golden file {}: {}",
|
|
golden_path.display(),
|
|
e
|
|
)
|
|
})?;
|
|
let golden: Value = serde_json::from_str(&golden_str)
|
|
.map_err(|e| format!("failed to parse golden file: {}", e))?;
|
|
let actual: Value = serde_json::to_value(snapshot)
|
|
.map_err(|e| format!("failed to serialize snapshot: {}", e))?;
|
|
|
|
let mut diffs = Vec::new();
|
|
diff_values("", &golden, &actual, &mut diffs);
|
|
Ok(diffs)
|
|
}
|
|
|
|
fn diff_values(path: &str, expected: &Value, actual: &Value, diffs: &mut Vec<String>) {
|
|
match (expected, actual) {
|
|
(Value::Object(e), Value::Object(a)) => {
|
|
for key in e.keys() {
|
|
let child_path = if path.is_empty() {
|
|
format!(".{}", key)
|
|
} else {
|
|
format!("{}.{}", path, key)
|
|
};
|
|
match a.get(key) {
|
|
Some(av) => diff_values(&child_path, &e[key], av, diffs),
|
|
None => diffs.push(format!(
|
|
"{}: expected {}, got <missing>",
|
|
child_path,
|
|
format_value(&e[key])
|
|
)),
|
|
}
|
|
}
|
|
for key in a.keys() {
|
|
if !e.contains_key(key) {
|
|
let child_path = if path.is_empty() {
|
|
format!(".{}", key)
|
|
} else {
|
|
format!("{}.{}", path, key)
|
|
};
|
|
diffs.push(format!(
|
|
"{}: expected <missing>, got {}",
|
|
child_path,
|
|
format_value(&a[key])
|
|
));
|
|
}
|
|
}
|
|
}
|
|
(Value::Array(e), Value::Array(a)) => {
|
|
let max_len = e.len().max(a.len());
|
|
for i in 0..max_len {
|
|
let child_path = format!("{}[{}]", path, i);
|
|
match (e.get(i), a.get(i)) {
|
|
(Some(ev), Some(av)) => diff_values(&child_path, ev, av, diffs),
|
|
(Some(ev), None) => diffs.push(format!(
|
|
"{}: expected {}, got <missing>",
|
|
child_path,
|
|
format_value(ev)
|
|
)),
|
|
(None, Some(av)) => diffs.push(format!(
|
|
"{}: expected <missing>, got {}",
|
|
child_path,
|
|
format_value(av)
|
|
)),
|
|
(None, None) => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
if expected != actual {
|
|
diffs.push(format!(
|
|
"{}: expected {}, got {}",
|
|
path,
|
|
format_value(expected),
|
|
format_value(actual)
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn format_value(v: &Value) -> String {
|
|
match v {
|
|
Value::String(s) => format!("{:?}", s),
|
|
Value::Null => "null".to_string(),
|
|
other => other.to_string(),
|
|
}
|
|
}
|