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