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