use std::collections::BTreeSet;
use std::fmt;
use std::path::Path;
use serde_json::Value;
pub const CANONICAL_PLACEHOLDER: &str = "<volatile>";
pub const UPDATE_GOLDENS_ENV: &str = "FSNOW_UPDATE_GOLDENS";
#[derive(Clone, Debug)]
pub struct GoldenConfig {
volatile_exact: BTreeSet<String>,
volatile_suffixes: Vec<String>,
}
const DEFAULT_VOLATILE_EXACT: &[&str] = &[
"host",
"hostname",
"pid",
"ppid",
"tid",
"thread_id",
"now",
"uptime",
"trace_id",
"run_id",
"session_id",
"span_id",
"parent_id",
"correlation_id",
"nonce",
"uuid",
"etag",
"seed",
"content_hash",
"payload_hash",
"result_hash",
"normalized_sql_hash",
"schema_hash",
];
const DEFAULT_VOLATILE_SUFFIXES: &[&str] = &[
"_at",
"_ms",
"_ns",
"_us",
"_secs",
"_seconds",
"_time",
"_timestamp",
"_ts",
"_duration",
"_elapsed",
"_latency",
"_host",
"_hostname",
"_sha256",
"_fingerprint",
"_etag",
"_uuid",
];
impl Default for GoldenConfig {
fn default() -> Self {
Self {
volatile_exact: DEFAULT_VOLATILE_EXACT
.iter()
.map(|k| (*k).to_owned())
.collect(),
volatile_suffixes: DEFAULT_VOLATILE_SUFFIXES
.iter()
.map(|s| (*s).to_owned())
.collect(),
}
}
}
impl GoldenConfig {
#[must_use]
pub fn strict() -> Self {
Self {
volatile_exact: BTreeSet::new(),
volatile_suffixes: Vec::new(),
}
}
#[must_use]
pub fn with_volatile_key(mut self, key: impl Into<String>) -> Self {
self.volatile_exact.insert(key.into().to_ascii_lowercase());
self
}
#[must_use]
pub fn with_volatile_suffix(mut self, suffix: impl Into<String>) -> Self {
self.volatile_suffixes
.push(suffix.into().to_ascii_lowercase());
self
}
#[must_use]
pub fn is_volatile(&self, key: &str) -> bool {
let lowered = key.to_ascii_lowercase();
self.volatile_exact.contains(&lowered)
|| self
.volatile_suffixes
.iter()
.any(|suffix| lowered.ends_with(suffix.as_str()))
}
}
#[must_use]
pub fn canonicalize(value: &Value, cfg: &GoldenConfig) -> Value {
match value {
Value::Object(map) => {
let mut out = serde_json::Map::new();
for (key, child) in map {
if cfg.is_volatile(key) {
out.insert(key.clone(), Value::String(CANONICAL_PLACEHOLDER.to_owned()));
} else {
out.insert(key.clone(), canonicalize(child, cfg));
}
}
Value::Object(out)
}
Value::Array(items) => {
Value::Array(items.iter().map(|item| canonicalize(item, cfg)).collect())
}
other => other.clone(),
}
}
#[must_use]
pub fn to_canonical_json(value: &Value, cfg: &GoldenConfig) -> String {
let canonical = canonicalize(value, cfg);
let mut out = String::new();
write_canonical(&canonical, &mut out);
out
}
#[must_use]
pub fn canonical_bytes(value: &Value, cfg: &GoldenConfig) -> Vec<u8> {
to_canonical_json(value, cfg).into_bytes()
}
#[must_use]
pub fn normalize_line_endings(bytes: &[u8]) -> Vec<u8> {
let mut normalized = Vec::with_capacity(bytes.len());
let mut index = 0usize;
while index < bytes.len() {
match bytes[index] {
b'\r' if bytes.get(index + 1) == Some(&b'\n') => {
normalized.push(b'\n');
index += 2;
}
b'\r' => {
normalized.push(b'\n');
index += 1;
}
byte => {
normalized.push(byte);
index += 1;
}
}
}
normalized
}
fn write_canonical(value: &Value, out: &mut String) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(true) => out.push_str("true"),
Value::Bool(false) => out.push_str("false"),
Value::Number(number) => out.push_str(&number.to_string()),
Value::String(text) => write_json_string(text, out),
Value::Array(items) => {
out.push('[');
for (index, item) in items.iter().enumerate() {
if index > 0 {
out.push(',');
}
write_canonical(item, out);
}
out.push(']');
}
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
out.push('{');
for (index, key) in keys.iter().enumerate() {
if index > 0 {
out.push(',');
}
write_json_string(key, out);
out.push(':');
if let Some(child) = map.get(*key) {
write_canonical(child, out);
}
}
out.push('}');
}
}
}
fn write_json_string(text: &str, out: &mut String) {
match serde_json::to_string(text) {
Ok(escaped) => out.push_str(&escaped),
Err(_) => out.push_str("\"<unserializable-string>\""),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MismatchKind {
Value,
Type,
Length,
MissingKey,
ExtraKey,
}
impl MismatchKind {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Value => "value mismatch",
Self::Type => "type mismatch",
Self::Length => "array length mismatch",
Self::MissingKey => "missing key",
Self::ExtraKey => "unexpected key",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FloatBits {
pub expected: f64,
pub actual: f64,
pub expected_bits: u64,
pub actual_bits: u64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct GoldenMismatch {
pub path: String,
pub kind: MismatchKind,
pub expected: String,
pub actual: String,
pub float_bits: Option<FloatBits>,
}
impl fmt::Display for GoldenMismatch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"golden {} at {}: expected {}, got {}",
self.kind.label(),
self.path,
self.expected,
self.actual
)?;
if let Some(bits) = self.float_bits {
write!(
f,
"\n ieee754 expected: {} = {:#018x}\n ieee754 actual: {} = {:#018x}",
bits.expected, bits.expected_bits, bits.actual, bits.actual_bits
)?;
}
Ok(())
}
}
impl std::error::Error for GoldenMismatch {}
pub fn compare(expected: &Value, actual: &Value, cfg: &GoldenConfig) -> Result<(), GoldenMismatch> {
let expected = canonicalize(expected, cfg);
let actual = canonicalize(actual, cfg);
compare_canonical(&expected, &actual, "$")
}
fn compare_canonical(expected: &Value, actual: &Value, path: &str) -> Result<(), GoldenMismatch> {
match (expected, actual) {
(Value::Null, Value::Null) => Ok(()),
(Value::Bool(left), Value::Bool(right)) => {
if left == right {
Ok(())
} else {
Err(value_mismatch(
path,
left.to_string(),
right.to_string(),
None,
))
}
}
(Value::String(left), Value::String(right)) => {
if left == right {
Ok(())
} else {
Err(value_mismatch(
path,
format!("{left:?}"),
format!("{right:?}"),
None,
))
}
}
(Value::Number(left), Value::Number(right)) => {
if left.to_string() == right.to_string() {
Ok(())
} else {
let float_bits = match (left.as_f64(), right.as_f64()) {
(Some(expected), Some(actual)) => Some(FloatBits {
expected,
actual,
expected_bits: expected.to_bits(),
actual_bits: actual.to_bits(),
}),
_ => None,
};
Err(value_mismatch(
path,
left.to_string(),
right.to_string(),
float_bits,
))
}
}
(Value::Array(left), Value::Array(right)) => {
if left.len() != right.len() {
return Err(GoldenMismatch {
path: path.to_owned(),
kind: MismatchKind::Length,
expected: left.len().to_string(),
actual: right.len().to_string(),
float_bits: None,
});
}
for (index, (left_item, right_item)) in left.iter().zip(right.iter()).enumerate() {
compare_canonical(left_item, right_item, &format!("{path}[{index}]"))?;
}
Ok(())
}
(Value::Object(left), Value::Object(right)) => {
let mut keys: BTreeSet<&String> = left.keys().collect();
keys.extend(right.keys());
for key in keys {
match (left.get(key), right.get(key)) {
(Some(left_child), Some(right_child)) => {
compare_canonical(left_child, right_child, &format!("{path}.{key}"))?;
}
(Some(left_child), None) => {
return Err(GoldenMismatch {
path: format!("{path}.{key}"),
kind: MismatchKind::MissingKey,
expected: short_render(left_child),
actual: "<absent>".to_owned(),
float_bits: None,
});
}
(None, Some(right_child)) => {
return Err(GoldenMismatch {
path: format!("{path}.{key}"),
kind: MismatchKind::ExtraKey,
expected: "<absent>".to_owned(),
actual: short_render(right_child),
float_bits: None,
});
}
(None, None) => {}
}
}
Ok(())
}
_ => Err(GoldenMismatch {
path: path.to_owned(),
kind: MismatchKind::Type,
expected: type_name(expected).to_owned(),
actual: type_name(actual).to_owned(),
float_bits: None,
}),
}
}
fn value_mismatch(
path: &str,
expected: String,
actual: String,
float_bits: Option<FloatBits>,
) -> GoldenMismatch {
GoldenMismatch {
path: path.to_owned(),
kind: MismatchKind::Value,
expected,
actual,
float_bits,
}
}
fn type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
fn short_render(value: &Value) -> String {
let rendered = value.to_string();
if rendered.len() > 80 {
let mut end = 79;
while end > 0 && !rendered.is_char_boundary(end) {
end -= 1;
}
format!("{}…", &rendered[..end])
} else {
rendered
}
}
#[must_use]
pub fn has_cr(text: &str) -> bool {
text.contains('\r')
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CrlfViolation {
pub byte_offset: usize,
pub line: usize,
}
impl fmt::Display for CrlfViolation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"carriage return at byte {} (line {}); goldens must be LF-only (eol=lf)",
self.byte_offset, self.line
)
}
}
impl std::error::Error for CrlfViolation {}
pub fn assert_no_cr(text: &str) -> Result<(), CrlfViolation> {
assert_lf_only(text.as_bytes())
}
pub fn assert_lf_only(bytes: &[u8]) -> Result<(), CrlfViolation> {
let mut line = 1usize;
for (offset, byte) in bytes.iter().enumerate() {
match byte {
b'\r' => {
return Err(CrlfViolation {
byte_offset: offset,
line,
});
}
b'\n' => line += 1,
_ => {}
}
}
Ok(())
}
#[derive(Debug)]
pub enum GoldenError {
Io(std::io::Error),
Utf8,
Crlf(CrlfViolation),
Parse(serde_json::Error),
Mismatch(GoldenMismatch),
}
impl fmt::Display for GoldenError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(error) => write!(f, "golden io error: {error}"),
Self::Utf8 => write!(f, "golden file is not valid UTF-8"),
Self::Crlf(violation) => write!(f, "golden file has CRLF: {violation}"),
Self::Parse(error) => write!(f, "golden file is not valid JSON: {error}"),
Self::Mismatch(mismatch) => write!(f, "{mismatch}"),
}
}
}
impl std::error::Error for GoldenError {}
impl From<std::io::Error> for GoldenError {
fn from(error: std::io::Error) -> Self {
Self::Io(error)
}
}
pub fn write_golden(path: &Path, value: &Value, cfg: &GoldenConfig) -> Result<(), GoldenError> {
let mut body = to_canonical_json(value, cfg);
body.push('\n');
std::fs::write(path, body)?;
Ok(())
}
pub fn check_golden_file(
path: &Path,
actual: &Value,
cfg: &GoldenConfig,
) -> Result<(), GoldenError> {
if std::env::var(UPDATE_GOLDENS_ENV).as_deref() == Ok("1") {
return write_golden(path, actual, cfg);
}
let bytes = std::fs::read(path)?;
let normalized = normalize_line_endings(&bytes);
let text = std::str::from_utf8(&normalized).map_err(|_| GoldenError::Utf8)?;
let expected: Value = serde_json::from_str(text).map_err(GoldenError::Parse)?;
compare(&expected, actual, cfg).map_err(GoldenError::Mismatch)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn canonicalization_zeroes_volatile_and_sorts_keys() {
let cfg = GoldenConfig::default();
let run_a = json!({
"trace_id": "abc-123",
"started_at": "2026-06-24T00:00:00Z",
"host": "builder-7",
"result_hash": "deadbeef",
"rows": 3,
"name": "scan",
});
let run_b = json!({
"name": "scan",
"rows": 3,
"trace_id": "zzz-999",
"started_at": "2026-06-25T11:22:33Z",
"host": "builder-42",
"result_hash": "cafef00d",
});
assert!(compare(&run_a, &run_b, &cfg).is_ok());
assert_eq!(
to_canonical_json(&run_a, &cfg),
to_canonical_json(&run_b, &cfg)
);
}
#[test]
fn float_mismatch_reports_ieee754_bits() -> Result<(), String> {
let cfg = GoldenConfig::strict();
let expected = json!({ "ratio": 0.3_f64 });
let actual = json!({ "ratio": 0.1_f64 + 0.2_f64 });
let mismatch = compare(&expected, &actual, &cfg)
.err()
.ok_or_else(|| "0.1 + 0.2 should not byte-equal 0.3".to_owned())?;
let bits = mismatch
.float_bits
.ok_or_else(|| "numeric mismatch must carry IEEE-754 bits".to_owned())?;
assert_ne!(bits.expected_bits, bits.actual_bits);
assert!(format!("{mismatch}").contains("ieee754"));
Ok(())
}
#[test]
fn crlf_discipline_locates_carriage_return() -> Result<(), String> {
assert!(assert_no_cr("clean\nlf\nonly\n").is_ok());
let violation = assert_no_cr("first\r\nsecond")
.err()
.ok_or_else(|| "CR must be rejected".to_owned())?;
assert_eq!(violation.byte_offset, 5);
assert_eq!(violation.line, 1);
Ok(())
}
#[test]
fn golden_comparison_normalizes_crlf_checkout_bytes() -> Result<(), Box<dyn std::error::Error>>
{
let cfg = GoldenConfig::strict();
let dir = std::env::temp_dir().join("fsnow-harness-golden-crlf");
std::fs::create_dir_all(&dir)?;
let path = dir.join("crlf.golden.json");
std::fs::write(&path, b"{\r\n \"a\": 1\r\n}\r\n")?;
check_golden_file(&path, &json!({ "a": 1 }), &cfg)?;
assert!(assert_lf_only(&std::fs::read(&path)?).is_err());
std::fs::remove_dir_all(&dir)?;
Ok(())
}
#[test]
fn normalize_line_endings_handles_crlf_and_bare_cr() {
assert_eq!(normalize_line_endings(b"a\r\nb\rc\n"), b"a\nb\nc\n");
}
#[test]
fn type_mismatch_is_reported_with_type_names() -> Result<(), String> {
let cfg = GoldenConfig::strict();
let mismatch = compare(&json!({ "v": 1 }), &json!({ "v": "1" }), &cfg)
.err()
.ok_or_else(|| "number vs string must mismatch".to_owned())?;
assert_eq!(mismatch.kind, MismatchKind::Type);
assert_eq!(mismatch.path, "$.v");
assert_eq!(mismatch.expected, "number");
assert_eq!(mismatch.actual, "string");
Ok(())
}
#[test]
fn array_length_mismatch_is_reported() -> Result<(), String> {
let cfg = GoldenConfig::strict();
let mismatch = compare(&json!({ "xs": [1, 2, 3] }), &json!({ "xs": [1, 2] }), &cfg)
.err()
.ok_or_else(|| "length difference must mismatch".to_owned())?;
assert_eq!(mismatch.kind, MismatchKind::Length);
assert_eq!(mismatch.path, "$.xs");
assert_eq!(mismatch.expected, "3");
assert_eq!(mismatch.actual, "2");
Ok(())
}
#[test]
fn missing_key_diff_truncates_long_multibyte_values_without_panicking() -> Result<(), String> {
let cfg = GoldenConfig::strict();
let long_value = format!("{}é{}", "a".repeat(77), "b".repeat(40));
let mismatch = compare(&json!({ "k": long_value }), &json!({}), &cfg)
.err()
.ok_or_else(|| "missing key must mismatch".to_owned())?;
assert_eq!(mismatch.kind, MismatchKind::MissingKey);
assert_eq!(mismatch.path, "$.k");
assert!(
mismatch.expected.ends_with('…'),
"long value should be ellipsized: {}",
mismatch.expected
);
Ok(())
}
#[test]
fn missing_and_extra_keys_are_distinguished() -> Result<(), String> {
let cfg = GoldenConfig::strict();
let missing = compare(&json!({ "a": 1, "b": 2 }), &json!({ "a": 1 }), &cfg)
.err()
.ok_or_else(|| "missing key must mismatch".to_owned())?;
assert_eq!(missing.kind, MismatchKind::MissingKey);
assert_eq!(missing.path, "$.b");
let extra = compare(&json!({ "a": 1 }), &json!({ "a": 1, "c": 3 }), &cfg)
.err()
.ok_or_else(|| "extra key must mismatch".to_owned())?;
assert_eq!(extra.kind, MismatchKind::ExtraKey);
assert_eq!(extra.path, "$.c");
Ok(())
}
#[test]
fn strict_config_does_not_zero_identifiers() -> Result<(), String> {
let left = json!({ "trace_id": "aaa" });
let right = json!({ "trace_id": "bbb" });
assert!(compare(&left, &right, &GoldenConfig::default()).is_ok());
let mismatch = compare(&left, &right, &GoldenConfig::strict())
.err()
.ok_or_else(|| "strict compare must see differing ids".to_owned())?;
assert_eq!(mismatch.path, "$.trace_id");
Ok(())
}
#[test]
fn stable_command_id_is_not_zeroed_by_default() -> Result<(), String> {
let cfg = GoldenConfig::default();
let run = compare(
&json!({ "command_id": "query.run" }),
&json!({ "command_id": "catalog.scan" }),
&cfg,
);
let mismatch = run
.err()
.ok_or_else(|| "command_id must stay stable".to_owned())?;
assert_eq!(mismatch.path, "$.command_id");
Ok(())
}
#[test]
fn stable_receipt_hash_is_not_zeroed_by_default() -> Result<(), String> {
let cfg = GoldenConfig::default();
assert!(!cfg.is_volatile("receipt_hash"));
let run = compare(
&json!({ "receipt_hash": "sha256:aaa" }),
&json!({ "receipt_hash": "sha256:bbb" }),
&cfg,
);
let mismatch = run
.err()
.ok_or_else(|| "receipt_hash must stay stable".to_owned())?;
assert_eq!(mismatch.path, "$.receipt_hash");
assert_eq!(mismatch.kind, MismatchKind::Value);
Ok(())
}
#[test]
fn domain_temporal_keys_are_not_zeroed_by_default() -> Result<(), String> {
let cfg = GoldenConfig::default();
for key in ["date", "time", "timestamp", "today"] {
assert!(
!cfg.is_volatile(key),
"{key} can be Snowflake row data and must not be hidden by default"
);
}
let mismatch = compare(
&json!({ "date": "2026-06-24" }),
&json!({ "date": "2026-06-25" }),
&cfg,
)
.err()
.ok_or_else(|| "domain date drift must mismatch".to_owned())?;
assert_eq!(mismatch.path, "$.date");
assert_eq!(mismatch.kind, MismatchKind::Value);
Ok(())
}
#[test]
fn generic_temporal_key_can_be_zeroed_when_fixture_opts_in() {
let cfg = GoldenConfig::default().with_volatile_key("timestamp");
assert!(
compare(
&json!({ "timestamp": "2026-06-24T00:00:00Z" }),
&json!({ "timestamp": "2026-06-25T00:00:00Z" }),
&cfg,
)
.is_ok()
);
}
#[test]
fn known_payload_hash_fields_are_zeroed_by_default() {
let cfg = GoldenConfig::default();
for key in [
"content_hash",
"payload_hash",
"result_hash",
"normalized_sql_hash",
"schema_hash",
] {
assert!(cfg.is_volatile(key), "{key} should be volatile");
}
}
#[test]
fn canonical_json_is_sorted_compact_and_lf_only() -> Result<(), String> {
let cfg = GoldenConfig::strict();
let value = json!({ "b": 1, "a": { "y": 2, "x": 1 }, "c": [3, 2, 1] });
let canonical = to_canonical_json(&value, &cfg);
assert_eq!(canonical, r#"{"a":{"x":1,"y":2},"b":1,"c":[3,2,1]}"#);
assert!(assert_no_cr(&canonical).is_ok());
Ok(())
}
#[test]
fn write_then_check_golden_file_round_trips() -> Result<(), Box<dyn std::error::Error>> {
let cfg = GoldenConfig::default();
let dir = std::env::temp_dir().join("fsnow-harness-golden");
std::fs::create_dir_all(&dir)?;
let path = dir.join("envelope.golden.json");
let golden = json!({ "command_id": "x", "trace_id": "run-A", "n": 7 });
write_golden(&path, &golden, &cfg)?;
let bytes = std::fs::read(&path)?;
assert_lf_only(&bytes)?;
assert_eq!(bytes.last().copied(), Some(b'\n'));
let rerun = json!({ "command_id": "x", "trace_id": "run-B", "n": 7 });
check_golden_file(&path, &rerun, &cfg)?;
let drifted = json!({ "command_id": "x", "trace_id": "run-C", "n": 8 });
assert!(check_golden_file(&path, &drifted, &cfg).is_err());
std::fs::remove_dir_all(&dir)?;
Ok(())
}
#[test]
fn on_disk_fixture_matches_a_fresh_run() -> Result<(), Box<dyn std::error::Error>> {
let cfg = GoldenConfig::default();
let path = Path::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/fixtures/golden/sample_run.golden.json"
));
assert_lf_only(&std::fs::read(path)?)?;
let fresh = json!({
"command_id": "query.run",
"data_source": "fixture",
"ok": true,
"outcome_kind": "success",
"rows": [
{ "id": 1, "name": "alpha", "ratio": 0.25 },
{ "id": 2, "name": "beta", "ratio": 0.5 }
],
"result_hash": "ffffffffffffffff",
"row_count": 2,
"started_at": "2030-01-01T12:34:56Z",
"trace_id": "fixture-trace-9999"
});
check_golden_file(path, &fresh, &cfg)?;
Ok(())
}
#[test]
fn canonical_json_is_deterministic_and_idempotent() {
let cfg = GoldenConfig::default();
let a = json!({ "z": 1, "a": 2, "m": { "q": 1, "b": 2 }, "trace_id": "x" });
let b = json!({ "a": 2, "trace_id": "y", "m": { "b": 2, "q": 1 }, "z": 1 });
assert_eq!(to_canonical_json(&a, &cfg), to_canonical_json(&b, &cfg));
let once = canonicalize(&a, &cfg);
let twice = canonicalize(&once, &cfg);
assert_eq!(once, twice);
}
}