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