1use crate::host::{
5 call_method, invoke, is_callable, promise_of, reject_promise_val, resolve_promise_val,
6 subscribe_native, take_exc_or_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, "===", &a(), &b()),
39 "notStrictEqual" => check(!strict(&a(), &b()), args, 2, "!==", &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(throw_assertion(
77 &message(args, 0).unwrap_or_else(|| "Failed".to_string()),
78 message(args, 0).is_none(),
79 "fail",
80 Value::Undef,
81 Value::Undef,
82 )),
83 "match" => assert_match(args, true),
84 "doesNotMatch" => assert_match(args, false),
85 "ifError" => if_error(&a()),
86 "partialDeepStrictEqual" => partial(&a(), &b(), args),
87 "rejects" => Ok(rejects_impl(&a(), true)),
88 "doesNotReject" => Ok(rejects_impl(&a(), false)),
89 _ => return None,
90 })
91}
92
93pub fn strict_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
96 let mapped = match method {
97 "equal" => "strictEqual",
98 "notEqual" => "notStrictEqual",
99 "deepEqual" => "deepStrictEqual",
100 "notDeepEqual" => "notDeepStrictEqual",
101 other => other,
102 };
103 call(mapped, args)
104}
105
106fn assert_match(args: &[Value], want_match: bool) -> Result<Value, String> {
109 let s = args.first().cloned().unwrap_or(Value::Undef);
110 let re = args.get(1).cloned().unwrap_or(Value::Undef);
111 if !with_host(|h| matches!(h.get(&re), Some(JsObj::RegExp(_)))) {
112 return Err(crate::host::coded_error(
114 "TypeError",
115 "ERR_INVALID_ARG_TYPE",
116 &format!(
117 "The \"regexp\" argument must be an instance of RegExp. Received {}",
118 crate::stdlib::received_desc(&re)
119 ),
120 ));
121 }
122 let matched = call_method(&re, "test", vec![s.clone()])?;
123 let matched = with_host(|h| h.truthy(&matched));
124 if matched == want_match {
125 return Ok(Value::Undef);
126 }
127 if let Some(m) = message(args, 2) {
128 return Err(assertion_error(&m));
129 }
130 let (sre, sstr) = with_host(|h| (h.inspect(&re), h.str_of(&s)));
131 let verb = if want_match {
132 "The input did not match the regular expression"
133 } else {
134 "The input was expected to not match the regular expression"
135 };
136 Err(assertion_error(&format!("{verb} {sre}. Input: '{sstr}'")))
137}
138
139fn if_error(v: &Value) -> Result<Value, String> {
141 if with_host(|h| h.is_nullish(v)) {
142 return Ok(Value::Undef);
143 }
144 let desc = with_host(|h| match h.get(v) {
145 Some(JsObj::Object(p)) => p
146 .get("message")
147 .map(|m| h.str_of(m))
148 .unwrap_or_else(|| h.inspect(v)),
149 _ => h.inspect(v),
150 });
151 Err(assertion_error(&format!(
152 "ifError got unwanted exception: {desc}"
153 )))
154}
155
156fn partial(actual: &Value, expected: &Value, args: &[Value]) -> Result<Value, String> {
160 if partial_deep(actual, expected) {
161 return Ok(Value::Undef);
162 }
163 if let Some(m) = message(args, 2) {
164 return Err(assertion_error(&m));
165 }
166 let (sa, sb) = with_host(|h| (h.inspect(actual), h.inspect(expected)));
167 Err(assertion_error(&format!(
168 "Expected values to be strictly deep-equal (partial):\n{sb} should be a subset of {sa}"
169 )))
170}
171
172fn partial_deep(actual: &Value, expected: &Value) -> bool {
173 let ekind = with_host(|h| h.get(expected).map(kind));
174 match ekind {
175 Some(Kind::Object) => {
176 if !matches!(with_host(|h| h.get(actual).map(kind)), Some(Kind::Object)) {
177 return false;
178 }
179 let (ea, ee) = with_host(|h| (object_of(h, actual), object_of(h, expected)));
180 ee.iter().all(|(k, ve)| {
181 ea.iter()
182 .find(|(k2, _)| k2 == k)
183 .is_some_and(|(_, va)| partial_deep(va, ve))
184 })
185 }
186 Some(Kind::Array) => {
187 if !matches!(with_host(|h| h.get(actual).map(kind)), Some(Kind::Array)) {
188 return false;
189 }
190 let (ia, ie) = with_host(|h| (array_of(h, actual), array_of(h, expected)));
191 ie.len() <= ia.len() && ie.iter().zip(ia.iter()).all(|(e, a)| partial_deep(a, e))
192 }
193 _ => strict(actual, expected),
194 }
195}
196
197fn rejects_impl(input: &Value, want_reject: bool) -> Value {
201 let result = with_host(|h| h.new_promise());
202 let rid = with_host(|h| h.promise_id(&result).unwrap());
203 let operand = if with_host(|h| is_callable(h, input)) {
205 match invoke(input, Vec::new(), None) {
206 Ok(v) => promise_of(&v),
207 Err(e) => {
208 let ev = take_exc_or_error(&e);
209 let p = with_host(|h| h.new_promise());
210 let pid = with_host(|h| h.promise_id(&p).unwrap());
211 reject_promise_val(pid, ev);
212 p
213 }
214 }
215 } else {
216 promise_of(input)
217 };
218 let Some(oid) = with_host(|h| h.promise_id(&operand)) else {
219 settle_rejects(rid, false, want_reject);
221 return result;
222 };
223 subscribe_native(
224 oid,
225 Box::new(move |state, _val| {
226 settle_rejects(rid, state == PromiseState::Rejected, want_reject);
227 Ok(())
228 }),
229 );
230 result
231}
232
233pub fn construct_assertion_error(args: &[Value]) -> Value {
237 let opts = args.first().cloned().unwrap_or(Value::Undef);
238 let (message, actual, expected, operator) = with_host(|h| match h.get(&opts) {
239 Some(JsObj::Object(p)) => (
240 p.get("message").map(|v| h.str_of(v)),
241 p.get("actual").cloned(),
242 p.get("expected").cloned(),
243 p.get("operator").map(|v| h.str_of(v)),
244 ),
245 _ => (None, None, None, None),
246 });
247 let generated = message.is_none();
248 let msg = message.unwrap_or_else(|| {
249 let (sa, se) = with_host(|h| {
250 (
251 actual.as_ref().map(|v| h.inspect(v)).unwrap_or_default(),
252 expected.as_ref().map(|v| h.inspect(v)).unwrap_or_default(),
253 )
254 });
255 let op = operator.clone().unwrap_or_else(|| "==".to_string());
256 format!("{sa} {op} {se}")
257 });
258 assertion_error_object(
259 &msg,
260 generated,
261 operator.as_deref(),
262 actual.unwrap_or(Value::Undef),
263 expected.unwrap_or(Value::Undef),
264 )
265}
266
267const DIFF_MODE: &str = "simple";
272
273fn assertion_error_object(
283 msg: &str,
284 generated: bool,
285 operator: Option<&str>,
286 actual: Value,
287 expected: Value,
288) -> Value {
289 let stack = format!("AssertionError [ERR_ASSERTION]: {msg}\n at <anonymous>");
290 let op_val = match operator {
291 Some(o) => with_host(|h| h.new_str(o)),
292 None => Value::Undef,
293 };
294 let name_v = with_host(|h| h.new_str("AssertionError"));
295 let msg_v = with_host(|h| h.new_str(msg));
296 let code_v = with_host(|h| h.new_str("ERR_ASSERTION"));
297 let stack_v = with_host(|h| h.new_str(stack));
298 let diff_v = with_host(|h| h.new_str(DIFF_MODE));
299 let mut props: indexmap::IndexMap<String, Value> = indexmap::IndexMap::new();
300 props.insert("generatedMessage".into(), Value::Bool(generated));
302 props.insert("code".into(), code_v);
303 props.insert("actual".into(), actual);
304 props.insert("expected".into(), expected);
305 props.insert("operator".into(), op_val);
306 props.insert("diff".into(), diff_v);
307 props.insert("name".into(), name_v);
308 props.insert("message".into(), msg_v);
309 props.insert("stack".into(), stack_v);
310 let obj = with_host(|h| h.new_object(props));
311 with_host(|h| {
312 for k in ["name", "message", "stack"] {
313 h.hide_prop(&obj, k);
314 }
315 h.ensure_error_protos();
316 if let Some(p) = crate::host::error_proto_of(h, "AssertionError") {
320 h.set_proto(&obj, p);
321 }
322 });
323 obj
324}
325
326fn throw_assertion(
333 msg: &str,
334 generated: bool,
335 operator: &str,
336 actual: Value,
337 expected: Value,
338) -> String {
339 let err = assertion_error_object(msg, generated, Some(operator), actual, expected);
340 with_host(|h| h.exc = Some(err));
341 assertion_error(msg)
342}
343
344fn settle_rejects(rid: u32, rejected: bool, want_reject: bool) {
345 if rejected == want_reject {
346 resolve_promise_val(rid, Value::Undef);
347 } else {
348 let msg = if want_reject {
349 "AssertionError [ERR_ASSERTION]: Missing expected rejection."
350 } else {
351 "AssertionError [ERR_ASSERTION]: Got unwanted rejection."
352 };
353 let ev = with_host(|h| crate::builtins::synth_error(h, msg));
354 reject_promise_val(rid, ev);
355 }
356}
357
358pub fn assert_ok(args: &[Value]) -> Result<Value, String> {
360 let v = args.first().cloned().unwrap_or(Value::Undef);
361 if with_host(|h| h.truthy(&v)) {
362 return Ok(Value::Undef);
363 }
364 let custom = message(args, 1);
365 let msg = custom.clone().unwrap_or_else(||
366 "The expression evaluated to a falsy value:".to_string());
369 Err(throw_assertion(
373 &msg,
374 custom.is_none(),
375 "==",
376 v,
377 Value::Bool(true),
378 ))
379}
380
381fn check(
382 pass: bool,
383 args: &[Value],
384 msg_idx: usize,
385 op: &str,
386 a: &Value,
387 b: &Value,
388) -> Result<Value, String> {
389 if pass {
390 return Ok(Value::Undef);
391 }
392 let custom = message(args, msg_idx);
393 let diff_operator = match op {
399 "===" => Some("strictEqual"),
400 "deepStrictEqual" => Some("deepStrictEqual"),
401 "partialDeepStrictEqual" => Some("partialDeepStrictEqual"),
402 _ => None,
403 };
404 if let Some(diff_op) = diff_operator {
405 let msg = super::assert_diff::create_err_diff(a, b, diff_op, custom.as_deref());
406 let operator = if op == "===" { "strictEqual" } else { op };
407 return Err(throw_assertion(
408 &msg,
409 custom.is_none(),
410 operator,
411 a.clone(),
412 b.clone(),
413 ));
414 }
415 let (sa, sb) = (
420 super::assert_diff::inspect_operand(a),
421 super::assert_diff::inspect_operand(b),
422 );
423 let msg = match op {
428 "==" | "!=" => format!("{sa} {op} {sb}"),
429 "===" => format!("Expected values to be strictly equal:\n\n{sa} !== {sb}\n"),
430 "!==" => format!("Expected \"actual\" to be strictly unequal to: {sa}"),
431 "deepEqual" => format!(
432 "Expected values to be loosely deep-equal:\n\n{sa}\n\nshould loosely \
433 deep-equal\n\n{sb}"
434 ),
435 "notDeepEqual" => {
436 format!("Expected \"actual\" not to be loosely deep-equal to:\n\n{sa}")
437 }
438 "deepStrictEqual" => {
443 format!("Expected values to be strictly deep-equal:\n\n{sa} !== {sb}\n")
444 }
445 "notDeepStrictEqual" => {
446 format!("Expected \"actual\" not to be strictly deep-equal to:\n\n{sa}\n")
447 }
448 _ => format!("{sa} {op} {sb}"),
449 };
450 let operator = match op {
455 "===" => "strictEqual",
456 "!==" => "notStrictEqual",
457 other => other,
458 };
459 Err(throw_assertion(
460 &custom.clone().unwrap_or(msg),
461 custom.is_none(),
462 operator,
463 a.clone(),
464 b.clone(),
465 ))
466}
467
468fn throws(args: &[Value], want_throw: bool) -> Result<Value, String> {
469 let f = args.first().cloned().unwrap_or(Value::Undef);
470 let caught = match invoke(&f, Vec::new(), None) {
473 Ok(_) => None,
474 Err(e) => Some(crate::host::take_exc_or_error(&e)),
475 };
476 let threw = caught.is_some();
477 match (threw, want_throw) {
478 (true, true) | (false, false) => Ok(Value::Undef),
479 (false, true) => Err(throw_assertion(
482 "Missing expected exception.",
483 false,
484 "throws",
485 Value::Undef,
486 Value::Undef,
487 )),
488 (true, false) => Err(throw_assertion(
489 "Got unwanted exception.",
490 false,
491 "doesNotThrow",
492 caught.unwrap_or(Value::Undef),
493 Value::Undef,
494 )),
495 }
496}
497
498fn message(args: &[Value], idx: usize) -> Option<String> {
499 match args.get(idx) {
500 Some(Value::Undef) | None => None,
501 Some(v) => Some(with_host(|h| h.str_of(v))),
502 }
503}
504
505fn assertion_error(msg: &str) -> String {
512 crate::host::coded_error("AssertionError", "ERR_ASSERTION", msg)
513}
514
515fn strict(a: &Value, b: &Value) -> bool {
521 crate::builtins::same_value(a, b)
522}
523
524fn loose_eq(a: &Value, b: &Value) -> bool {
525 if strict(a, b) {
526 return true;
527 }
528 with_host(|h| {
529 let (na, nb) = (h.to_number(a), h.to_number(b));
530 if !na.is_nan() && !nb.is_nan() && (na == nb) {
531 return true;
532 }
533 h.str_of(a) == h.str_of(b)
534 })
535}
536
537pub fn deep_equal(a: &Value, b: &Value, strict_mode: bool) -> bool {
539 deep_equal_seen(a, b, strict_mode, &mut Vec::new())
540}
541
542fn deep_equal_seen(
550 a: &Value,
551 b: &Value,
552 strict_mode: bool,
553 seen: &mut Vec<(Value, Value)>,
554) -> bool {
555 if seen.iter().any(|(x, y)| x == a && y == b) {
556 return true;
557 }
558 if strict_mode {
563 let both_objects = with_host(|h| h.get(a).is_some() && h.get(b).is_some());
564 if both_objects
565 && with_host(|h| {
566 h.proto_of(a) != h.proto_of(b) || h.has_null_proto(a) != h.has_null_proto(b)
571 })
572 {
573 return false;
574 }
575 }
576 let kinds = with_host(|h| {
577 let av = h.get(a).map(kind);
578 let bv = h.get(b).map(kind);
579 (av, bv)
580 });
581 seen.push((a.clone(), b.clone()));
582 let result = deep_equal_body(a, b, strict_mode, seen, kinds);
583 seen.pop();
584 result
585}
586
587fn deep_equal_body(
588 a: &Value,
589 b: &Value,
590 strict_mode: bool,
591 seen: &mut Vec<(Value, Value)>,
592 kinds: (Option<Kind>, Option<Kind>),
593) -> bool {
594 match kinds {
595 (Some(Kind::Array), Some(Kind::Array)) => {
596 let (ia, ib) = with_host(|h| (array_of(h, a), array_of(h, b)));
597 ia.len() == ib.len()
598 && ia
599 .iter()
600 .zip(ib.iter())
601 .all(|(x, y)| deep_equal_seen(x, y, strict_mode, seen))
602 }
603 (Some(Kind::Object), Some(Kind::Object)) => {
604 let (ea, eb) = with_host(|h| (object_of(h, a), object_of(h, b)));
605 if ea.len() != eb.len() {
606 return false;
607 }
608 let props_match = ea.iter().all(|(k, va)| {
609 eb.iter()
610 .find(|(k2, _)| k2 == k)
611 .is_some_and(|(_, vb)| deep_equal_seen(va, vb, strict_mode, seen))
612 });
613 if !props_match {
614 return false;
615 }
616 let (ia, ib) = with_host(|h| (internals_of(h, a), internals_of(h, b)));
626 if ia.len() != ib.len() {
627 return false;
628 }
629 ia.iter().all(|(k, va)| {
630 ib.iter()
631 .find(|(k2, _)| k2 == k)
632 .is_some_and(|(_, vb)| deep_equal_seen(va, vb, strict_mode, seen))
633 })
634 }
635 (Some(Kind::Map), Some(Kind::Map)) => {
640 let (ea, eb) = with_host(|h| (map_entries_of(h, a), map_entries_of(h, b)));
641 unordered_match(&ea, &eb, seen, |(ka, va), (kb, vb), seen| {
642 deep_equal_seen(ka, kb, strict_mode, seen)
643 && deep_equal_seen(va, vb, strict_mode, seen)
644 })
645 }
646 (Some(Kind::Set), Some(Kind::Set)) => {
647 let (ea, eb) = with_host(|h| (set_members_of(h, a), set_members_of(h, b)));
648 unordered_match(&ea, &eb, seen, |x, y, seen| {
649 deep_equal_seen(x, y, strict_mode, seen)
650 })
651 }
652 (Some(Kind::RegExp), Some(Kind::RegExp)) => {
655 with_host(|h| regexp_key(h, a) == regexp_key(h, b))
656 }
657 _ => {
663 if strict_mode {
664 strict(a, b)
665 } else {
666 loose_eq(a, b)
667 }
668 }
669 }
670}
671
672fn unordered_match<T>(
677 ea: &[T],
678 eb: &[T],
679 seen: &mut Vec<(Value, Value)>,
680 eq: impl Fn(&T, &T, &mut Vec<(Value, Value)>) -> bool,
681) -> bool {
682 if ea.len() != eb.len() {
683 return false;
684 }
685 let mut claimed = vec![false; eb.len()];
686 'outer: for x in ea {
687 for (i, y) in eb.iter().enumerate() {
688 if !claimed[i] && eq(x, y, seen) {
689 claimed[i] = true;
690 continue 'outer;
691 }
692 }
693 return false;
694 }
695 true
696}
697
698enum Kind {
699 Array,
700 Object,
701 Map,
702 Set,
703 RegExp,
704 Other,
706}
707fn kind(o: &JsObj) -> Kind {
708 match o {
709 JsObj::Array(_) => Kind::Array,
710 JsObj::Object(_) => Kind::Object,
711 JsObj::Map { weak: false, .. } => Kind::Map,
714 JsObj::Set { weak: false, .. } => Kind::Set,
715 JsObj::RegExp(_) => Kind::RegExp,
716 _ => Kind::Other,
717 }
718}
719fn map_entries_of(h: &crate::host::JsHost, v: &Value) -> Vec<(Value, Value)> {
721 match h.get(v) {
722 Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
723 _ => Vec::new(),
724 }
725}
726fn set_members_of(h: &crate::host::JsHost, v: &Value) -> Vec<Value> {
728 match h.get(v) {
729 Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
730 _ => Vec::new(),
731 }
732}
733fn regexp_key(h: &crate::host::JsHost, v: &Value) -> Option<(String, String)> {
735 match h.get(v) {
736 Some(JsObj::RegExp(r)) => Some((r.source.clone(), r.flags.clone())),
737 _ => None,
738 }
739}
740fn internals_of(h: &crate::host::JsHost, v: &Value) -> Vec<(String, Value)> {
745 match h.get(v) {
746 Some(JsObj::Object(p)) => p
747 .iter()
748 .filter(|(k, _)| k.starts_with("@@"))
749 .map(|(k, v)| (k.clone(), v.clone()))
750 .collect(),
751 _ => Vec::new(),
752 }
753}
754fn array_of(h: &crate::host::JsHost, v: &Value) -> Vec<Value> {
755 match h.get(v) {
756 Some(JsObj::Array(items)) => items.clone(),
757 _ => Vec::new(),
758 }
759}
760fn object_of(h: &crate::host::JsHost, v: &Value) -> Vec<(String, Value)> {
761 match h.get(v) {
762 Some(JsObj::Object(p)) => p
763 .iter()
764 .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
765 .map(|(k, v)| (k.clone(), v.clone()))
766 .collect(),
767 _ => Vec::new(),
768 }
769}