๐Ÿฆ€ Functional Rust

757: Golden File Testing Pattern

Difficulty: 2 Level: Intermediate Capture the output of a function to a `.txt` file on first run; on subsequent runs, compare against the saved "golden" output and fail if it changes.

The Problem This Solves

Some outputs are too complex to write expected values for by hand. Pretty-printed ASTs, formatted reports, rendered templates, compiler diagnostic text โ€” you want to ensure these outputs don't change accidentally, but writing `assert_eq!(output, "Add\n Mul\n Num(2)\n Var(x)\n Num(3)\n")` is painful and brittle. Golden file tests (also called snapshot tests) solve this: run the function once, save the output as the "expected" file, commit it to version control. Every subsequent run compares against that file. If the output changes โ€” intentionally or by accident โ€” the test fails, and you review the diff. If the change is intentional, update with `UPDATE_GOLDEN=1 cargo test`. This pattern is widely used in compilers (LLVM's `FileCheck`), CLI tools, formatters, and any code with complex human-readable output.

The Intuition

The golden file is the test assertion. You write the code, run it once to generate the golden files, commit them. Now your test is "does the current output match what it looked like when I last said it was correct?" The `UPDATE_GOLDEN=1` environment variable acts as an explicit "accept this output" signal โ€” you must deliberately choose to update, which prevents accidental regressions being silently accepted.

How It Works in Rust

The infrastructure โ€” compare or update based on `UPDATE_GOLDEN`:
pub fn assert_golden(name: &str, actual: &str) {
 let path = PathBuf::from("tests/golden").join(format!("{}.txt", name));
 let update = std::env::var("UPDATE_GOLDEN").map(|v| v == "1").unwrap_or(false);

 if !path.exists() || update {
     std::fs::create_dir_all("tests/golden").unwrap();
     std::fs::write(&path, actual).unwrap();
     return;
 }

 let expected = std::fs::read_to_string(&path).unwrap();
 // Normalize line endings (Windows โ†” Unix)
 assert_eq!(
     actual.replace("\r\n", "\n"),
     expected.replace("\r\n", "\n"),
     "Golden file mismatch for '{}'. Run with UPDATE_GOLDEN=1 to update.", name
 );
}
Using it in tests:
#[test]
fn golden_tree_render() {
 let expr = make_expr();
 assert_golden("expr_tree", &render_tree(&expr, 0));
}
Generating the golden file (first run or after intentional change):
UPDATE_GOLDEN=1 cargo test
The `tests/golden/expr_tree.txt` file is created or updated. Committing golden files โ€” add `tests/golden/` to version control. The diff in your PR shows exactly what output changed, making code review of rendering logic much easier. Normalizing line endings โ€” always strip `\r\n` to `\n` before comparison to avoid false failures on Windows CI runners.

What This Unlocks

Key Differences

ConceptOCamlRust
Snapshot testing`ppx_expect` (Jane Street)Manual golden-file infra, or `insta` crate
File I/O in tests`open_in` / `output_string``std::fs::read_to_string` / `std::fs::write`
Environment variable`Sys.getenv``std::env::var("KEY")` โ†’ `Result<String, VarError>`
Test update workflow`PPXEXPECT_UPDATE=true``UPDATE_GOLDEN=1 cargo test`
/// 757: Golden File Testing Pattern โ€” std-only

use std::path::{Path, PathBuf};
use std::fs;
use std::env;

// โ”€โ”€ Golden file infrastructure โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

const GOLDEN_DIR: &str = "tests/golden";

/// Compare `actual` against a stored golden file.
/// If `UPDATE_GOLDEN=1`, updates the file instead of comparing.
pub fn assert_golden(name: &str, actual: &str) {
    let path = PathBuf::from(GOLDEN_DIR).join(format!("{}.txt", name));
    let update = env::var("UPDATE_GOLDEN").map(|v| v == "1").unwrap_or(false);

    if !path.exists() || update {
        fs::create_dir_all(GOLDEN_DIR).expect("cannot create golden dir");
        fs::write(&path, actual).expect("cannot write golden file");
        eprintln!("[golden:{}] {}", name,
            if update { "Updated" } else { "Created" });
        return;
    }

    let expected = fs::read_to_string(&path)
        .expect("cannot read golden file");

    let actual_n   = actual.replace("\r\n", "\n");
    let expected_n = expected.replace("\r\n", "\n");

    if actual_n != expected_n {
        eprintln!("[golden:{}] MISMATCH", name);
        eprintln!("--- expected ({}) ---", path.display());
        eprintln!("{}", &expected_n);
        eprintln!("--- actual ---");
        eprintln!("{}", &actual_n);
        panic!("Golden file mismatch for '{}'. Run with UPDATE_GOLDEN=1 to update.", name);
    }
}

