// 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, 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) { 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 ", 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 , 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 ", child_path, format_value(ev) )), (None, Some(av)) => diffs.push(format!( "{}: expected , 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(), } }