Skip to main content

logicaffeine_compile/page_host/
trace.rs

1//! A scenario's run on one tier, as the page looked after every statement — and the comparison
2//! of two such runs.
3
4use super::snapshot::{Axis, DocSnapshot};
5use serde::Serialize;
6
7/// The page after one statement of a scenario.
8#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
9pub struct TraceStep {
10    /// Which top-level statement this follows: `0` is the page the program's own statements left
11    /// (before the scenario's first statement), `k` the page after its `k`-th.
12    pub statement: usize,
13    /// Whether this tier executed the statement. A statement that only READS the page (an
14    /// `Expect`, a `Let` of a page query) is executed by the reference tier; a tier that drives the
15    /// page from outside has nothing to do for it, and says so here.
16    pub executed: bool,
17    pub snapshot: DocSnapshot,
18}
19
20/// Where a scenario stopped: the statement whose failure ended the run, and the failure.
21#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
22pub struct Stop {
23    pub statement: usize,
24    pub message: String,
25}
26
27/// One scenario's run on one tier: a snapshot per top-level statement, until it stopped.
28#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
29pub struct PageTrace {
30    pub steps: Vec<TraceStep>,
31    pub stopped: Option<Stop>,
32}
33
34/// Two tiers disagreeing about one axis, first at `statement`.
35#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
36pub struct AxisDivergence {
37    pub axis: Axis,
38    pub statement: usize,
39    pub reference: String,
40    pub candidate: String,
41}
42
43/// Everything two runs of one scenario disagree about, one entry per axis, each at the first
44/// statement where that axis differs.
45#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
46pub struct Divergence {
47    pub axes: Vec<AxisDivergence>,
48}
49
50impl Divergence {
51    pub fn is_empty(&self) -> bool {
52        self.axes.is_empty()
53    }
54
55    /// The first statement at which the runs disagree about anything.
56    pub fn first_statement(&self) -> Option<usize> {
57        self.axes.iter().map(|a| a.statement).min()
58    }
59
60    /// A report a person can act on: each axis, where it first differs, and how.
61    pub fn render(&self) -> String {
62        let mut out = String::new();
63        for a in &self.axes {
64            out.push_str(&format!(
65                "{} differs after statement {}:\n{}",
66                a.axis.name(),
67                a.statement,
68                describe_difference(&a.reference, &a.candidate)
69            ));
70        }
71        out
72    }
73}
74
75impl PageTrace {
76    /// The last step at or before `statement`.
77    pub fn at(&self, statement: usize) -> Option<&TraceStep> {
78        self.steps.iter().rev().find(|s| s.statement <= statement)
79    }
80
81    /// Where `candidate` disagrees with `self`, the reference.
82    ///
83    /// The runs are compared statement by statement, over the statements both reached. A
84    /// reference stop at a statement the candidate did not execute (a page query that found no
85    /// element, say) ends the comparison there: the candidate would have read the same page and
86    /// failed the same way, since every earlier statement already agreed.
87    pub fn compare(&self, candidate: &PageTrace) -> Divergence {
88        let mut limit = usize::MAX;
89        let mut axes: Vec<AxisDivergence> = Vec::new();
90        if let Some(stop) = &self.stopped {
91            let candidate_ran_it = candidate
92                .steps
93                .iter()
94                .any(|s| s.statement == stop.statement && s.executed);
95            if !candidate_ran_it {
96                limit = stop.statement.saturating_sub(1);
97            }
98        }
99        for axis in Axis::SNAPSHOT {
100            let first = self
101                .steps
102                .iter()
103                .filter(|s| s.statement <= limit)
104                .filter_map(|r| {
105                    let c = candidate
106                        .steps
107                        .iter()
108                        .find(|c| c.statement == r.statement)?;
109                    let (rv, cv) = (r.snapshot.axis(axis), c.snapshot.axis(axis));
110                    (rv != cv).then_some((r.statement, rv, cv))
111                })
112                .next();
113            if let Some((statement, reference, candidate)) = first {
114                axes.push(AxisDivergence {
115                    axis,
116                    statement,
117                    reference,
118                    candidate,
119                });
120            }
121        }
122        let reference_stop = self.stopped.as_ref().filter(|s| s.statement <= limit);
123        let candidate_stop = candidate.stopped.as_ref().filter(|s| s.statement <= limit);
124        if reference_stop != candidate_stop {
125            let render = |s: Option<&Stop>| match s {
126                Some(s) => format!("stopped at statement {}: {}", s.statement, s.message),
127                None => "ran to the end".to_string(),
128            };
129            let statement = [reference_stop, candidate_stop]
130                .into_iter()
131                .flatten()
132                .map(|s| s.statement)
133                .min()
134                .unwrap_or(0);
135            axes.push(AxisDivergence {
136                axis: Axis::Stopped,
137                statement,
138                reference: render(reference_stop),
139                candidate: render(candidate_stop),
140            });
141        }
142        axes.sort_by_key(|a| (a.statement, a.axis));
143        Divergence { axes }
144    }
145}
146
147/// Two renderings of one axis, with the first difference pointed at — a document is long, and a
148/// person looking for what differs should not have to find it.
149pub fn describe_difference(reference: &str, candidate: &str) -> String {
150    let common = reference
151        .char_indices()
152        .zip(candidate.chars())
153        .take_while(|((_, a), b)| a == b)
154        .last()
155        .map(|((i, a), _)| i + a.len_utf8())
156        .unwrap_or(0);
157    let window = |s: &str| -> String {
158        let start = s[..common.min(s.len())]
159            .char_indices()
160            .rev()
161            .nth(40)
162            .map(|(i, _)| i)
163            .unwrap_or(0);
164        let rest: String = s[start..].chars().take(160).collect();
165        let lead = if start > 0 { "…" } else { "" };
166        format!("{lead}{rest}")
167    };
168    format!(
169        "  reference: {:?}\n  candidate: {:?}\n  (first difference at byte {common})\n",
170        window(reference),
171        window(candidate)
172    )
173}