// โ”€โ”€ Code under test โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// A simple expression AST
#[derive(Debug)]
pub enum Expr {
    Num(i64),
    Var(String),
    Add(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
}

/// Render the AST to a human-readable string
pub fn render(expr: &Expr) -> String {
    match expr {
        Expr::Num(n)       => n.to_string(),
        Expr::Var(s)       => s.clone(),
        Expr::Add(a, b)    => format!("({} + {})", render(a), render(b)),
        Expr::Mul(a, b)    => format!("({} * {})", render(a), render(b)),
    }
}

/// Render with indentation (tree format)
pub fn render_tree(expr: &Expr, indent: usize) -> String {
    let pad = "  ".repeat(indent);
    match expr {
        Expr::Num(n)    => format!("{}Num({})\n", pad, n),
        Expr::Var(s)    => format!("{}Var({})\n", pad, s),
        Expr::Add(a, b) => format!("{}Add\n{}{}", pad,
            render_tree(a, indent + 1), render_tree(b, indent + 1)),
        Expr::Mul(a, b) => format!("{}Mul\n{}{}", pad,
            render_tree(a, indent + 1), render_tree(b, indent + 1)),
    }
}

/// Generate a simple markdown report from key-value data
pub fn render_report(title: &str, rows: &[(&str, &str)]) -> String {
    let mut out = format!("# {}\n\n", title);
    out.push_str("| Key | Value |\n");
    out.push_str("|-----|-------|\n");
    for (k, v) in rows {
        out.push_str(&format!("| {} | {} |\n", k, v));
    }
    out.push('\n');
    out
}

fn main() {
    let expr = Expr::Add(
        Box::new(Expr::Mul(Box::new(Expr::Num(2)), Box::new(Expr::Var("x".into())))),
        Box::new(Expr::Num(3)),
    );
    println!("Inline: {}", render(&expr));
    println!("Tree:\n{}", render_tree(&expr, 0));

    let report = render_report("Summary", &[
        ("Status", "OK"),
        ("Items", "42"),
        ("Errors", "0"),
    ]);
    println!("{}", report);
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_expr() -> Expr {
        Expr::Add(
            Box::new(Expr::Mul(
                Box::new(Expr::Num(2)),
                Box::new(Expr::Var("x".into()))
            )),
            Box::new(Expr::Num(3)),
        )
    }

    // Golden tests โ€” output captured to tests/golden/*.txt
    #[test]
    fn golden_inline_render() {
        let expr = make_expr();
        assert_golden("expr_inline", &render(&expr));
    }

    #[test]
    fn golden_tree_render() {
        let expr = make_expr();
        assert_golden("expr_tree", &render_tree(&expr, 0));
    }

    #[test]
    fn golden_report() {
        let report = render_report("Test Report", &[
            ("Alpha", "1"),
            ("Beta",  "2"),
            ("Gamma", "3"),
        ]);
        assert_golden("test_report", &report);
    }

    // Non-golden unit tests
    #[test]
    fn render_num() {
        assert_eq!(render(&Expr::Num(42)), "42");
    }

    #[test]
    fn render_var() {
        assert_eq!(render(&Expr::Var("x".into())), "x");
    }

    #[test]
    fn render_add() {
        let e = Expr::Add(Box::new(Expr::Num(1)), Box::new(Expr::Num(2)));
        assert_eq!(render(&e), "(1 + 2)");
    }

    #[test]
    fn render_nested() {
        let e = Expr::Mul(
            Box::new(Expr::Add(Box::new(Expr::Num(1)), Box::new(Expr::Num(2)))),
            Box::new(Expr::Var("y".into())),
        );
        assert_eq!(render(&e), "((1 + 2) * y)");
    }
}
(* 757: Golden File Tests โ€” OCaml *)

(* Code under test: renders an AST to a string *)
type expr =
  | Num   of int
  | Add   of expr * expr
  | Mul   of expr * expr
  | Var   of string

let rec render = function
  | Num n       -> string_of_int n
  | Var s       -> s
  | Add (a, b)  -> Printf.sprintf "(%s + %s)" (render a) (render b)
  | Mul (a, b)  -> Printf.sprintf "(%s * %s)" (render a) (render b)

(* Golden file infrastructure *)
let read_file path =
  try
    let ic = open_in path in
    let n = in_channel_length ic in
    let s = Bytes.create n in
    really_input ic s 0 n;
    close_in ic;
    Some (Bytes.to_string s)
  with Sys_error _ -> None

let write_file path content =
  let oc = open_out path in
  output_string oc content;
  close_out oc

let golden_dir = "tests/golden"

let assert_golden name actual =
  let path = Printf.sprintf "%s/%s.txt" golden_dir name in
  let update = (try Sys.getenv "UPDATE_GOLDEN" = "1" with Not_found -> false) in
  match read_file path, update with
  | None, _ | _, true ->
    (try Unix.mkdir golden_dir 0o755 with Unix.Unix_error _ -> ());
    write_file path actual;
    Printf.printf "[golden:%s] %s\n" name (if update then "Updated" else "Created")
  | Some expected, false ->
    if actual = expected
    then Printf.printf "[golden:%s] OK\n" name
    else begin
      Printf.printf "[golden:%s] MISMATCH\n" name;
      Printf.printf "Expected:\n%s\nActual:\n%s\n" expected actual;
      failwith "golden test failed"
    end

let () =
  let expr = Add (Mul (Num 2, Var "x"), Num 3) in
  assert_golden "expr_render" (render expr);
  let complex = Mul (Add (Var "a", Var "b"), Add (Num 1, Num 2)) in
  assert_golden "complex_expr" (render complex);
  Printf.printf "Golden tests done!\n"