1use nanvm_lib::{
23 common::sized_index::SizedIndex,
24 naive::Naive,
25 vm::{Any, IVm, Nullish, Object, ToAny, Unpacked},
26};
27
28pub struct Case {
31 pub name: &'static str,
33 pub fixture: crate::Module<Naive>,
35 pub expected: crate::Module<Naive>,
38 pub json: Option<&'static [u8]>,
40 pub throws: bool,
42}
43
44pub fn same_graph<A: IVm>(actual: &Any<A>, expected: &Any<A>) -> Result<(), String> {
60 Walk { pairs: Vec::new() }.walk("$", actual, expected)
61}
62
63struct Walk<A: IVm> {
64 pairs: Vec<(Any<A>, Any<A>)>,
67}
68
69impl<A: IVm> Walk<A> {
70 fn walk(&mut self, path: &str, actual: &Any<A>, expected: &Any<A>) -> Result<(), String> {
71 match (actual.clone().into(), expected.clone().into()) {
72 (Unpacked::Number(a), Unpacked::Number(e)) => {
73 let (a, e) = (f64::from(a), f64::from(e));
74 if a.to_bits() == e.to_bits() {
75 Ok(())
76 } else {
77 Err(format!("{path}: {a:?} is not {e:?}"))
78 }
79 }
80 (Unpacked::Array(a), Unpacked::Array(e)) => {
81 if self.entered(path, actual, expected)? {
82 return Ok(());
83 }
84 if a.length() != e.length() {
85 return Err(format!(
86 "{path}: length {} is not {}",
87 a.length(),
88 e.length()
89 ));
90 }
91 (0..a.length()).try_for_each(|i| self.walk(&format!("{path}[{i}]"), &a[i], &e[i]))
92 }
93 (Unpacked::Object(a), Unpacked::Object(e)) => {
94 if self.entered(path, actual, expected)? {
95 return Ok(());
96 }
97 let (a, e) = (a.own_entries(), e.own_entries());
98 if a.len() != e.len() {
99 return Err(format!("{path}: {} entries are not {}", a.len(), e.len()));
100 }
101 a.iter().zip(&e).try_for_each(|((ak, av), (ek, ev))| {
102 if ak == ek {
103 self.walk(&format!("{path}.{ak:?}"), av, ev)
104 } else {
105 Err(format!("{path}: key {ak:?} is not {ek:?}"))
106 }
107 })
108 }
109 _ if actual == expected => Ok(()),
110 _ => Err(format!("{path}: {actual:?} is not {expected:?}")),
111 }
112 }
113
114 fn entered(&mut self, path: &str, actual: &Any<A>, expected: &Any<A>) -> Result<bool, String> {
117 match self
118 .pairs
119 .iter()
120 .find(|(a, e)| a == actual || e == expected)
121 {
122 Some((a, e)) if a == actual && e == expected => Ok(true),
123 Some(_) => Err(format!("{path}: sharing differs")),
124 None => {
125 self.pairs.push((actual.clone(), expected.clone()));
126 Ok(false)
127 }
128 }
129 }
130}
131
132fn default_of<A: IVm>(exports: Any<A>) -> Result<Any<A>, String> {
136 let exports = Object::try_from(exports).map_err(|_| "the exports are not an object")?;
137 Ok(exports
138 .own_property(&"default".into())
139 .unwrap_or(Nullish::Undefined.to_any()))
140}
141
142pub fn check(case: &Case) -> Result<(), String> {
150 let runtime = crate::runtime::<Naive>();
151 match ((case.fixture)(runtime.clone()), case.throws) {
152 (Err(_), true) => Ok(()),
153 (Err(thrown), false) => Err(format!("threw {thrown:?}, expected a value")),
154 (Ok(value), true) => Err(format!("expected a throw, got {value:?}")),
155 (Ok(actual), false) => {
156 let expected =
157 (case.expected)(runtime).map_err(|e| format!("the expectation threw {e:?}"))?;
158 let (actual, expected) = (default_of(actual)?, default_of(expected)?);
159 check_json(case.json, &actual)?;
160 same_graph(&actual, &expected)
161 }
162 }
163}
164
165fn check_json(json: Option<&[u8]>, actual: &Any<Naive>) -> Result<(), String> {
168 match (json, actual.clone().to_json()) {
169 (Some(bytes), Ok(text)) if text.as_bytes() == bytes => Ok(()),
170 (Some(bytes), Ok(text)) => Err(format!(
171 "JSON {text:?} is not {:?}",
172 String::from_utf8_lossy(bytes)
173 )),
174 (Some(_), Err(e)) => Err(format!("expected JSON, got {e}")),
175 (None, Err(_)) => Ok(()),
176 (None, Ok(text)) => Err(format!("expected no JSON form, got {text:?}")),
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use nanvm_lib::{
183 naive::Naive,
184 vm::{Any, ToAny, ToArray, ToObject},
185 };
186
187 use super::{Case, check, same_graph};
188
189 type V = Any<Naive>;
190
191 fn array(items: impl IntoIterator<Item = V>) -> V {
192 items.into_iter().collect::<Vec<_>>().to_array().to_any()
193 }
194
195 fn object<const N: usize>(entries: [(&str, V); N]) -> V {
196 entries.map(|(k, v)| (k.into(), v)).to_object().to_any()
197 }
198
199 fn num(x: f64) -> V {
200 x.to_any()
201 }
202
203 #[test]
204 fn outcomes_follow_the_independent_throw_marker() {
205 fn throws(_runtime: V) -> Result<V, V> {
206 Err(Any::undefined())
207 }
208 fn value(_runtime: V) -> Result<V, V> {
209 Ok(object([("default", Any::undefined())]))
210 }
211 for json in [Some(&b"1"[..]), None] {
212 let case = Case {
213 name: "regression",
214 fixture: throws,
215 expected: throws,
216 throws: false,
217 json,
218 };
219 assert!(check(&case).is_err());
220 }
221 let case = Case {
222 name: "throw",
223 fixture: value,
224 expected: value,
225 throws: true,
226 json: None,
227 };
228 assert!(check(&case).is_err());
229 assert_eq!(
230 check(&Case {
231 throws: false,
232 ..case
233 }),
234 Ok(())
235 );
236 assert_eq!(
237 check(&Case {
238 fixture: throws,
239 expected: throws,
240 ..case
241 }),
242 Ok(())
243 );
244 assert!(
245 check(&Case {
246 fixture: throws,
247 throws: false,
248 ..case
249 })
250 .is_err()
251 );
252 assert!(
253 check(&Case {
254 expected: throws,
255 throws: false,
256 ..case
257 })
258 .is_err()
259 );
260 }
261
262 #[test]
265 fn sharing_is_compared() {
266 let shared = object([("x", num(1.0))]);
267 let twice = array([shared.clone(), shared]);
268 let copies = array([object([("x", num(1.0))]), object([("x", num(1.0))])]);
269 assert_eq!(same_graph(&twice, &twice.clone()), Ok(()));
270 assert_eq!(same_graph(&copies, &copies.clone()), Ok(()));
271 assert_eq!(
272 same_graph(&twice, &copies),
273 Err("$[1]: sharing differs".into())
274 );
275 assert_eq!(
276 same_graph(&copies, &twice),
277 Err("$[1]: sharing differs".into())
278 );
279 }
280
281 #[test]
283 fn numbers_compare_by_bits() {
284 assert_eq!(same_graph(&num(f64::NAN), &num(f64::NAN)), Ok(()));
285 assert_eq!(
286 same_graph(&num(-0.0), &num(0.0)),
287 Err("$: -0.0 is not 0.0".into())
288 );
289 }
290
291 #[test]
294 fn objects_compare_by_observable_entries() {
295 let ab = object([("a", num(1.0)), ("b", num(2.0))]);
296 let ba = object([("b", num(2.0)), ("a", num(1.0))]);
297 assert_eq!(
298 same_graph(&ab, &ba),
299 Err("$: key \"a\" is not \"b\"".into())
300 );
301 let overwritten = object([("a", num(0.0)), ("b", num(2.0)), ("a", num(1.0))]);
302 assert_eq!(same_graph(&overwritten, &ab), Ok(()));
303 assert_eq!(
304 same_graph(&ab, &object([("a", num(1.0))])),
305 Err("$: 2 entries are not 1".into())
306 );
307 }
308
309 #[test]
312 fn differences_are_located() {
313 let a = array([num(1.0), object([("k", array([num(2.0)]))])]);
314 let b = array([num(1.0), object([("k", array([num(3.0)]))])]);
315 assert_eq!(
316 same_graph(&a, &b),
317 Err("$[1].\"k\"[0]: 2.0 is not 3.0".into())
318 );
319 assert_eq!(
320 same_graph(&array([num(1.0)]), &array([])),
321 Err("$: length 1 is not 0".into())
322 );
323 assert!(same_graph(&array([]), &object([])).is_err());
324 }
325}