boxworks_testing/
lib.rs

1//! Testing utilities for Boxworks-based code.
2//!
3//! This crate makes writing tests for Boxworks code easier.
4//! It provides the [`assert_box_eq`] macro,
5//!     which validates that two Boxworks data structures are the same.
6//! The data structures can be provided as Rust values from [`boxworks::ds`],
7//!     or as Boxworks language strings.
8//! If there is a mismatch, the diff is printed nicely.
9//!
10//! ## Example
11//!
12//! ```rust
13//! # use boxworks::ds;
14//! # use boxworks_testing::assert_box_eq;
15//! # use common::font;
16//! let v_box = ds::VBox {
17//!     list: vec![ds::HBox {
18//!         list: vec![
19//!             ds::Char { char: 'A', font: font::Id(33) }.into(),
20//!             ds::Char { char: 'Z', font: font::Id(33) }.into(),
21//!         ],
22//!         ..Default::default()
23//!     }.into()],
24//!     ..Default::default()
25//! };
26//! assert_box_eq!(
27//!     v_box,
28//!     r#"
29//!     vbox(
30//!       content=[
31//!         hbox(
32//!           content=[
33//!             chars("AZ", 33)
34//!           ]
35//!         )
36//!       ]
37//!     )
38//!     "#,
39//! );
40//! ```
41
42use boxworks::ds;
43use boxworks::lang;
44use boxworks::lang::convert::ToBoxLang;
45
46#[macro_export]
47macro_rules! assert_box_eq {
48    ($left:expr, $right:expr$(,)?) => {{
49        let lhs: boxworks_testing::Value = $left.into();
50        let rhs: boxworks_testing::Value = $right.into();
51        boxworks_testing::assert_eq(lhs, rhs);
52    }};
53}
54
55#[macro_export]
56macro_rules! is_box_eq {
57    ($left:expr, $right:expr$(,)?) => {{
58        let lhs: boxworks_testing::Value = $left.into();
59        let rhs: boxworks_testing::Value = $right.into();
60        boxworks_testing::is_eq(lhs, rhs)
61    }};
62}
63
64#[macro_export]
65macro_rules! assert_box_lossy_eq {
66    ($left:expr, $right:expr$(,)?) => {{
67        let lhs: boxworks_testing::Value = $left.into();
68        let rhs: boxworks_testing::Value = $right.into();
69        boxworks_testing::assert_lossy_eq(lhs, rhs);
70    }};
71}
72
73#[macro_export]
74macro_rules! is_box_lossy_eq {
75    ($left:expr, $right:expr$(,)?) => {{
76        let lhs: boxworks_testing::Value = $left.into();
77        let rhs: boxworks_testing::Value = $right.into();
78        boxworks_testing::is_lossy_eq(lhs, rhs)
79    }};
80}
81
82pub enum Value {
83    String(String),
84    Box(Vec<ds::Horizontal>),
85}
86
87impl From<&str> for Value {
88    fn from(value: &str) -> Self {
89        Value::String(value.into())
90    }
91}
92
93impl From<String> for Value {
94    fn from(value: String) -> Self {
95        Value::String(value)
96    }
97}
98
99impl From<ds::VBox> for Value {
100    fn from(value: ds::VBox) -> Self {
101        Value::Box(vec![ds::Horizontal::VBox(value)])
102    }
103}
104
105impl From<ds::HBox> for Value {
106    fn from(value: ds::HBox) -> Self {
107        Value::Box(vec![ds::Horizontal::HBox(value)])
108    }
109}
110
111impl From<Vec<ds::Horizontal>> for Value {
112    fn from(value: Vec<ds::Horizontal>) -> Self {
113        Value::Box(value)
114    }
115}
116
117pub fn is_eq(lhs: Value, rhs: Value) -> bool {
118    is_eq_impl(lhs, rhs, false)
119}
120
121pub fn is_lossy_eq(lhs: Value, rhs: Value) -> bool {
122    is_eq_impl(lhs, rhs, true)
123}
124
125fn is_eq_impl(lhs: Value, rhs: Value, standardize_lossy: bool) -> bool {
126    let (lhs_list, _) = normalize(lhs, "lhs.box", standardize_lossy);
127    let (rhs_list, _) = normalize(rhs, "rhs.box", standardize_lossy);
128    lhs_list == rhs_list
129}
130
131pub fn assert_eq(lhs: Value, rhs: Value) {
132    assert_eq_impl(lhs, rhs, false);
133}
134
135pub fn assert_lossy_eq(lhs: Value, rhs: Value) {
136    assert_eq_impl(lhs, rhs, true);
137}
138
139fn assert_eq_impl(lhs: Value, rhs: Value, standardize_lossy: bool) {
140    let (lhs_list, lhs_s) = normalize(lhs, "lhs.box", standardize_lossy);
141    let (rhs_list, rhs_s) = normalize(rhs, "rhs.box", standardize_lossy);
142    // We first diff the boxlang representation because this is clearer.
143    use pretty_assertions::assert_eq;
144    assert_eq!(lhs_s, rhs_s);
145    // But we also diff the data structure, in case the ds to lang process is lossy
146    // and causes different lists be the same.
147    assert_eq!(rhs_list, lhs_list);
148}
149
150fn normalize(val: Value, side: &str, standardize_lossy: bool) -> (Vec<ds::Horizontal>, String) {
151    let mut list = match val {
152        Value::String(s) => match lang::parse_horizontal_list(&s.clone()) {
153            Ok(v) => v,
154            Err(err) => {
155                let source = ariadne::Source::from(s);
156                let cache: (&str, _) = (side, source);
157                for err in err {
158                    err.ariadne_report(side).eprint(cache.clone()).unwrap();
159                }
160                panic!("failed to parse boxlang input; errors printed above.")
161            }
162        },
163        Value::Box(v) => v,
164    };
165    if standardize_lossy {
166        standardize_lossy_h_list(&mut list);
167    }
168    let mut s = String::new();
169    for elem in &list {
170        use std::fmt::Write;
171        write!(&mut s, "{}", elem.to_box_lang()).unwrap();
172    }
173    (list, s)
174}
175
176fn standardize_lossy_h_list(v: &mut [ds::Horizontal]) {
177    for elem in v.iter_mut() {
178        use ds::Horizontal::*;
179        match elem {
180            HBox(hbox) => {
181                standardize_lossy_h_list(&mut hbox.list);
182            }
183            VBox(vbox) => {
184                standardize_lossy_v_list(&mut vbox.list);
185            }
186            Discretionary(disc) => {
187                standardize_lossy_d_list(&mut disc.pre_break);
188                standardize_lossy_d_list(&mut disc.post_break);
189            }
190            Ligature(ligature) => ligature.standardize_lossy(),
191            _ => {}
192        }
193    }
194}
195
196fn standardize_lossy_v_list(v: &mut [ds::Vertical]) {
197    for elem in v.iter_mut() {
198        use ds::Vertical::*;
199        match elem {
200            HBox(hbox) => {
201                standardize_lossy_h_list(&mut hbox.list);
202            }
203            VBox(vbox) => {
204                standardize_lossy_v_list(&mut vbox.list);
205            }
206            _ => {}
207        }
208    }
209}
210
211fn standardize_lossy_d_list(v: &mut [ds::DiscretionaryElem]) {
212    for elem in v.iter_mut() {
213        use ds::DiscretionaryElem::*;
214        match elem {
215            HBox(hbox) => {
216                standardize_lossy_h_list(&mut hbox.list);
217            }
218            VBox(vbox) => {
219                standardize_lossy_v_list(&mut vbox.list);
220            }
221            Ligature(ligature) => ligature.standardize_lossy(),
222            _ => {}
223        }
224    }
225}