1use crate::host::{
5 call_method, invoke, is_callable, promise_of, reject_promise_val, resolve_promise_val,
6 subscribe_native, take_exc_or_error, type_error, with_host, JsObj, PromiseState,
7};
8use fusevm::Value;
9
10pub const METHODS: &[&str] = &[
11 "ok",
12 "equal",
13 "notEqual",
14 "strictEqual",
15 "notStrictEqual",
16 "deepEqual",
17 "notDeepEqual",
18 "deepStrictEqual",
19 "notDeepStrictEqual",
20 "throws",
21 "doesNotThrow",
22 "fail",
23 "match",
24 "doesNotMatch",
25 "ifError",
26 "partialDeepStrictEqual",
27 "rejects",
28 "doesNotReject",
29];
30
31pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
32 let a = || args.first().cloned().unwrap_or(Value::Undef);
33 let b = || args.get(1).cloned().unwrap_or(Value::Undef);
34 Some(match method {
35 "ok" => assert_ok(args),
36 "equal" => check(loose_eq(&a(), &b()), args, 2, "==", &a(), &b()),
37 "notEqual" => check(!loose_eq(&a(), &b()), args, 2, "!=", &a(), &b()),
38 "strictEqual" => check(strict(&a(), &b()), args, 2, "strictEqual", &a(), &b()),
39 "notStrictEqual" => check(!strict(&a(), &b()), args, 2, "notStrictEqual", &a(), &b()),
40 "deepEqual" => check(
41 deep_equal(&a(), &b(), false),
42 args,
43 2,
44 "deepEqual",
45 &a(),
46 &b(),
47 ),
48 "notDeepEqual" => check(
49 !deep_equal(&a(), &b(), false),
50 args,
51 2,
52 "notDeepEqual",
53 &a(),
54 &b(),
55 ),
56 "deepStrictEqual" => check(
57 deep_equal(&a(), &b(), true),
58 args,
59 2,
60 "deepStrictEqual",
61 &a(),
62 &b(),
63 ),
64 "notDeepStrictEqual" => check(
65 !deep_equal(&a(), &b(), true),
66 args,
67 2,
68 "notDeepStrictEqual",
69 &a(),
70 &b(),
71 ),
72 "throws" => throws(args, true),
73 "doesNotThrow" => throws(args, false),
74 "fail" => Err(fail_msg(args, 0, "Failed")),
75 "match" => assert_match(args, true),
76 "doesNotMatch" => assert_match(args, false),
77 "ifError" => if_error(&a()),
78 "partialDeepStrictEqual" => partial(&a(), &b(), args),
79 "rejects" => Ok(rejects_impl(&a(), true)),
80 "doesNotReject" => Ok(rejects_impl(&a(), false)),
81 _ => return None,
82 })
83}
84
85pub fn strict_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
88 let mapped = match method {
89 "equal" => "strictEqual",
90 "notEqual" => "notStrictEqual",
91 "deepEqual" => "deepStrictEqual",
92 "notDeepEqual" => "notDeepStrictEqual",
93 other => other,
94 };
95 call(mapped, args)
96}
97
98fn assert_match(args: &[Value], want_match: bool) -> Result<Value, String> {
101 let s = args.first().cloned().unwrap_or(Value::Undef);
102 let re = args.get(1).cloned().unwrap_or(Value::Undef);
103 if !with_host(|h| matches!(h.get(&re), Some(JsObj::RegExp(_)))) {
104 return Err(type_error(
105 "The \"regexp\" argument must be an instance of RegExp.",
106 ));
107 }
108 let matched = call_method(&re, "test", vec![s.clone()])?;
109 let matched = with_host(|h| h.truthy(&matched));
110 if matched == want_match {
111 return Ok(Value::Undef);
112 }
113 if let Some(m) = message(args, 2) {
114 return Err(assertion_error(&m));
115 }
116 let (sre, sstr) = with_host(|h| (h.inspect(&re), h.str_of(&s)));
117 let verb = if want_match {
118 "The input did not match the regular expression"
119 } else {
120 "The input was expected to not match the regular expression"
121 };
122 Err(assertion_error(&format!("{verb} {sre}. Input: '{sstr}'")))
123}
124
125fn if_error(v: &Value) -> Result<Value, String> {
127 if with_host(|h| h.is_nullish(v)) {
128 return Ok(Value::Undef);
129 }
130 let desc = with_host(|h| match h.get(v) {
131 Some(JsObj::Object(p)) => p
132 .get("message")
133 .map(|m| h.str_of(m))
134 .unwrap_or_else(|| h.inspect(v)),
135 _ => h.inspect(v),
136 });
137 Err(assertion_error(&format!(
138 "ifError got unwanted exception: {desc}"
139 )))
140}
141
142fn partial(actual: &Value, expected: &Value, args: &[Value]) -> Result<Value, String> {
146 if partial_deep(actual, expected) {
147 return Ok(Value::Undef);
148 }
149 if let Some(m) = message(args, 2) {
150 return Err(assertion_error(&m));
151 }
152 let (sa, sb) = with_host(|h| (h.inspect(actual), h.inspect(expected)));
153 Err(assertion_error(&format!(
154 "Expected values to be strictly deep-equal (partial):\n{sb} should be a subset of {sa}"
155 )))
156}
157
158fn partial_deep(actual: &Value, expected: &Value) -> bool {
159 let ekind = with_host(|h| h.get(expected).map(kind));
160 match ekind {
161 Some(Kind::Object) => {
162 if !matches!(with_host(|h| h.get(actual).map(kind)), Some(Kind::Object)) {
163 return false;
164 }
165 let (ea, ee) = with_host(|h| (object_of(h, actual), object_of(h, expected)));
166 ee.iter().all(|(k, ve)| {
167 ea.iter()
168 .find(|(k2, _)| k2 == k)
169 .is_some_and(|(_, va)| partial_deep(va, ve))
170 })
171 }
172 Some(Kind::Array) => {
173 if !matches!(with_host(|h| h.get(actual).map(kind)), Some(Kind::Array)) {
174 return false;
175 }
176 let (ia, ie) = with_host(|h| (array_of(h, actual), array_of(h, expected)));
177 ie.len() <= ia.len() && ie.iter().zip(ia.iter()).all(|(e, a)| partial_deep(a, e))
178 }
179 _ => strict(actual, expected),
180 }
181}
182
183fn rejects_impl(input: &Value, want_reject: bool) -> Value {
187 let result = with_host(|h| h.new_promise());
188 let rid = with_host(|h| h.promise_id(&result).unwrap());
189 let operand = if with_host(|h| is_callable(h, input)) {
191 match invoke(input, Vec::new(), None) {
192 Ok(v) => promise_of(&v),
193 Err(e) => {
194 let ev = take_exc_or_error(&e);
195 let p = with_host(|h| h.new_promise());
196 let pid = with_host(|h| h.promise_id(&p).unwrap());
197 reject_promise_val(pid, ev);
198 p
199 }
200 }
201 } else {
202 promise_of(input)
203 };
204 let Some(oid) = with_host(|h| h.promise_id(&operand)) else {
205 settle_rejects(rid, false, want_reject);
207 return result;
208 };
209 subscribe_native(
210 oid,
211 Box::new(move |state, _val| {
212 settle_rejects(rid, state == PromiseState::Rejected, want_reject);
213 Ok(())
214 }),
215 );
216 result
217}
218
219pub fn construct_assertion_error(args: &[Value]) -> Value {
223 let opts = args.first().cloned().unwrap_or(Value::Undef);
224 let (message, actual, expected, operator) = with_host(|h| match h.get(&opts) {
225 Some(JsObj::Object(p)) => (
226 p.get("message").map(|v| h.str_of(v)),
227 p.get("actual").cloned(),
228 p.get("expected").cloned(),
229 p.get("operator").map(|v| h.str_of(v)),
230 ),
231 _ => (None, None, None, None),
232 });
233 let generated = message.is_none();
234 let msg = message.unwrap_or_else(|| {
235 let (sa, se) = with_host(|h| {
236 (
237 actual.as_ref().map(|v| h.inspect(v)).unwrap_or_default(),
238 expected.as_ref().map(|v| h.inspect(v)).unwrap_or_default(),
239 )
240 });
241 let op = operator.clone().unwrap_or_else(|| "==".to_string());
242 format!("{sa} {op} {se}")
243 });
244 let stack = format!("AssertionError [ERR_ASSERTION]: {msg}\n at <anonymous>");
245 let op_val = operator
246 .map(|o| with_host(|h| h.new_str(o)))
247 .unwrap_or(Value::Undef);
248 let name_v = with_host(|h| h.new_str("AssertionError"));
249 let msg_v = with_host(|h| h.new_str(msg));
250 let code_v = with_host(|h| h.new_str("ERR_ASSERTION"));
251 let stack_v = with_host(|h| h.new_str(stack));
252 let mut props: indexmap::IndexMap<String, Value> = indexmap::IndexMap::new();
253 props.insert("name".into(), name_v);
254 props.insert("message".into(), msg_v);
255 props.insert("code".into(), code_v);
256 props.insert("actual".into(), actual.unwrap_or(Value::Undef));
257 props.insert("expected".into(), expected.unwrap_or(Value::Undef));
258 props.insert("operator".into(), op_val);
259 props.insert("generatedMessage".into(), Value::Bool(generated));
260 props.insert("stack".into(), stack_v);
261 let obj = with_host(|h| h.new_object(props));
262 with_host(|h| {
263 h.ensure_error_protos();
264 if let Some(p) = crate::host::error_proto_of(h, "Error") {
265 h.set_proto(&obj, p);
266 }
267 });
268 obj
269}
270
271fn settle_rejects(rid: u32, rejected: bool, want_reject: bool) {
272 if rejected == want_reject {
273 resolve_promise_val(rid, Value::Undef);
274 } else {
275 let msg = if want_reject {
276 "AssertionError [ERR_ASSERTION]: Missing expected rejection."
277 } else {
278 "AssertionError [ERR_ASSERTION]: Got unwanted rejection."
279 };
280 let ev = with_host(|h| crate::builtins::synth_error(h, msg));
281 reject_promise_val(rid, ev);
282 }
283}
284
285pub fn assert_ok(args: &[Value]) -> Result<Value, String> {
287 let v = args.first().cloned().unwrap_or(Value::Undef);
288 if with_host(|h| h.truthy(&v)) {
289 Ok(Value::Undef)
290 } else {
291 Err(fail_msg(
292 args,
293 1,
294 "The expression evaluated to a falsy value",
295 ))
296 }
297}
298
299fn check(
300 pass: bool,
301 args: &[Value],
302 msg_idx: usize,
303 op: &str,
304 a: &Value,
305 b: &Value,
306) -> Result<Value, String> {
307 if pass {
308 return Ok(Value::Undef);
309 }
310 if let Some(m) = message(args, msg_idx) {
311 return Err(assertion_error(&m));
312 }
313 let (sa, sb) = with_host(|h| (h.inspect(a), h.inspect(b)));
314 Err(assertion_error(&format!("{sa} {op} {sb}")))
315}
316
317fn throws(args: &[Value], want_throw: bool) -> Result<Value, String> {
318 let f = args.first().cloned().unwrap_or(Value::Undef);
319 let threw = invoke(&f, Vec::new(), None).is_err();
320 match (threw, want_throw) {
321 (true, true) | (false, false) => Ok(Value::Undef),
322 (false, true) => Err(assertion_error("Missing expected exception.")),
323 (true, false) => Err(assertion_error("Got unwanted exception.")),
324 }
325}
326
327fn message(args: &[Value], idx: usize) -> Option<String> {
328 match args.get(idx) {
329 Some(Value::Undef) | None => None,
330 Some(v) => Some(with_host(|h| h.str_of(v))),
331 }
332}
333
334fn fail_msg(args: &[Value], idx: usize, default: &str) -> String {
335 assertion_error(&message(args, idx).unwrap_or_else(|| default.to_string()))
336}
337
338fn assertion_error(msg: &str) -> String {
339 format!("AssertionError [ERR_ASSERTION]: {msg}")
340}
341
342fn strict(a: &Value, b: &Value) -> bool {
343 with_host(|h| h.strict_eq(a, b))
344}
345
346fn loose_eq(a: &Value, b: &Value) -> bool {
347 if strict(a, b) {
348 return true;
349 }
350 with_host(|h| {
351 let (na, nb) = (h.to_number(a), h.to_number(b));
352 if !na.is_nan() && !nb.is_nan() && (na == nb) {
353 return true;
354 }
355 h.str_of(a) == h.str_of(b)
356 })
357}
358
359pub fn deep_equal(a: &Value, b: &Value, strict_mode: bool) -> bool {
361 let kinds = with_host(|h| {
362 let av = h.get(a).map(kind);
363 let bv = h.get(b).map(kind);
364 (av, bv)
365 });
366 match kinds {
367 (Some(Kind::Array), Some(Kind::Array)) => {
368 let (ia, ib) = with_host(|h| (array_of(h, a), array_of(h, b)));
369 ia.len() == ib.len()
370 && ia
371 .iter()
372 .zip(ib.iter())
373 .all(|(x, y)| deep_equal(x, y, strict_mode))
374 }
375 (Some(Kind::Object), Some(Kind::Object)) => {
376 let (ea, eb) = with_host(|h| (object_of(h, a), object_of(h, b)));
377 if ea.len() != eb.len() {
378 return false;
379 }
380 ea.iter().all(|(k, va)| {
381 eb.iter()
382 .find(|(k2, _)| k2 == k)
383 .is_some_and(|(_, vb)| deep_equal(va, vb, strict_mode))
384 })
385 }
386 _ => {
387 if strict_mode {
388 strict(a, b)
389 } else {
390 loose_eq(a, b)
391 }
392 }
393 }
394}
395
396enum Kind {
397 Array,
398 Object,
399}
400fn kind(o: &JsObj) -> Kind {
401 match o {
402 JsObj::Array(_) => Kind::Array,
403 _ => Kind::Object,
404 }
405}
406fn array_of(h: &crate::host::JsHost, v: &Value) -> Vec<Value> {
407 match h.get(v) {
408 Some(JsObj::Array(items)) => items.clone(),
409 _ => Vec::new(),
410 }
411}
412fn object_of(h: &crate::host::JsHost, v: &Value) -> Vec<(String, Value)> {
413 match h.get(v) {
414 Some(JsObj::Object(p)) => p
415 .iter()
416 .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
417 .map(|(k, v)| (k.clone(), v.clone()))
418 .collect(),
419 _ => Vec::new(),
420 }
421}