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, 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` 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(&a(), true)),
88        "doesNotReject" => Ok(rejects_impl(&a(), 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 !with_host(|h| matches!(h.get(&re), Some(JsObj::RegExp(_)))) {
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    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
139/// `assert.ifError(value)` — throws unless `value` is `null`/`undefined`.
140fn 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
156/// `assert.partialDeepStrictEqual(actual, expected)` — passes when every leaf of
157/// `expected` strict-deep-matches the corresponding part of `actual` (extra
158/// props/elements in `actual` are ignored).
159fn 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
197/// `assert.rejects(fn|promise)` / `assert.doesNotReject(...)` — returns a Promise
198/// that fulfills when the operand settles the expected way, else rejects with an
199/// `AssertionError`.
200fn 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    // Reduce the operand to a promise: call it if it is a function.
204    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        // Not thenable: treat as an immediate non-rejection.
220        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
233/// `new assert.AssertionError(options)` — a real `Error`-prototype-linked object
234/// carrying `name`/`message`/`code`/`actual`/`expected`/`operator`. Parent wires
235/// this to `construct("AssertionError")` and `constant("assert","AssertionError")`.
236pub 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
267/// Node's `diff` field. Every failure form measured on node v26.7.0 — `ok`,
268/// `equal`, `strictEqual`, `notStrictEqual`, `deepEqual`, `deepStrictEqual`,
269/// `match`, `throws`, `fail` — reports the same `"simple"`; it names the diff
270/// MODE the error was built under, not a rendered diff.
271const DIFF_MODE: &str = "simple";
272
273/// Build the `AssertionError` object a failing assertion throws.
274///
275/// The own-property set is what a test runner reads, and node-js carried only
276/// `code`/`message`/`stack`: `err.actual`, `err.expected`, `err.operator` and
277/// `err.generatedMessage` were all `undefined`, so every framework that reports
278/// "expected X, got Y" from a caught `AssertionError` had nothing to report.
279/// Measured on node v26.7.0, `Object.keys(err)` is
280/// `["generatedMessage","code","actual","expected","operator","diff"]` — in that
281/// order — while `name`, `message` and `stack` are own but NOT enumerable.
282fn 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    // Enumerable, in node's order, first.
301    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        // The `AssertionError` prototype, not `Error`'s: `e.constructor.name`
317        // is what a test runner branches on, and linking straight to `Error`
318        // reported `Error` there while `e.name` still said `AssertionError`.
319        if let Some(p) = crate::host::error_proto_of(h, "AssertionError") {
320            h.set_proto(&obj, p);
321        }
322    });
323    obj
324}
325
326/// Raise a failing assertion as a REAL `AssertionError` object.
327///
328/// The internal `Name [CODE]: message` string is still what propagates (it is
329/// what an uncaught failure prints), but the live thrown VALUE is parked in
330/// `host.exc` so a `catch` receives the object with its full property set rather
331/// than one synthesized from the message alone.
332fn 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
358/// `assert(value[, message])` — throws unless `value` is truthy.
359pub 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        // Node's heading ends with a colon and is followed by an echo of
367        // the failing source line, which needs the call site's text.
368        "The expression evaluated to a falsy value:".to_string());
369    // `ok` reports the operand as `actual` against a literal `true`, under the
370    // `==` operator (measured on node v26.7.0: `assert.ok(0)` gives
371    // `actual: 0`, `expected: true`, `operator: '=='`).
372    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    // Each comparison has its OWN generated-message shape in Node; `{a} {op} {b}`
395    // is only right for the two loose forms. `strictEqual(1, 2)` produced
396    // `1 strictEqual 2` here, which is not a sentence any Node emits — the
397    // operator name was being substituted where Node writes a whole heading.
398    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        // The `+ actual - expected` structural DIFF Node renders between two
410        // non-primitive operands is not reproduced (it needs a line-oriented
411        // differ over `util.inspect` output); the primitive form, which is the
412        // whole message when neither side is an object, is exact.
413        "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    // Node names the METHOD for the strict/deep forms and the OPERATOR for the
422    // two loose ones: `strictEqual` reports `operator: 'strictEqual'` while
423    // `equal` reports `'=='`. A custom message replaces the generated text but
424    // keeps every other field, and flips `generatedMessage` to false.
425    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    // The thrown value becomes `err.actual` on a `doesNotThrow` failure, so it
442    // has to be captured rather than discarded with `.is_err()`.
443    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        // `generatedMessage` is FALSE for both, which is what node reports even
451        // though it wrote the sentence itself (v26.7.0, `assert.throws(()=>{})`).
452        (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
476/// An `AssertionError` as an internal error string.
477///
478/// `Name [CODE]: message` is the shared encoding `synth_error` parses back into
479/// `.name`/`.code`/`.message`, so the prefix must be built by the one
480/// constructor rather than written out here — spelling it inline is what let
481/// this site keep a form the parser did not recognize.
482fn 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
503/// Structural equality. `strict` compares leaves with `===`, otherwise `==`.
504pub 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}