logicaffeine_compile/page_host/
trace.rs1use super::snapshot::{Axis, DocSnapshot};
5use serde::Serialize;
6
7#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
9pub struct TraceStep {
10 pub statement: usize,
13 pub executed: bool,
17 pub snapshot: DocSnapshot,
18}
19
20#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
22pub struct Stop {
23 pub statement: usize,
24 pub message: String,
25}
26
27#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
29pub struct PageTrace {
30 pub steps: Vec<TraceStep>,
31 pub stopped: Option<Stop>,
32}
33
34#[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#[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 pub fn first_statement(&self) -> Option<usize> {
57 self.axes.iter().map(|a| a.statement).min()
58 }
59
60 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 pub fn at(&self, statement: usize) -> Option<&TraceStep> {
78 self.steps.iter().rev().find(|s| s.statement <= statement)
79 }
80
81 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
147pub 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}