omena_transform_passes/runtime/
observation_projection.rs1use omena_parser::{ParsedSelectorFactKind, StyleDialect, collect_style_facts};
4use omena_transform_cst::{
5 ObservationKindV0, TransformObservationEquivalenceV0, TransformPassKind,
6 compare_raw_transform_observation_bytes_v0, compare_transform_observation_projection_values_v0,
7};
8use serde::Serialize;
9
10use crate::{
11 TransformWinnerEqualityObservationV0, execute_transform_passes_on_source,
12 runtime::winner_equality::compare_transform_winner_equality_for_conformance_v0,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
16#[serde(rename_all = "camelCase")]
17pub struct TransformExecutableObservationProjectionV0 {
18 pub kind: ObservationKindV0,
19 pub value: String,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
23#[serde(rename_all = "camelCase")]
24pub struct TransformExecutableObservationReportV0 {
25 pub schema_version: &'static str,
26 pub product: &'static str,
27 pub pass_id: &'static str,
28 pub pass_executed: bool,
29 pub input_projection: TransformExecutableObservationProjectionV0,
30 pub output_projection: TransformExecutableObservationProjectionV0,
31 pub projection_equivalence: TransformObservationEquivalenceV0,
32 pub raw_equivalence: TransformObservationEquivalenceV0,
33 pub output_css: String,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
37#[serde(rename_all = "camelCase")]
38pub struct TransformExecutableCascadeWinnerEquivalenceV0 {
39 pub schema_version: &'static str,
40 pub product: &'static str,
41 pub profile_id: &'static str,
42 pub compared_obligation_count: usize,
43 pub equivalent: bool,
44}
45
46pub fn execute_transform_pass_selector_matching_observation_v0(
49 source: &str,
50 pass: TransformPassKind,
51) -> TransformExecutableObservationReportV0 {
52 let execution = execute_transform_passes_on_source(source, &[pass]);
53 let input_projection = selector_matching_projection_v0(source);
54 let output_projection = selector_matching_projection_v0(execution.output_css.as_str());
55 TransformExecutableObservationReportV0 {
56 schema_version: "0",
57 product: "omena-transform-passes.executable-observation-projection",
58 pass_id: pass.id(),
59 pass_executed: execution.executed_pass_ids.contains(&pass.id()),
60 projection_equivalence: compare_transform_observation_projection_values_v0(
61 "selector-matching-executable-v0",
62 ObservationKindV0::SelectorMatching,
63 input_projection.as_str(),
64 output_projection.as_str(),
65 ),
66 raw_equivalence: compare_raw_transform_observation_bytes_v0(
67 "raw-bytes-executable-v0",
68 source.as_bytes(),
69 execution.output_css.as_bytes(),
70 ),
71 input_projection: TransformExecutableObservationProjectionV0 {
72 kind: ObservationKindV0::SelectorMatching,
73 value: input_projection,
74 },
75 output_projection: TransformExecutableObservationProjectionV0 {
76 kind: ObservationKindV0::SelectorMatching,
77 value: output_projection,
78 },
79 output_css: execution.output_css,
80 }
81}
82
83pub fn compare_transform_cascade_winner_observation_v0(
86 input: &str,
87 output: &str,
88 pass: TransformPassKind,
89) -> TransformExecutableCascadeWinnerEquivalenceV0 {
90 let obligations = compare_transform_winner_equality_for_conformance_v0(
91 input,
92 output,
93 StyleDialect::Css,
94 pass,
95 );
96 let equivalent = !obligations.is_empty()
97 && obligations.iter().all(|obligation| {
98 matches!(
99 obligation.observation,
100 TransformWinnerEqualityObservationV0::ObservedEqual { .. }
101 | TransformWinnerEqualityObservationV0::ObservedGuardedEqual { .. }
102 )
103 });
104 TransformExecutableCascadeWinnerEquivalenceV0 {
105 schema_version: "0",
106 product: "omena-transform-passes.executable-cascade-winner-equivalence",
107 profile_id: "cascade-winner-executable-v0",
108 compared_obligation_count: obligations.len(),
109 equivalent,
110 }
111}
112
113fn selector_matching_projection_v0(source: &str) -> String {
114 let mut selectors = collect_style_facts(source, StyleDialect::Css)
115 .selectors
116 .into_iter()
117 .map(|selector| {
118 let kind = match selector.kind {
119 ParsedSelectorFactKind::Class => "class",
120 ParsedSelectorFactKind::Id => "id",
121 ParsedSelectorFactKind::Placeholder => "placeholder",
122 };
123 format!("{kind}:{}", selector.name)
124 })
125 .collect::<Vec<_>>();
126 selectors.sort();
127 selectors.dedup();
128 selectors.join("\n")
129}
130
131#[cfg(test)]
132mod tests {
133 use serde::Deserialize;
134
135 use super::*;
136
137 #[derive(Deserialize)]
138 #[serde(rename_all = "camelCase")]
139 struct ExecutableTruthRowV0 {
140 case_id: String,
141 pass_id: String,
142 observation_kind: String,
143 input_css: String,
144 candidate_output_css: Option<String>,
145 expected_equivalent: bool,
146 }
147
148 #[test]
149 fn executable_truth_table_runs_product_pass_and_authority_projections()
150 -> Result<(), Box<dyn std::error::Error>> {
151 let rows = serde_json::from_str::<Vec<ExecutableTruthRowV0>>(include_str!(
152 "../../data/observation-executable-truth-table-v0.json"
153 ))?;
154 let mut positive_rows = 0usize;
155 for row in rows {
156 if row.pass_id != TransformPassKind::WhitespaceStrip.id() {
157 return Err(format!("unknown executable pass in {}", row.case_id).into());
158 }
159 let report = execute_transform_pass_selector_matching_observation_v0(
160 row.input_css.as_str(),
161 TransformPassKind::WhitespaceStrip,
162 );
163 assert!(report.pass_executed, "{}", row.case_id);
164 let equivalent = match row.observation_kind.as_str() {
165 "selectorMatching" => report.projection_equivalence.equivalent,
166 "rawBytes" => report.raw_equivalence.equivalent,
167 "cascadeWinner" => {
168 compare_transform_cascade_winner_observation_v0(
169 row.input_css.as_str(),
170 row.candidate_output_css
171 .as_deref()
172 .unwrap_or(report.output_css.as_str()),
173 TransformPassKind::WhitespaceStrip,
174 )
175 .equivalent
176 }
177 other => return Err(format!("unknown executable observer: {other}").into()),
178 };
179 assert_eq!(equivalent, row.expected_equivalent, "{}", row.case_id);
180 positive_rows += usize::from(equivalent);
181 }
182 assert!(
183 positive_rows > 0,
184 "executable truth table has no positive row"
185 );
186 Ok(())
187 }
188
189 #[test]
190 fn executed_whitespace_pass_is_selector_equivalent_but_not_raw_equivalent() {
191 let report = execute_transform_pass_selector_matching_observation_v0(
192 ".a { color: red; }",
193 TransformPassKind::WhitespaceStrip,
194 );
195 println!(
196 "selectorProjection passExecuted={} pass={} selectorMatching={} rawBytes={} output={:?}",
197 report.pass_executed,
198 report.pass_id,
199 report.projection_equivalence.equivalent,
200 report.raw_equivalence.equivalent,
201 report.output_css,
202 );
203 assert!(report.pass_executed);
204 assert!(report.projection_equivalence.equivalent);
205 assert!(!report.raw_equivalence.equivalent);
206 }
207
208 #[test]
209 fn executed_pass_baseline_detects_cascade_winner_corruption() {
210 let source = ".a { color: red; } .a { color: blue; }";
211 let report = execute_transform_pass_selector_matching_observation_v0(
212 source,
213 TransformPassKind::WhitespaceStrip,
214 );
215 let pass_relation = compare_transform_cascade_winner_observation_v0(
216 source,
217 report.output_css.as_str(),
218 TransformPassKind::WhitespaceStrip,
219 );
220 let corrupted = ".a{color:blue}.a{color:red}";
221 let corrupted_relation = compare_transform_cascade_winner_observation_v0(
222 source,
223 corrupted,
224 TransformPassKind::WhitespaceStrip,
225 );
226 println!(
227 "cascadeWinnerProjection passExecuted={} pass={} passCascadeWinner={} corruptedCascadeWinner={} disjointSelectorMatching={} passOutput={:?}",
228 report.pass_executed,
229 report.pass_id,
230 pass_relation.equivalent,
231 corrupted_relation.equivalent,
232 report.projection_equivalence.equivalent,
233 report.output_css,
234 );
235 assert!(report.pass_executed);
236 assert!(pass_relation.equivalent);
237 assert!(!corrupted_relation.equivalent);
238 assert!(report.projection_equivalence.equivalent);
239 }
240}