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 (sa, sb) = with_host(|h| (h.inspect(a), h.inspect(b)));
394 let msg = match op {
399 "==" | "!=" => format!("{sa} {op} {sb}"),
400 "===" => format!("Expected values to be strictly equal:\n\n{sa} !== {sb}\n"),
401 "!==" => format!("Expected \"actual\" to be strictly unequal to: {sa}"),
402 "deepEqual" => format!(
403 "Expected values to be loosely deep-equal:\n\n{sa}\n\nshould loosely \
404 deep-equal\n\n{sb}"
405 ),
406 "notDeepEqual" => {
407 format!("Expected \"actual\" not to be loosely deep-equal to:\n\n{sa}")
408 }
409 "deepStrictEqual" => {
414 format!("Expected values to be strictly deep-equal:\n\n{sa} !== {sb}\n")
415 }
416 "notDeepStrictEqual" => {
417 format!("Expected \"actual\" not to be strictly deep-equal to:\n\n{sa}\n")
418 }
419 _ => format!("{sa} {op} {sb}"),
420 };
421 let operator = match op {
426 "===" => "strictEqual",
427 "!==" => "notStrictEqual",
428 other => other,
429 };
430 Err(throw_assertion(
431 &custom.clone().unwrap_or(msg),
432 custom.is_none(),
433 operator,
434 a.clone(),
435 b.clone(),
436 ))
437}
438
439fn throws(args: &[Value], want_throw: bool) -> Result<Value, String> {
440 let f = args.first().cloned().unwrap_or(Value::Undef);
441 let caught = match invoke(&f, Vec::new(), None) {
444 Ok(_) => None,
445 Err(e) => Some(crate::host::take_exc_or_error(&e)),
446 };
447 let threw = caught.is_some();
448 match (threw, want_throw) {
449 (true, true) | (false, false) => Ok(Value::Undef),
450 (false, true) => Err(throw_assertion(
453 "Missing expected exception.",
454 false,
455 "throws",
456 Value::Undef,
457 Value::Undef,
458 )),
459 (true, false) => Err(throw_assertion(
460 "Got unwanted exception.",
461 false,
462 "doesNotThrow",
463 caught.unwrap_or(Value::Undef),
464 Value::Undef,
465 )),
466 }
467}
468
469fn message(args: &[Value], idx: usize) -> Option<String> {
470 match args.get(idx) {
471 Some(Value::Undef) | None => None,
472 Some(v) => Some(with_host(|h| h.str_of(v))),
473 }
474}
475
476fn assertion_error(msg: &str) -> String {
483 crate::host::coded_error("AssertionError", "ERR_ASSERTION", msg)
484}
485
486fn strict(a: &Value, b: &Value) -> bool {
487 with_host(|h| h.strict_eq(a, b))
488}
489
490fn loose_eq(a: &Value, b: &Value) -> bool {
491 if strict(a, b) {
492 return true;
493 }
494 with_host(|h| {
495 let (na, nb) = (h.to_number(a), h.to_number(b));
496 if !na.is_nan() && !nb.is_nan() && (na == nb) {
497 return true;
498 }
499 h.str_of(a) == h.str_of(b)
500 })
501}
502
503pub fn deep_equal(a: &Value, b: &Value, strict_mode: bool) -> bool {
505 let kinds = with_host(|h| {
506 let av = h.get(a).map(kind);
507 let bv = h.get(b).map(kind);
508 (av, bv)
509 });
510 match kinds {
511 (Some(Kind::Array), Some(Kind::Array)) => {
512 let (ia, ib) = with_host(|h| (array_of(h, a), array_of(h, b)));
513 ia.len() == ib.len()
514 && ia
515 .iter()
516 .zip(ib.iter())
517 .all(|(x, y)| deep_equal(x, y, strict_mode))
518 }
519 (Some(Kind::Object), Some(Kind::Object)) => {
520 let (ea, eb) = with_host(|h| (object_of(h, a), object_of(h, b)));
521 if ea.len() != eb.len() {
522 return false;
523 }
524 ea.iter().all(|(k, va)| {
525 eb.iter()
526 .find(|(k2, _)| k2 == k)
527 .is_some_and(|(_, vb)| deep_equal(va, vb, strict_mode))
528 })
529 }
530 _ => {
531 if strict_mode {
532 strict(a, b)
533 } else {
534 loose_eq(a, b)
535 }
536 }
537 }
538}
539
540enum Kind {
541 Array,
542 Object,
543}
544fn kind(o: &JsObj) -> Kind {
545 match o {
546 JsObj::Array(_) => Kind::Array,
547 _ => Kind::Object,
548 }
549}
550fn array_of(h: &crate::host::JsHost, v: &Value) -> Vec<Value> {
551 match h.get(v) {
552 Some(JsObj::Array(items)) => items.clone(),
553 _ => Vec::new(),
554 }
555}
556fn object_of(h: &crate::host::JsHost, v: &Value) -> Vec<(String, Value)> {
557 match h.get(v) {
558 Some(JsObj::Object(p)) => p
559 .iter()
560 .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
561 .map(|(k, v)| (k.clone(), v.clone()))
562 .collect(),
563 _ => Vec::new(),
564 }
565}