Skip to main content

tzcompile/compare/
mod.rs

1//! The compatibility oracle: compile a zone both ways and compare.
2//!
3//! `compare` is what turns this project from "a reimplementation" into a *verified* one.
4//! For a chosen zone it compiles with **our** compiler and with the **reference** `zic`,
5//! then compares the results in one of two modes:
6//!
7//! * [`CompareMode::Zdump`] (**default**) — the faithful semantic oracle: dump both files
8//!   with `zdump -v -c LO,HI` over a declared year horizon and diff the behaviour (UTC
9//!   instant, local time, UT offset, DST flag, abbreviation). This is the contract that
10//!   matters, and the only mode appropriate for recurring zones (whose explicit-transition
11//!   horizons differ between implementations while behaviour is identical). See [`zdump`].
12//! * [`CompareMode::Structural`] — decode both TZif files and diff the decoded model
13//!   (footer + transition list + types). Useful for fixed-offset/finite fixtures and for
14//!   debugging, but **not** the correctness criterion for recurring zones, because it
15//!   penalises valid representational differences. See [`semantic`].
16//!
17//! This module is the only one that runs an external `zic`/`zdump`, and only ever here.
18
19pub mod reference_zic;
20pub mod report;
21pub mod semantic;
22pub mod zdump;
23
24use std::path::{Path, PathBuf};
25
26use crate::error::{Error, Result};
27use crate::fs::output_tree;
28use crate::model::Database;
29use crate::tzif;
30use report::{ComparisonKind, ZoneComparison};
31
32/// How a comparison is performed.
33#[derive(Debug, Clone)]
34pub enum CompareMode {
35    /// Decode both TZif files and diff the decoded model. Default horizon-free; suited to
36    /// fixed-offset/finite fixtures and debugging.
37    Structural,
38    /// Diff `zdump -v -c LO,HI` behaviour over `[lo, hi]` (inclusive years). The faithful
39    /// semantic oracle. `program` is the `zdump` executable name/path.
40    Zdump { program: String, lo: i32, hi: i32 },
41}
42
43/// Compare our compilation of `zone` against reference `zic`.
44///
45/// `inputs` are the source files fed to both compilers; `reference_zic` is the reference
46/// compiler program; `work_dir` is a caller-controlled (absolute) directory for scratch
47/// output (typically a tempdir).
48pub fn compare_zone(
49    db: &Database,
50    inputs: &[PathBuf],
51    zone: &str,
52    reference_zic: &str,
53    work_dir: &Path,
54    mode: &CompareMode,
55) -> Result<ZoneComparison> {
56    // Our output, in memory.
57    let ours_bytes = crate::compile_zone_to_bytes(db, zone)?;
58
59    // Reference output, via the C `zic`, into work_dir/ref.
60    let ref_root = work_dir.join("ref");
61    std::fs::create_dir_all(&ref_root).map_err(|e| Error::io(&ref_root, e))?;
62    reference_zic::compile_with_reference(reference_zic, inputs, &ref_root)?;
63    let ref_path = reference_zic::compiled_path(&ref_root, zone);
64    let theirs_bytes = std::fs::read(&ref_path).map_err(|e| Error::io(&ref_path, e))?;
65    let byte_identical = ours_bytes == theirs_bytes;
66
67    match mode {
68        CompareMode::Structural => {
69            // Decode both and diff the model.
70            let ours = tzif::parse(&ours_bytes)?;
71            let theirs = tzif::parse(&theirs_bytes)?;
72            Ok(ZoneComparison {
73                zone: zone.to_string(),
74                kind: ComparisonKind::DecodedTzif,
75                differences: semantic::diff(&ours, &theirs),
76                byte_identical,
77            })
78        }
79        CompareMode::Zdump { program, lo, hi } => {
80            // Write our bytes to a file so `zdump` can read it (absolute path required).
81            let ours_root = work_dir.join("ours");
82            let ours_path =
83                output_tree::write_zone_file(&ours_root, zone, &ours_bytes, true, false)?;
84
85            let ours_lines = zdump::run(program, &ours_path, *lo, *hi)?;
86            let theirs_lines = zdump::run(program, &ref_path, *lo, *hi)?;
87            Ok(ZoneComparison {
88                zone: zone.to_string(),
89                kind: ComparisonKind::ZdumpBehaviour { lo: *lo, hi: *hi },
90                differences: zdump::diff(&ours_lines, &theirs_lines),
91                byte_identical,
92            })
93        }
94    }
95}