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}