Skip to main content

nodejs/stdlib/
assert.rs

1//! Node `assert` module. Failing assertions throw an `AssertionError` (returned
2//! as an `Err`, which the host surfaces as a thrown JS exception).
3
4use crate::host::{
5    call_method, invoke, is_callable, reject_promise_val, resolve_promise_val, subscribe_native,
6    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` carries no operands: `actual`/`expected` are own properties
75        // holding `undefined`, and `operator` is the literal `"fail"`.
76        "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(args, true)),
88        "doesNotReject" => Ok(rejects_impl(args, false)),
89        _ => return None,
90    })
91}
92
93/// The strict-mode variants (`assert.strict.equal` === `assert.strictEqual`).
94/// Maps the loose method names onto their strict counterparts, then delegates.
95pub 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
106/// `assert.match(string, regexp)` / `assert.doesNotMatch(...)`. `regexp` must be
107/// a `RegExp`; matching runs through the JS `RegExp.prototype.test`.
108fn 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 !is_regexp(&re) {
112        // Node names the instance, not a type, and appends the received value.
113        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    // `internalMatch`: a non-string input fails either assertion, and the
123    // test is `RegExp.prototype.exec`, which honours `lastIndex`.
124    let is_string = with_host(|h| h.type_of(&s) == "string");
125    if is_string && regexp_matches(&re, &s)? == want_match {
126        return Ok(Value::Undef);
127    }
128    let operator = if want_match { "match" } else { "doesNotMatch" };
129    let custom = message(args, 2);
130    let generated = custom.is_none();
131    let msg = custom.unwrap_or_else(|| {
132        with_host(|h| {
133            if !is_string {
134                return format!(
135                    "The \"string\" argument must be of type string. Received type {} ({})",
136                    h.type_of(&s),
137                    h.inspect(&s)
138                );
139            }
140            let verb = if want_match {
141                "The input did not match the regular expression"
142            } else {
143                "The input was expected to not match the regular expression"
144            };
145            format!("{verb} {}. Input:\n\n{}\n", h.inspect(&re), h.inspect(&s))
146        })
147    });
148    Err(throw_assertion(&msg, generated, operator, s, re))
149}
150
151fn is_regexp(v: &Value) -> bool {
152    with_host(|h| matches!(h.get(v), Some(JsObj::RegExp(_))))
153}
154
155/// `RegExpPrototypeExec(re, s) !== null`.
156fn regexp_matches(re: &Value, s: &Value) -> Result<bool, String> {
157    let m = call_method(re, "exec", vec![s.clone()])?;
158    Ok(!with_host(|h| h.is_null(&m)) && !matches!(m, Value::Undef))
159}
160
161/// `assert.ifError(value)` — throws unless `value` is `null`/`undefined`.
162fn if_error(v: &Value) -> Result<Value, String> {
163    if with_host(|h| h.is_nullish(v)) {
164        return Ok(Value::Undef);
165    }
166    let desc = with_host(|h| match h.get(v) {
167        Some(JsObj::Object(p)) => p
168            .get("message")
169            .map(|m| h.str_of(m))
170            .unwrap_or_else(|| h.inspect(v)),
171        _ => h.inspect(v),
172    });
173    Err(assertion_error(&format!(
174        "ifError got unwanted exception: {desc}"
175    )))
176}
177
178/// `assert.partialDeepStrictEqual(actual, expected)` — passes when every leaf of
179/// `expected` strict-deep-matches the corresponding part of `actual` (extra
180/// props/elements in `actual` are ignored).
181fn partial(actual: &Value, expected: &Value, args: &[Value]) -> Result<Value, String> {
182    if partial_deep(actual, expected) {
183        return Ok(Value::Undef);
184    }
185    if let Some(m) = message(args, 2) {
186        return Err(assertion_error(&m));
187    }
188    let (sa, sb) = with_host(|h| (h.inspect(actual), h.inspect(expected)));
189    Err(assertion_error(&format!(
190        "Expected values to be strictly deep-equal (partial):\n{sb} should be a subset of {sa}"
191    )))
192}
193
194fn partial_deep(actual: &Value, expected: &Value) -> bool {
195    let ekind = with_host(|h| h.get(expected).map(kind));
196    match ekind {
197        Some(Kind::Object) => {
198            if !matches!(with_host(|h| h.get(actual).map(kind)), Some(Kind::Object)) {
199                return false;
200            }
201            let (ea, ee) = with_host(|h| (object_of(h, actual), object_of(h, expected)));
202            ee.iter().all(|(k, ve)| {
203                ea.iter()
204                    .find(|(k2, _)| k2 == k)
205                    .is_some_and(|(_, va)| partial_deep(va, ve))
206            })
207        }
208        Some(Kind::Array) => {
209            if !matches!(with_host(|h| h.get(actual).map(kind)), Some(Kind::Array)) {
210                return false;
211            }
212            let (ia, ie) = with_host(|h| (array_of(h, actual), array_of(h, expected)));
213            ie.len() <= ia.len() && ie.iter().zip(ia.iter()).all(|(e, a)| partial_deep(a, e))
214        }
215        _ => strict(actual, expected),
216    }
217}
218
219/// `assert.rejects(fn|promise[, error][, message])` / `assert.doesNotReject(...)`
220/// — node's `waitForActual` then `expectsError`/`expectsNoError`, so the
221/// `error` argument validates a rejection exactly as it validates a throw.
222/// Returns the promise those async functions return.
223fn rejects_impl(args: &[Value], want_reject: bool) -> Value {
224    let input = args.first().cloned().unwrap_or(Value::Undef);
225    let rest: Vec<Value> = args.get(1..).unwrap_or(&[]).to_vec();
226    let result = with_host(|h| h.new_promise());
227    let rid = with_host(|h| h.promise_id(&result).unwrap());
228    let fail = |e: String| {
229        let ev = take_exc_or_error(&e);
230        reject_promise_val(rid, ev);
231    };
232    // A function is called and must hand back a promise; anything else must
233    // be one. A synchronous throw rejects this promise with that error, as an
234    // `async function` would.
235    let operand = if with_host(|h| is_callable(h, &input)) {
236        match invoke(&input, Vec::new(), None) {
237            Ok(v) if is_promise_like(&v) => crate::host::promise_of(&v),
238            Ok(v) => {
239                fail(crate::host::coded_error(
240                    "TypeError",
241                    "ERR_INVALID_RETURN_VALUE",
242                    &format!(
243                        "Expected instance of Promise to be returned from the \"promiseFn\" function but got {}.",
244                        crate::stdlib::received_desc(&v)
245                    ),
246                ));
247                return result;
248            }
249            Err(e) => {
250                fail(e);
251                return result;
252            }
253        }
254    } else if is_promise_like(&input) {
255        crate::host::promise_of(&input)
256    } else {
257        fail(crate::host::coded_error(
258            "TypeError",
259            "ERR_INVALID_ARG_TYPE",
260            &format!(
261                "The \"promiseFn\" argument must be of type function or an instance of Promise. Received {}",
262                crate::stdlib::received_desc(&input)
263            ),
264        ));
265        return result;
266    };
267    let oid = with_host(|h| h.promise_id(&operand).unwrap());
268    subscribe_native(
269        oid,
270        Box::new(move |state, val| {
271            let actual = (state == PromiseState::Rejected).then_some(val);
272            let checked = if want_reject {
273                expects_error("rejects", "rejection", actual, &rest)
274            } else {
275                expects_no_error("doesNotReject", "rejection", actual, &rest)
276            };
277            match checked {
278                Ok(()) => resolve_promise_val(rid, Value::Undef),
279                Err(e) => {
280                    let ev = take_exc_or_error(&e);
281                    reject_promise_val(rid, ev);
282                }
283            }
284            Ok(())
285        }),
286    );
287    result
288}
289
290/// `new assert.AssertionError(options)` — a real `Error`-prototype-linked object
291/// carrying `name`/`message`/`code`/`actual`/`expected`/`operator`. Parent wires
292/// this to `construct("AssertionError")` and `constant("assert","AssertionError")`.
293pub fn construct_assertion_error(args: &[Value]) -> Value {
294    let opts = args.first().cloned().unwrap_or(Value::Undef);
295    let (message, actual, expected, operator) = with_host(|h| match h.get(&opts) {
296        Some(JsObj::Object(p)) => (
297            p.get("message").map(|v| h.str_of(v)),
298            p.get("actual").cloned(),
299            p.get("expected").cloned(),
300            p.get("operator").map(|v| h.str_of(v)),
301        ),
302        _ => (None, None, None, None),
303    });
304    let generated = message.is_none();
305    let msg = message.unwrap_or_else(|| {
306        let (sa, se) = with_host(|h| {
307            (
308                actual.as_ref().map(|v| h.inspect(v)).unwrap_or_default(),
309                expected.as_ref().map(|v| h.inspect(v)).unwrap_or_default(),
310            )
311        });
312        let op = operator.clone().unwrap_or_else(|| "==".to_string());
313        format!("{sa} {op} {se}")
314    });
315    assertion_error_object(
316        &msg,
317        generated,
318        operator.as_deref(),
319        actual.unwrap_or(Value::Undef),
320        expected.unwrap_or(Value::Undef),
321    )
322}
323
324/// Node's `diff` field. Every failure form measured on node v26.7.0 — `ok`,
325/// `equal`, `strictEqual`, `notStrictEqual`, `deepEqual`, `deepStrictEqual`,
326/// `match`, `throws`, `fail` — reports the same `"simple"`; it names the diff
327/// MODE the error was built under, not a rendered diff.
328const DIFF_MODE: &str = "simple";
329
330/// Build the `AssertionError` object a failing assertion throws.
331///
332/// The own-property set is what a test runner reads, and node-js carried only
333/// `code`/`message`/`stack`: `err.actual`, `err.expected`, `err.operator` and
334/// `err.generatedMessage` were all `undefined`, so every framework that reports
335/// "expected X, got Y" from a caught `AssertionError` had nothing to report.
336/// Measured on node v26.7.0, `Object.keys(err)` is
337/// `["generatedMessage","code","actual","expected","operator","diff"]` — in that
338/// order — while `name`, `message` and `stack` are own but NOT enumerable.
339fn assertion_error_object(
340    msg: &str,
341    generated: bool,
342    operator: Option<&str>,
343    actual: Value,
344    expected: Value,
345) -> Value {
346    let stack = format!("AssertionError [ERR_ASSERTION]: {msg}\n    at <anonymous>");
347    let op_val = match operator {
348        Some(o) => with_host(|h| h.new_str(o)),
349        None => Value::Undef,
350    };
351    let name_v = with_host(|h| h.new_str("AssertionError"));
352    let msg_v = with_host(|h| h.new_str(msg));
353    let code_v = with_host(|h| h.new_str("ERR_ASSERTION"));
354    let stack_v = with_host(|h| h.new_str(stack));
355    let diff_v = with_host(|h| h.new_str(DIFF_MODE));
356    let mut props: indexmap::IndexMap<String, Value> = indexmap::IndexMap::new();
357    // Enumerable, in node's order, first.
358    props.insert("generatedMessage".into(), Value::Bool(generated));
359    props.insert("code".into(), code_v);
360    props.insert("actual".into(), actual);
361    props.insert("expected".into(), expected);
362    props.insert("operator".into(), op_val);
363    props.insert("diff".into(), diff_v);
364    props.insert("name".into(), name_v);
365    props.insert("message".into(), msg_v);
366    props.insert("stack".into(), stack_v);
367    let obj = with_host(|h| h.new_object(props));
368    with_host(|h| {
369        for k in ["name", "message", "stack"] {
370            h.hide_prop(&obj, k);
371        }
372        h.ensure_error_protos();
373        // The `AssertionError` prototype, not `Error`'s: `e.constructor.name`
374        // is what a test runner branches on, and linking straight to `Error`
375        // reported `Error` there while `e.name` still said `AssertionError`.
376        if let Some(p) = crate::host::error_proto_of(h, "AssertionError") {
377            h.set_proto(&obj, p);
378        }
379    });
380    obj
381}
382
383/// Raise a failing assertion as a REAL `AssertionError` object.
384///
385/// The internal `Name [CODE]: message` string is still what propagates (it is
386/// what an uncaught failure prints), but the live thrown VALUE is parked in
387/// `host.exc` so a `catch` receives the object with its full property set rather
388/// than one synthesized from the message alone.
389fn throw_assertion(
390    msg: &str,
391    generated: bool,
392    operator: &str,
393    actual: Value,
394    expected: Value,
395) -> String {
396    let err = assertion_error_object(msg, generated, Some(operator), actual, expected);
397    with_host(|h| h.exc = Some(err));
398    assertion_error(msg)
399}
400
401/// `assert(value[, message])` — throws unless `value` is truthy.
402pub fn assert_ok(args: &[Value]) -> Result<Value, String> {
403    let v = args.first().cloned().unwrap_or(Value::Undef);
404    if with_host(|h| h.truthy(&v)) {
405        return Ok(Value::Undef);
406    }
407    let custom = message(args, 1);
408    let msg = custom.clone().unwrap_or_else(||
409        // Node's heading ends with a colon and is followed by an echo of
410        // the failing source line, which needs the call site's text.
411        "The expression evaluated to a falsy value:".to_string());
412    // `ok` reports the operand as `actual` against a literal `true`, under the
413    // `==` operator (measured on node v26.7.0: `assert.ok(0)` gives
414    // `actual: 0`, `expected: true`, `operator: '=='`).
415    Err(throw_assertion(
416        &msg,
417        custom.is_none(),
418        "==",
419        v,
420        Value::Bool(true),
421    ))
422}
423
424fn check(
425    pass: bool,
426    args: &[Value],
427    msg_idx: usize,
428    op: &str,
429    a: &Value,
430    b: &Value,
431) -> Result<Value, String> {
432    if pass {
433        return Ok(Value::Undef);
434    }
435    let custom = message(args, msg_idx);
436    // `strictEqual`, `deepStrictEqual` and `partialDeepStrictEqual` are Node's
437    // `kMethodsWithCustomMessageDiff`: they render a structural `+ actual -
438    // expected` diff of the two operands, and they keep rendering it when a
439    // custom message is supplied (the custom text replaces the heading only).
440    // Every other comparison writes a fixed sentence.
441    let diff_operator = match op {
442        "===" => Some("strictEqual"),
443        "deepStrictEqual" => Some("deepStrictEqual"),
444        "partialDeepStrictEqual" => Some("partialDeepStrictEqual"),
445        _ => None,
446    };
447    if let Some(diff_op) = diff_operator {
448        let msg = super::assert_diff::create_err_diff(a, b, diff_op, custom.as_deref());
449        let operator = if op == "===" { "strictEqual" } else { op };
450        return Err(throw_assertion(
451            &msg,
452            custom.is_none(),
453            operator,
454            a.clone(),
455            b.clone(),
456        ));
457    }
458    // The remaining messages echo one or both operands, and do so with assert's
459    // OWN inspect settings (expanded and sorted), not `console.log`'s: node
460    // reports `notDeepStrictEqual({a:1},{a:1})` as `{\n  a: 1\n}`, one property
461    // per line, where the default rendering is `{ a: 1 }`.
462    let (sa, sb) = (
463        super::assert_diff::inspect_operand(a),
464        super::assert_diff::inspect_operand(b),
465    );
466    // Each comparison has its OWN generated-message shape in Node; `{a} {op} {b}`
467    // is only right for the two loose forms. `strictEqual(1, 2)` produced
468    // `1 strictEqual 2` here, which is not a sentence any Node emits — the
469    // operator name was being substituted where Node writes a whole heading.
470    let msg = match op {
471        "==" | "!=" => format!("{sa} {op} {sb}"),
472        "===" => format!("Expected values to be strictly equal:\n\n{sa} !== {sb}\n"),
473        "!==" => format!("Expected \"actual\" to be strictly unequal to: {sa}"),
474        "deepEqual" => format!(
475            "Expected values to be loosely deep-equal:\n\n{sa}\n\nshould loosely \
476             deep-equal\n\n{sb}"
477        ),
478        "notDeepEqual" => {
479            format!("Expected \"actual\" not to be loosely deep-equal to:\n\n{sa}")
480        }
481        // The `+ actual - expected` structural DIFF Node renders between two
482        // non-primitive operands is not reproduced (it needs a line-oriented
483        // differ over `util.inspect` output); the primitive form, which is the
484        // whole message when neither side is an object, is exact.
485        "deepStrictEqual" => {
486            format!("Expected values to be strictly deep-equal:\n\n{sa} !== {sb}\n")
487        }
488        "notDeepStrictEqual" => {
489            format!("Expected \"actual\" not to be strictly deep-equal to:\n\n{sa}\n")
490        }
491        _ => format!("{sa} {op} {sb}"),
492    };
493    // Node names the METHOD for the strict/deep forms and the OPERATOR for the
494    // two loose ones: `strictEqual` reports `operator: 'strictEqual'` while
495    // `equal` reports `'=='`. A custom message replaces the generated text but
496    // keeps every other field, and flips `generatedMessage` to false.
497    let operator = match op {
498        "===" => "strictEqual",
499        "!==" => "notStrictEqual",
500        other => other,
501    };
502    Err(throw_assertion(
503        &custom.clone().unwrap_or(msg),
504        custom.is_none(),
505        operator,
506        a.clone(),
507        b.clone(),
508    ))
509}
510
511/// `assert.throws(fn[, error][, message])` / `assert.doesNotThrow(...)`: a port
512/// of node's `expectsError`/`expectedException` and `expectsNoError`
513/// (`lib/assert.js`). The `error` argument was ignored entirely, so
514/// `assert.throws(fn, RangeError)` passed whatever `fn` threw.
515fn throws(args: &[Value], want_throw: bool) -> Result<Value, String> {
516    let f = args.first().cloned().unwrap_or(Value::Undef);
517    if !with_host(|h| is_callable(h, &f)) {
518        return Err(crate::host::coded_error(
519            "TypeError",
520            "ERR_INVALID_ARG_TYPE",
521            &format!(
522                "The \"fn\" argument must be of type function. Received {}",
523                crate::stdlib::received_desc(&f)
524            ),
525        ));
526    }
527    // The thrown value becomes `err.actual`, so it has to be captured rather
528    // than discarded with `.is_err()`.
529    let caught = match invoke(&f, Vec::new(), None) {
530        Ok(_) => None,
531        Err(e) => Some(crate::host::take_exc_or_error(&e)),
532    };
533    let rest = args.get(1..).unwrap_or(&[]);
534    if want_throw {
535        expects_error("throws", "exception", caught, rest)
536    } else {
537        expects_no_error("doesNotThrow", "exception", caught, rest)
538    }
539    .map(|_| Value::Undef)
540}
541
542/// The `error` argument's validation (`expectsError`). `actual` is `None`
543/// when nothing was thrown (or rejected).
544fn expects_error(
545    operator: &str,
546    kind: &str,
547    actual: Option<Value>,
548    rest: &[Value],
549) -> Result<(), String> {
550    let mut error = rest.first().cloned().unwrap_or(Value::Undef);
551    let mut msg = message(rest, 1);
552    let type_of = |v: &Value| with_host(|h| h.type_of(v));
553    if type_of(&error) == "string" {
554        if rest.len() >= 2 {
555            return Err(invalid_error_arg(&error));
556        }
557        let text = with_host(|h| h.str_of(&error));
558        if let Some(a) = &actual {
559            if is_object(a) {
560                let m = crate::builtins::get_property(a, "message")?;
561                if with_host(|h| h.type_of(&m) == "string" && h.str_of(&m) == text) {
562                    return Err(ambiguous(&format!(
563                        "The error message \"{text}\" is identical to the message."
564                    )));
565                }
566            } else if with_host(|h| h.type_of(a) == "string" && h.str_of(a) == text) {
567                return Err(ambiguous(&format!(
568                    "The error \"{text}\" is identical to the message."
569                )));
570            }
571        }
572        msg = Some(text);
573        error = Value::Undef;
574    } else if !with_host(|h| h.is_nullish(&error))
575        && !matches!(type_of(&error), "object" | "function")
576    {
577        return Err(invalid_error_arg(&error));
578    }
579    let Some(actual) = actual else {
580        let name = if with_host(|h| h.is_nullish(&error)) {
581            None
582        } else {
583            let n = crate::builtins::get_property(&error, "name")?;
584            with_host(|h| h.truthy(&n).then(|| h.str_of(&n)))
585        };
586        let mut details = name.map(|n| format!(" ({n})")).unwrap_or_default();
587        details.push_str(&msg.map(|m| format!(": {m}")).unwrap_or_else(|| ".".into()));
588        return Err(throw_assertion(
589            &format!("Missing expected {kind}{details}"),
590            false,
591            operator,
592            Value::Undef,
593            error,
594        ));
595    };
596    if !with_host(|h| h.truthy(&error)) {
597        return Ok(());
598    }
599    expected_exception(operator, actual, error, msg)
600}
601
602fn invalid_error_arg(error: &Value) -> String {
603    crate::host::coded_error(
604        "TypeError",
605        "ERR_INVALID_ARG_TYPE",
606        &format!(
607            "The \"error\" argument must be of type function or an instance of Error, RegExp, or Object. Received {}",
608            crate::stdlib::received_desc(error)
609        ),
610    )
611}
612
613fn ambiguous(detail: &str) -> String {
614    crate::host::coded_error(
615        "TypeError",
616        "ERR_AMBIGUOUS_ARGUMENT",
617        &format!("The \"error/message\" argument is ambiguous. {detail}"),
618    )
619}
620
621fn is_object(v: &Value) -> bool {
622    with_host(|h| h.type_of(v) == "object" && !h.is_null(v))
623}
624
625/// Whether `v` is an Error (node's `isError`: its brand is `Error`).
626fn is_error(v: &Value) -> bool {
627    is_object(v) && with_host(|h| crate::builtins::object_tag(h, v) == "[object Error]")
628}
629
630/// `expectedException`: check what was thrown against a RegExp, a validation
631/// object, an Error class or a validation function.
632fn expected_exception(
633    operator: &str,
634    actual: Value,
635    expected: Value,
636    msg: Option<String>,
637) -> Result<(), String> {
638    let generated = msg.is_none();
639    let fail = |text: String| -> Result<(), String> {
640        Err(throw_assertion(
641            &text,
642            generated,
643            operator,
644            actual.clone(),
645            expected.clone(),
646        ))
647    };
648    if with_host(|h| h.type_of(&expected)) != "function" {
649        if is_regexp(&expected) {
650            let s = crate::host::to_string_value(&actual)?;
651            if regexp_matches(&expected, &s)? {
652                return Ok(());
653            }
654            return fail(msg.unwrap_or_else(|| {
655                with_host(|h| {
656                    format!(
657                        "The input did not match the regular expression {}. Input:\n\n{}\n",
658                        h.inspect(&expected),
659                        h.inspect(&s)
660                    )
661                })
662            }));
663        }
664        if !is_object(&actual) {
665            let text = msg.unwrap_or_else(|| {
666                super::assert_diff::create_err_diff(&actual, &expected, "deepStrictEqual", None)
667            });
668            return fail(text);
669        }
670        // A validation object: every key must be deep-strict-equal, or a
671        // RegExp matching a string property. An Error compares `name` and
672        // `message` as well.
673        let keys_v = crate::builtins::call_builtin_function("Object.keys", vec![expected.clone()])?;
674        let mut keys: Vec<Value> = with_host(|h| match h.get(&keys_v) {
675            Some(JsObj::Array(items)) => items.clone(),
676            _ => Vec::new(),
677        });
678        if is_error(&expected) {
679            keys.extend(with_host(|h| [h.new_str("name"), h.new_str("message")]));
680        } else if keys.is_empty() {
681            let shown = with_host(|h| h.inspect(&expected));
682            return Err(crate::host::coded_error(
683                "TypeError",
684                "ERR_INVALID_ARG_VALUE",
685                &format!("The argument 'error' may not be an empty object. Received {shown}"),
686            ));
687        }
688        for key in &keys {
689            let k = with_host(|h| h.str_of(key));
690            let a = crate::builtins::get_property(&actual, &k)?;
691            let e = crate::builtins::get_property(&expected, &k)?;
692            if with_host(|h| h.type_of(&a)) == "string" && is_regexp(&e) && regexp_matches(&e, &a)?
693            {
694                continue;
695            }
696            let present = crate::builtins::has_property(&actual, &k)?;
697            if present && deep_equal(&a, &e, true) {
698                continue;
699            }
700            let text = match &msg {
701                Some(m) => m.clone(),
702                None => {
703                    let keys_arr = with_host(|h| h.new_array(keys.clone()));
704                    let ca = comparison(&actual, &keys_arr, &Value::Undef)?;
705                    let cb = comparison(&expected, &keys_arr, &actual)?;
706                    super::assert_diff::create_err_diff(&ca, &cb, "deepStrictEqual", None)
707                }
708            };
709            return fail(text);
710        }
711        return Ok(());
712    }
713    // An Error class: the thrown value must be an instance of it.
714    let proto = crate::builtins::get_property(&expected, "prototype")?;
715    if !matches!(proto, Value::Undef) && crate::host::instance_of(&actual, &expected)? {
716        return Ok(());
717    }
718    if is_error_subclass(&expected) {
719        let text = match msg {
720            Some(m) => m,
721            None => {
722                let name = with_host(|h| h.callable_name(&expected));
723                let mut m =
724                    format!("The error is expected to be an instance of \"{name}\". Received ");
725                if is_error(&actual) {
726                    let ctor = crate::builtins::get_property(&actual, "constructor")?;
727                    let ctor_name = if with_host(|h| h.truthy(&ctor)) {
728                        crate::builtins::get_property(&ctor, "name")?
729                    } else {
730                        Value::Undef
731                    };
732                    let actual_name = if with_host(|h| h.truthy(&ctor_name)) {
733                        with_host(|h| h.str_of(&ctor_name))
734                    } else {
735                        let n = crate::builtins::get_property(&actual, "name")?;
736                        with_host(|h| h.str_of(&n))
737                    };
738                    if actual_name == name {
739                        m.push_str("an error with identical name but a different prototype.");
740                    } else {
741                        m.push_str(&format!("\"{actual_name}\""));
742                    }
743                    let am = crate::builtins::get_property(&actual, "message")?;
744                    if with_host(|h| h.truthy(&am)) {
745                        m.push_str(&format!(
746                            "\n\nError message:\n\n{}",
747                            with_host(|h| h.str_of(&am))
748                        ));
749                    }
750                } else {
751                    m.push_str(&format!("\"{}\"", with_host(|h| h.inspect(&actual))));
752                }
753                m
754            }
755        };
756        return fail(text);
757    }
758    // A validation function, called with a fresh `this`, must return `true`.
759    let this = with_host(|h| h.new_object(indexmap::IndexMap::new()));
760    let res = invoke(&expected, vec![actual.clone()], Some(this))?;
761    if matches!(res, Value::Bool(true)) {
762        return Ok(());
763    }
764    let text = match msg {
765        Some(m) => m,
766        None => {
767            let name = with_host(|h| h.callable_name(&expected));
768            let named = if name.is_empty() {
769                String::new()
770            } else {
771                format!("\"{name}\" ")
772            };
773            let mut m = format!(
774                "The {named}validation function is expected to return \"true\". Received {}",
775                with_host(|h| h.inspect(&res))
776            );
777            if is_error(&actual) {
778                let s = crate::host::to_string_value(&actual)?;
779                m.push_str(&format!(
780                    "\n\nCaught error:\n\n{}",
781                    with_host(|h| h.str_of(&s))
782                ));
783            }
784            m
785        }
786    };
787    fail(text)
788}
789
790/// `expectsNoError`: anything thrown fails, unless an `error` argument is
791/// given that it does NOT match, in which case it is rethrown as is.
792fn expects_no_error(
793    operator: &str,
794    kind: &str,
795    actual: Option<Value>,
796    rest: &[Value],
797) -> Result<(), String> {
798    let Some(actual) = actual else {
799        return Ok(());
800    };
801    let mut error = rest.first().cloned().unwrap_or(Value::Undef);
802    let mut msg = message(rest, 1);
803    if with_host(|h| h.type_of(&error)) == "string" {
804        msg = Some(with_host(|h| h.str_of(&error)));
805        error = Value::Undef;
806    }
807    if !with_host(|h| h.truthy(&error)) || has_matching_error(&actual, &error)? {
808        let details = msg.map(|m| format!(": {m}")).unwrap_or_else(|| ".".into());
809        let actual_message = if is_object(&actual) {
810            let m = crate::builtins::get_property(&actual, "message")?;
811            with_host(|h| h.str_of(&m))
812        } else {
813            "undefined".to_string()
814        };
815        return Err(throw_assertion(
816            &format!("Got unwanted {kind}{details}\nActual message: \"{actual_message}\""),
817            false,
818            operator,
819            actual,
820            error,
821        ));
822    }
823    Err(with_host(|h| {
824        h.exc = Some(actual.clone());
825        crate::builtins::error_string(h, &actual)
826    }))
827}
828
829/// `hasMatchingError`.
830fn has_matching_error(actual: &Value, expected: &Value) -> Result<bool, String> {
831    if with_host(|h| h.type_of(expected)) != "function" {
832        if is_regexp(expected) {
833            let s = crate::host::to_string_value(actual)?;
834            return regexp_matches(expected, &s);
835        }
836        return Err(crate::host::coded_error(
837            "TypeError",
838            "ERR_INVALID_ARG_TYPE",
839            &format!(
840                "The \"expected\" argument must be of type function or an instance of RegExp. Received {}",
841                crate::stdlib::received_desc(expected)
842            ),
843        ));
844    }
845    let proto = crate::builtins::get_property(expected, "prototype")?;
846    if !matches!(proto, Value::Undef) && crate::host::instance_of(actual, expected)? {
847        return Ok(true);
848    }
849    if is_error_subclass(expected) {
850        return Ok(false);
851    }
852    let this = with_host(|h| h.new_object(indexmap::IndexMap::new()));
853    let res = invoke(expected, vec![actual.clone()], Some(this))?;
854    Ok(matches!(res, Value::Bool(true)))
855}
856
857/// `ObjectPrototypeIsPrototypeOf(Error, expected)`: whether `Error` is on
858/// `expected`'s prototype chain — a subclass of it, not `Error` itself.
859fn is_error_subclass(expected: &Value) -> bool {
860    let mut cur = crate::builtins::prototype_of(expected);
861    for _ in 0..100_000 {
862        if with_host(|h| h.is_null(&cur) || matches!(cur, Value::Undef)) {
863            return false;
864        }
865        if with_host(|h| matches!(h.get(&cur), Some(JsObj::Builtin(n)) if n == "Error")) {
866            return true;
867        }
868        cur = crate::builtins::prototype_of(&cur);
869    }
870    false
871}
872
873/// Node's `Comparison`: an object of the compared keys, so the diff an
874/// `assert.throws` validation object fails with names only those keys, under
875/// the class name `Comparison`. Defined once, in JavaScript, since it IS a
876/// class node defines in JavaScript.
877fn comparison(obj: &Value, keys: &Value, actual: &Value) -> Result<Value, String> {
878    thread_local! {
879        static FACTORY: std::cell::RefCell<Option<Value>> = const { std::cell::RefCell::new(None) };
880    }
881    let factory = match FACTORY.with(|f| f.borrow().clone()) {
882        Some(f) => f,
883        None => {
884            let f = crate::eval_in_global_scope(
885                "(() => { class Comparison { constructor(obj, keys, actual) { for (const key of keys) { if (key in obj) { if (actual !== undefined && typeof actual[key] === 'string' && Object.prototype.toString.call(obj[key]) === '[object RegExp]' && obj[key].exec(actual[key]) !== null) { this[key] = actual[key]; } else { this[key] = obj[key]; } } } } } return (obj, keys, actual) => new Comparison(obj, keys, actual); })()",
886            )?;
887            FACTORY.with(|c| *c.borrow_mut() = Some(f.clone()));
888            f
889        }
890    };
891    invoke(
892        &factory,
893        vec![obj.clone(), keys.clone(), actual.clone()],
894        None,
895    )
896}
897
898fn message(args: &[Value], idx: usize) -> Option<String> {
899    match args.get(idx) {
900        Some(Value::Undef) | None => None,
901        Some(v) => Some(with_host(|h| h.str_of(v))),
902    }
903}
904
905/// An `AssertionError` as an internal error string.
906///
907/// `Name [CODE]: message` is the shared encoding `synth_error` parses back into
908/// `.name`/`.code`/`.message`, so the prefix must be built by the one
909/// constructor rather than written out here — spelling it inline is what let
910/// this site keep a form the parser did not recognize.
911fn assertion_error(msg: &str) -> String {
912    crate::host::coded_error("AssertionError", "ERR_ASSERTION", msg)
913}
914
915/// `assert.strictEqual` compares with `Object.is`, not `===`.
916///
917/// That is the whole difference for two values: `NaN` equals itself, and `+0`
918/// does not equal `-0`. Using `===` had both backwards — `strictEqual(NaN, NaN)`
919/// failed and `strictEqual(0, -0)` passed.
920fn strict(a: &Value, b: &Value) -> bool {
921    crate::builtins::same_value(a, b)
922}
923
924fn loose_eq(a: &Value, b: &Value) -> bool {
925    if strict(a, b) {
926        return true;
927    }
928    with_host(|h| {
929        let (na, nb) = (h.to_number(a), h.to_number(b));
930        if !na.is_nan() && !nb.is_nan() && (na == nb) {
931            return true;
932        }
933        h.str_of(a) == h.str_of(b)
934    })
935}
936
937/// Structural equality. `strict` compares leaves with `===`, otherwise `==`.
938pub fn deep_equal(a: &Value, b: &Value, strict_mode: bool) -> bool {
939    deep_equal_seen(a, b, strict_mode, &mut Vec::new())
940}
941
942/// `deep_equal` carrying the pairs currently being compared.
943///
944/// Without it a self-referential structure recursed until the stack overflowed
945/// and the process aborted — `const x = {}; x.self = x;` compared against
946/// another of the same shape, which is exactly what a test asserting on a
947/// linked structure does. A pair already on the stack is treated as equal: if
948/// anything else about the two differs, some other comparison finds it.
949fn deep_equal_seen(
950    a: &Value,
951    b: &Value,
952    strict_mode: bool,
953    seen: &mut Vec<(Value, Value)>,
954) -> bool {
955    if seen.iter().any(|(x, y)| x == a && y == b) {
956        return true;
957    }
958    // `deepStrictEqual` requires the two to share a [[Prototype]]. That single
959    // check is what separates `Object.create(null)` from `{}`, an instance of
960    // one class from an instance of another, and a `Uint8Array` from an
961    // `Int8Array` — none of which were being distinguished.
962    if strict_mode {
963        let both_objects = with_host(|h| h.get(a).is_some() && h.get(b).is_some());
964        if both_objects
965            && with_host(|h| {
966                // A null-prototype object is tracked separately rather than by
967                // `proto_of` returning None — which a plain object does too, its
968                // `Object.prototype` being implicit. Comparing only `proto_of`
969                // therefore called `Object.create(null)` and `{}` alike.
970                h.proto_of(a) != h.proto_of(b) || h.has_null_proto(a) != h.has_null_proto(b)
971            })
972        {
973            return false;
974        }
975    }
976    let kinds = with_host(|h| {
977        let av = h.get(a).map(kind);
978        let bv = h.get(b).map(kind);
979        (av, bv)
980    });
981    seen.push((a.clone(), b.clone()));
982    let result = deep_equal_body(a, b, strict_mode, seen, kinds);
983    seen.pop();
984    result
985}
986
987fn deep_equal_body(
988    a: &Value,
989    b: &Value,
990    strict_mode: bool,
991    seen: &mut Vec<(Value, Value)>,
992    kinds: (Option<Kind>, Option<Kind>),
993) -> bool {
994    match kinds {
995        (Some(Kind::Array), Some(Kind::Array)) => {
996            let (ia, ib) = with_host(|h| (array_of(h, a), array_of(h, b)));
997            ia.len() == ib.len()
998                && ia
999                    .iter()
1000                    .zip(ib.iter())
1001                    .all(|(x, y)| deep_equal_seen(x, y, strict_mode, seen))
1002        }
1003        (Some(Kind::Object), Some(Kind::Object)) => {
1004            let (ea, eb) = with_host(|h| (object_of(h, a), object_of(h, b)));
1005            if ea.len() != eb.len() {
1006                return false;
1007            }
1008            let props_match = ea.iter().all(|(k, va)| {
1009                eb.iter()
1010                    .find(|(k2, _)| k2 == k)
1011                    .is_some_and(|(_, vb)| deep_equal_seen(va, vb, strict_mode, seen))
1012            });
1013            if !props_match {
1014                return false;
1015            }
1016            // The brands whose whole state lives in INTERNAL slots — a Date's
1017            // `@@ms`, a typed array's `@@buffer`/`byteOffset`, an Error's name
1018            // and message — are objects with no enumerable own properties at
1019            // all. Comparing only the public ones therefore reported every pair
1020            // of them as deep-equal: `deepStrictEqual(new Date(0), new Date(1))`
1021            // PASSED, as did two Buffers with different bytes. Slots are
1022            // compared here rather than folded into `object_of` because they are
1023            // not properties — they must not affect the key COUNT above, which
1024            // node takes over enumerable keys only.
1025            let (ia, ib) = with_host(|h| (internals_of(h, a), internals_of(h, b)));
1026            if ia.len() != ib.len() {
1027                return false;
1028            }
1029            ia.iter().all(|(k, va)| {
1030                ib.iter()
1031                    .find(|(k2, _)| k2 == k)
1032                    .is_some_and(|(_, vb)| deep_equal_seen(va, vb, strict_mode, seen))
1033            })
1034        }
1035        // A Map compares by ENTRIES and a Set by MEMBERS, both order-insensitively
1036        // (`new Set([1, 2])` deep-equals `new Set([2, 1])`), so each entry on the
1037        // left is matched against an as-yet-unclaimed entry on the right rather
1038        // than against the one at its own index.
1039        (Some(Kind::Map), Some(Kind::Map)) => {
1040            let (ea, eb) = with_host(|h| (map_entries_of(h, a), map_entries_of(h, b)));
1041            unordered_match(&ea, &eb, seen, |(ka, va), (kb, vb), seen| {
1042                deep_equal_seen(ka, kb, strict_mode, seen)
1043                    && deep_equal_seen(va, vb, strict_mode, seen)
1044            })
1045        }
1046        (Some(Kind::Set), Some(Kind::Set)) => {
1047            let (ea, eb) = with_host(|h| (set_members_of(h, a), set_members_of(h, b)));
1048            unordered_match(&ea, &eb, seen, |x, y, seen| {
1049                deep_equal_seen(x, y, strict_mode, seen)
1050            })
1051        }
1052        // Two distinct RegExp objects are deep-equal when their pattern and flags
1053        // are; `same_value` would call every one of them unequal.
1054        (Some(Kind::RegExp), Some(Kind::RegExp)) => {
1055            with_host(|h| regexp_key(h, a) == regexp_key(h, b))
1056        }
1057        // Everything else — strings, symbols, bigints, functions, and any two
1058        // values of DIFFERENT kinds — is compared as a leaf. This arm used to be
1059        // unreachable for heap values because `kind` called them all `Object`,
1060        // and `object_of` then reported each as having zero properties, so any
1061        // two of them matched: `deepStrictEqual('abc', 'abd')` passed silently.
1062        _ => {
1063            if strict_mode {
1064                strict(a, b)
1065            } else {
1066                loose_eq(a, b)
1067            }
1068        }
1069    }
1070}
1071
1072/// Whether every element of `ea` can be paired off with a DISTINCT element of
1073/// `eb` under `eq`. Greedy matching is enough here because the relation is an
1074/// equivalence: anything that matches a claimed element would have matched
1075/// whatever claimed it.
1076fn unordered_match<T>(
1077    ea: &[T],
1078    eb: &[T],
1079    seen: &mut Vec<(Value, Value)>,
1080    eq: impl Fn(&T, &T, &mut Vec<(Value, Value)>) -> bool,
1081) -> bool {
1082    if ea.len() != eb.len() {
1083        return false;
1084    }
1085    let mut claimed = vec![false; eb.len()];
1086    'outer: for x in ea {
1087        for (i, y) in eb.iter().enumerate() {
1088            if !claimed[i] && eq(x, y, seen) {
1089                claimed[i] = true;
1090                continue 'outer;
1091            }
1092        }
1093        return false;
1094    }
1095    true
1096}
1097
1098enum Kind {
1099    Array,
1100    Object,
1101    Map,
1102    Set,
1103    RegExp,
1104    /// A leaf: compared with `===`, never structurally.
1105    Other,
1106}
1107fn kind(o: &JsObj) -> Kind {
1108    match o {
1109        JsObj::Array(_) => Kind::Array,
1110        JsObj::Object(_) => Kind::Object,
1111        // A WEAK collection exposes no entries, so there is nothing to compare
1112        // structurally; node treats two of them as equal only by identity.
1113        JsObj::Map { weak: false, .. } => Kind::Map,
1114        JsObj::Set { weak: false, .. } => Kind::Set,
1115        JsObj::RegExp(_) => Kind::RegExp,
1116        _ => Kind::Other,
1117    }
1118}
1119/// A Map's entries as `(key, value)` pairs, in insertion order.
1120fn map_entries_of(h: &crate::host::JsHost, v: &Value) -> Vec<(Value, Value)> {
1121    match h.get(v) {
1122        Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
1123        _ => Vec::new(),
1124    }
1125}
1126/// A Set's members, in insertion order.
1127fn set_members_of(h: &crate::host::JsHost, v: &Value) -> Vec<Value> {
1128    match h.get(v) {
1129        Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
1130        _ => Vec::new(),
1131    }
1132}
1133/// The identity of a regular expression for comparison: its pattern and flags.
1134fn regexp_key(h: &crate::host::JsHost, v: &Value) -> Option<(String, String)> {
1135    match h.get(v) {
1136        Some(JsObj::RegExp(r)) => Some((r.source.clone(), r.flags.clone())),
1137        _ => None,
1138    }
1139}
1140/// An object's INTERNAL slots — the `@@`-prefixed keys that carry brand state
1141/// (`@@ms`, `@@buffer`, `@@kind`) and are deliberately absent from `object_of`.
1142/// Private class fields (`#`-prefixed) stay excluded: node's `deepStrictEqual`
1143/// compares own ENUMERABLE properties, and a private field is neither.
1144fn internals_of(h: &crate::host::JsHost, v: &Value) -> Vec<(String, Value)> {
1145    match h.get(v) {
1146        Some(JsObj::Object(p)) => p
1147            .iter()
1148            .filter(|(k, _)| k.starts_with("@@"))
1149            .map(|(k, v)| (k.clone(), v.clone()))
1150            .collect(),
1151        _ => Vec::new(),
1152    }
1153}
1154fn array_of(h: &crate::host::JsHost, v: &Value) -> Vec<Value> {
1155    match h.get(v) {
1156        Some(JsObj::Array(items)) => items.clone(),
1157        _ => Vec::new(),
1158    }
1159}
1160fn object_of(h: &crate::host::JsHost, v: &Value) -> Vec<(String, Value)> {
1161    match h.get(v) {
1162        Some(JsObj::Object(p)) => p
1163            .iter()
1164            .filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
1165            .map(|(k, v)| (k.clone(), v.clone()))
1166            .collect(),
1167        _ => Vec::new(),
1168    }
1169}
1170
1171/// Node's `checkIsPromise`: a native promise, or an object whose `then` and
1172/// `catch` are both functions.
1173fn is_promise_like(v: &Value) -> bool {
1174    if with_host(|h| h.promise_id(v)).is_some() {
1175        return true;
1176    }
1177    if !is_object(v) {
1178        return false;
1179    }
1180    ["then", "catch"].iter().all(|k| {
1181        crate::builtins::get_property(v, k)
1182            .map(|f| with_host(|h| is_callable(h, &f)))
1183            .unwrap_or(false)
1184    })
1185}