use crate::host::{
call_method, invoke, is_callable, promise_of, reject_promise_val, resolve_promise_val,
subscribe_native, take_exc_or_error, with_host, JsObj, PromiseState,
};
use fusevm::Value;
pub const METHODS: &[&str] = &[
"ok",
"equal",
"notEqual",
"strictEqual",
"notStrictEqual",
"deepEqual",
"notDeepEqual",
"deepStrictEqual",
"notDeepStrictEqual",
"throws",
"doesNotThrow",
"fail",
"match",
"doesNotMatch",
"ifError",
"partialDeepStrictEqual",
"rejects",
"doesNotReject",
];
pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
let a = || args.first().cloned().unwrap_or(Value::Undef);
let b = || args.get(1).cloned().unwrap_or(Value::Undef);
Some(match method {
"ok" => assert_ok(args),
"equal" => check(loose_eq(&a(), &b()), args, 2, "==", &a(), &b()),
"notEqual" => check(!loose_eq(&a(), &b()), args, 2, "!=", &a(), &b()),
"strictEqual" => check(strict(&a(), &b()), args, 2, "===", &a(), &b()),
"notStrictEqual" => check(!strict(&a(), &b()), args, 2, "!==", &a(), &b()),
"deepEqual" => check(
deep_equal(&a(), &b(), false),
args,
2,
"deepEqual",
&a(),
&b(),
),
"notDeepEqual" => check(
!deep_equal(&a(), &b(), false),
args,
2,
"notDeepEqual",
&a(),
&b(),
),
"deepStrictEqual" => check(
deep_equal(&a(), &b(), true),
args,
2,
"deepStrictEqual",
&a(),
&b(),
),
"notDeepStrictEqual" => check(
!deep_equal(&a(), &b(), true),
args,
2,
"notDeepStrictEqual",
&a(),
&b(),
),
"throws" => throws(args, true),
"doesNotThrow" => throws(args, false),
"fail" => Err(throw_assertion(
&message(args, 0).unwrap_or_else(|| "Failed".to_string()),
message(args, 0).is_none(),
"fail",
Value::Undef,
Value::Undef,
)),
"match" => assert_match(args, true),
"doesNotMatch" => assert_match(args, false),
"ifError" => if_error(&a()),
"partialDeepStrictEqual" => partial(&a(), &b(), args),
"rejects" => Ok(rejects_impl(&a(), true)),
"doesNotReject" => Ok(rejects_impl(&a(), false)),
_ => return None,
})
}
pub fn strict_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
let mapped = match method {
"equal" => "strictEqual",
"notEqual" => "notStrictEqual",
"deepEqual" => "deepStrictEqual",
"notDeepEqual" => "notDeepStrictEqual",
other => other,
};
call(mapped, args)
}
fn assert_match(args: &[Value], want_match: bool) -> Result<Value, String> {
let s = args.first().cloned().unwrap_or(Value::Undef);
let re = args.get(1).cloned().unwrap_or(Value::Undef);
if !with_host(|h| matches!(h.get(&re), Some(JsObj::RegExp(_)))) {
return Err(crate::host::coded_error(
"TypeError",
"ERR_INVALID_ARG_TYPE",
&format!(
"The \"regexp\" argument must be an instance of RegExp. Received {}",
crate::stdlib::received_desc(&re)
),
));
}
let matched = call_method(&re, "test", vec![s.clone()])?;
let matched = with_host(|h| h.truthy(&matched));
if matched == want_match {
return Ok(Value::Undef);
}
if let Some(m) = message(args, 2) {
return Err(assertion_error(&m));
}
let (sre, sstr) = with_host(|h| (h.inspect(&re), h.str_of(&s)));
let verb = if want_match {
"The input did not match the regular expression"
} else {
"The input was expected to not match the regular expression"
};
Err(assertion_error(&format!("{verb} {sre}. Input: '{sstr}'")))
}
fn if_error(v: &Value) -> Result<Value, String> {
if with_host(|h| h.is_nullish(v)) {
return Ok(Value::Undef);
}
let desc = with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => p
.get("message")
.map(|m| h.str_of(m))
.unwrap_or_else(|| h.inspect(v)),
_ => h.inspect(v),
});
Err(assertion_error(&format!(
"ifError got unwanted exception: {desc}"
)))
}
fn partial(actual: &Value, expected: &Value, args: &[Value]) -> Result<Value, String> {
if partial_deep(actual, expected) {
return Ok(Value::Undef);
}
if let Some(m) = message(args, 2) {
return Err(assertion_error(&m));
}
let (sa, sb) = with_host(|h| (h.inspect(actual), h.inspect(expected)));
Err(assertion_error(&format!(
"Expected values to be strictly deep-equal (partial):\n{sb} should be a subset of {sa}"
)))
}
fn partial_deep(actual: &Value, expected: &Value) -> bool {
let ekind = with_host(|h| h.get(expected).map(kind));
match ekind {
Some(Kind::Object) => {
if !matches!(with_host(|h| h.get(actual).map(kind)), Some(Kind::Object)) {
return false;
}
let (ea, ee) = with_host(|h| (object_of(h, actual), object_of(h, expected)));
ee.iter().all(|(k, ve)| {
ea.iter()
.find(|(k2, _)| k2 == k)
.is_some_and(|(_, va)| partial_deep(va, ve))
})
}
Some(Kind::Array) => {
if !matches!(with_host(|h| h.get(actual).map(kind)), Some(Kind::Array)) {
return false;
}
let (ia, ie) = with_host(|h| (array_of(h, actual), array_of(h, expected)));
ie.len() <= ia.len() && ie.iter().zip(ia.iter()).all(|(e, a)| partial_deep(a, e))
}
_ => strict(actual, expected),
}
}
fn rejects_impl(input: &Value, want_reject: bool) -> Value {
let result = with_host(|h| h.new_promise());
let rid = with_host(|h| h.promise_id(&result).unwrap());
let operand = if with_host(|h| is_callable(h, input)) {
match invoke(input, Vec::new(), None) {
Ok(v) => promise_of(&v),
Err(e) => {
let ev = take_exc_or_error(&e);
let p = with_host(|h| h.new_promise());
let pid = with_host(|h| h.promise_id(&p).unwrap());
reject_promise_val(pid, ev);
p
}
}
} else {
promise_of(input)
};
let Some(oid) = with_host(|h| h.promise_id(&operand)) else {
settle_rejects(rid, false, want_reject);
return result;
};
subscribe_native(
oid,
Box::new(move |state, _val| {
settle_rejects(rid, state == PromiseState::Rejected, want_reject);
Ok(())
}),
);
result
}
pub fn construct_assertion_error(args: &[Value]) -> Value {
let opts = args.first().cloned().unwrap_or(Value::Undef);
let (message, actual, expected, operator) = with_host(|h| match h.get(&opts) {
Some(JsObj::Object(p)) => (
p.get("message").map(|v| h.str_of(v)),
p.get("actual").cloned(),
p.get("expected").cloned(),
p.get("operator").map(|v| h.str_of(v)),
),
_ => (None, None, None, None),
});
let generated = message.is_none();
let msg = message.unwrap_or_else(|| {
let (sa, se) = with_host(|h| {
(
actual.as_ref().map(|v| h.inspect(v)).unwrap_or_default(),
expected.as_ref().map(|v| h.inspect(v)).unwrap_or_default(),
)
});
let op = operator.clone().unwrap_or_else(|| "==".to_string());
format!("{sa} {op} {se}")
});
assertion_error_object(
&msg,
generated,
operator.as_deref(),
actual.unwrap_or(Value::Undef),
expected.unwrap_or(Value::Undef),
)
}
const DIFF_MODE: &str = "simple";
fn assertion_error_object(
msg: &str,
generated: bool,
operator: Option<&str>,
actual: Value,
expected: Value,
) -> Value {
let stack = format!("AssertionError [ERR_ASSERTION]: {msg}\n at <anonymous>");
let op_val = match operator {
Some(o) => with_host(|h| h.new_str(o)),
None => Value::Undef,
};
let name_v = with_host(|h| h.new_str("AssertionError"));
let msg_v = with_host(|h| h.new_str(msg));
let code_v = with_host(|h| h.new_str("ERR_ASSERTION"));
let stack_v = with_host(|h| h.new_str(stack));
let diff_v = with_host(|h| h.new_str(DIFF_MODE));
let mut props: indexmap::IndexMap<String, Value> = indexmap::IndexMap::new();
props.insert("generatedMessage".into(), Value::Bool(generated));
props.insert("code".into(), code_v);
props.insert("actual".into(), actual);
props.insert("expected".into(), expected);
props.insert("operator".into(), op_val);
props.insert("diff".into(), diff_v);
props.insert("name".into(), name_v);
props.insert("message".into(), msg_v);
props.insert("stack".into(), stack_v);
let obj = with_host(|h| h.new_object(props));
with_host(|h| {
for k in ["name", "message", "stack"] {
h.hide_prop(&obj, k);
}
h.ensure_error_protos();
if let Some(p) = crate::host::error_proto_of(h, "AssertionError") {
h.set_proto(&obj, p);
}
});
obj
}
fn throw_assertion(
msg: &str,
generated: bool,
operator: &str,
actual: Value,
expected: Value,
) -> String {
let err = assertion_error_object(msg, generated, Some(operator), actual, expected);
with_host(|h| h.exc = Some(err));
assertion_error(msg)
}
fn settle_rejects(rid: u32, rejected: bool, want_reject: bool) {
if rejected == want_reject {
resolve_promise_val(rid, Value::Undef);
} else {
let msg = if want_reject {
"AssertionError [ERR_ASSERTION]: Missing expected rejection."
} else {
"AssertionError [ERR_ASSERTION]: Got unwanted rejection."
};
let ev = with_host(|h| crate::builtins::synth_error(h, msg));
reject_promise_val(rid, ev);
}
}
pub fn assert_ok(args: &[Value]) -> Result<Value, String> {
let v = args.first().cloned().unwrap_or(Value::Undef);
if with_host(|h| h.truthy(&v)) {
return Ok(Value::Undef);
}
let custom = message(args, 1);
let msg = custom.clone().unwrap_or_else(||
"The expression evaluated to a falsy value:".to_string());
Err(throw_assertion(
&msg,
custom.is_none(),
"==",
v,
Value::Bool(true),
))
}
fn check(
pass: bool,
args: &[Value],
msg_idx: usize,
op: &str,
a: &Value,
b: &Value,
) -> Result<Value, String> {
if pass {
return Ok(Value::Undef);
}
let custom = message(args, msg_idx);
let diff_operator = match op {
"===" => Some("strictEqual"),
"deepStrictEqual" => Some("deepStrictEqual"),
"partialDeepStrictEqual" => Some("partialDeepStrictEqual"),
_ => None,
};
if let Some(diff_op) = diff_operator {
let msg = super::assert_diff::create_err_diff(a, b, diff_op, custom.as_deref());
let operator = if op == "===" { "strictEqual" } else { op };
return Err(throw_assertion(
&msg,
custom.is_none(),
operator,
a.clone(),
b.clone(),
));
}
let (sa, sb) = (
super::assert_diff::inspect_operand(a),
super::assert_diff::inspect_operand(b),
);
let msg = match op {
"==" | "!=" => format!("{sa} {op} {sb}"),
"===" => format!("Expected values to be strictly equal:\n\n{sa} !== {sb}\n"),
"!==" => format!("Expected \"actual\" to be strictly unequal to: {sa}"),
"deepEqual" => format!(
"Expected values to be loosely deep-equal:\n\n{sa}\n\nshould loosely \
deep-equal\n\n{sb}"
),
"notDeepEqual" => {
format!("Expected \"actual\" not to be loosely deep-equal to:\n\n{sa}")
}
"deepStrictEqual" => {
format!("Expected values to be strictly deep-equal:\n\n{sa} !== {sb}\n")
}
"notDeepStrictEqual" => {
format!("Expected \"actual\" not to be strictly deep-equal to:\n\n{sa}\n")
}
_ => format!("{sa} {op} {sb}"),
};
let operator = match op {
"===" => "strictEqual",
"!==" => "notStrictEqual",
other => other,
};
Err(throw_assertion(
&custom.clone().unwrap_or(msg),
custom.is_none(),
operator,
a.clone(),
b.clone(),
))
}
fn throws(args: &[Value], want_throw: bool) -> Result<Value, String> {
let f = args.first().cloned().unwrap_or(Value::Undef);
let caught = match invoke(&f, Vec::new(), None) {
Ok(_) => None,
Err(e) => Some(crate::host::take_exc_or_error(&e)),
};
let threw = caught.is_some();
match (threw, want_throw) {
(true, true) | (false, false) => Ok(Value::Undef),
(false, true) => Err(throw_assertion(
"Missing expected exception.",
false,
"throws",
Value::Undef,
Value::Undef,
)),
(true, false) => Err(throw_assertion(
"Got unwanted exception.",
false,
"doesNotThrow",
caught.unwrap_or(Value::Undef),
Value::Undef,
)),
}
}
fn message(args: &[Value], idx: usize) -> Option<String> {
match args.get(idx) {
Some(Value::Undef) | None => None,
Some(v) => Some(with_host(|h| h.str_of(v))),
}
}
fn assertion_error(msg: &str) -> String {
crate::host::coded_error("AssertionError", "ERR_ASSERTION", msg)
}
fn strict(a: &Value, b: &Value) -> bool {
crate::builtins::same_value(a, b)
}
fn loose_eq(a: &Value, b: &Value) -> bool {
if strict(a, b) {
return true;
}
with_host(|h| {
let (na, nb) = (h.to_number(a), h.to_number(b));
if !na.is_nan() && !nb.is_nan() && (na == nb) {
return true;
}
h.str_of(a) == h.str_of(b)
})
}
pub fn deep_equal(a: &Value, b: &Value, strict_mode: bool) -> bool {
deep_equal_seen(a, b, strict_mode, &mut Vec::new())
}
fn deep_equal_seen(
a: &Value,
b: &Value,
strict_mode: bool,
seen: &mut Vec<(Value, Value)>,
) -> bool {
if seen.iter().any(|(x, y)| x == a && y == b) {
return true;
}
if strict_mode {
let both_objects = with_host(|h| h.get(a).is_some() && h.get(b).is_some());
if both_objects
&& with_host(|h| {
h.proto_of(a) != h.proto_of(b) || h.has_null_proto(a) != h.has_null_proto(b)
})
{
return false;
}
}
let kinds = with_host(|h| {
let av = h.get(a).map(kind);
let bv = h.get(b).map(kind);
(av, bv)
});
seen.push((a.clone(), b.clone()));
let result = deep_equal_body(a, b, strict_mode, seen, kinds);
seen.pop();
result
}
fn deep_equal_body(
a: &Value,
b: &Value,
strict_mode: bool,
seen: &mut Vec<(Value, Value)>,
kinds: (Option<Kind>, Option<Kind>),
) -> bool {
match kinds {
(Some(Kind::Array), Some(Kind::Array)) => {
let (ia, ib) = with_host(|h| (array_of(h, a), array_of(h, b)));
ia.len() == ib.len()
&& ia
.iter()
.zip(ib.iter())
.all(|(x, y)| deep_equal_seen(x, y, strict_mode, seen))
}
(Some(Kind::Object), Some(Kind::Object)) => {
let (ea, eb) = with_host(|h| (object_of(h, a), object_of(h, b)));
if ea.len() != eb.len() {
return false;
}
let props_match = ea.iter().all(|(k, va)| {
eb.iter()
.find(|(k2, _)| k2 == k)
.is_some_and(|(_, vb)| deep_equal_seen(va, vb, strict_mode, seen))
});
if !props_match {
return false;
}
let (ia, ib) = with_host(|h| (internals_of(h, a), internals_of(h, b)));
if ia.len() != ib.len() {
return false;
}
ia.iter().all(|(k, va)| {
ib.iter()
.find(|(k2, _)| k2 == k)
.is_some_and(|(_, vb)| deep_equal_seen(va, vb, strict_mode, seen))
})
}
(Some(Kind::Map), Some(Kind::Map)) => {
let (ea, eb) = with_host(|h| (map_entries_of(h, a), map_entries_of(h, b)));
unordered_match(&ea, &eb, seen, |(ka, va), (kb, vb), seen| {
deep_equal_seen(ka, kb, strict_mode, seen)
&& deep_equal_seen(va, vb, strict_mode, seen)
})
}
(Some(Kind::Set), Some(Kind::Set)) => {
let (ea, eb) = with_host(|h| (set_members_of(h, a), set_members_of(h, b)));
unordered_match(&ea, &eb, seen, |x, y, seen| {
deep_equal_seen(x, y, strict_mode, seen)
})
}
(Some(Kind::RegExp), Some(Kind::RegExp)) => {
with_host(|h| regexp_key(h, a) == regexp_key(h, b))
}
_ => {
if strict_mode {
strict(a, b)
} else {
loose_eq(a, b)
}
}
}
}
fn unordered_match<T>(
ea: &[T],
eb: &[T],
seen: &mut Vec<(Value, Value)>,
eq: impl Fn(&T, &T, &mut Vec<(Value, Value)>) -> bool,
) -> bool {
if ea.len() != eb.len() {
return false;
}
let mut claimed = vec![false; eb.len()];
'outer: for x in ea {
for (i, y) in eb.iter().enumerate() {
if !claimed[i] && eq(x, y, seen) {
claimed[i] = true;
continue 'outer;
}
}
return false;
}
true
}
enum Kind {
Array,
Object,
Map,
Set,
RegExp,
Other,
}
fn kind(o: &JsObj) -> Kind {
match o {
JsObj::Array(_) => Kind::Array,
JsObj::Object(_) => Kind::Object,
JsObj::Map { weak: false, .. } => Kind::Map,
JsObj::Set { weak: false, .. } => Kind::Set,
JsObj::RegExp(_) => Kind::RegExp,
_ => Kind::Other,
}
}
fn map_entries_of(h: &crate::host::JsHost, v: &Value) -> Vec<(Value, Value)> {
match h.get(v) {
Some(JsObj::Map { entries, .. }) => entries.values().cloned().collect(),
_ => Vec::new(),
}
}
fn set_members_of(h: &crate::host::JsHost, v: &Value) -> Vec<Value> {
match h.get(v) {
Some(JsObj::Set { entries, .. }) => entries.values().cloned().collect(),
_ => Vec::new(),
}
}
fn regexp_key(h: &crate::host::JsHost, v: &Value) -> Option<(String, String)> {
match h.get(v) {
Some(JsObj::RegExp(r)) => Some((r.source.clone(), r.flags.clone())),
_ => None,
}
}
fn internals_of(h: &crate::host::JsHost, v: &Value) -> Vec<(String, Value)> {
match h.get(v) {
Some(JsObj::Object(p)) => p
.iter()
.filter(|(k, _)| k.starts_with("@@"))
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
_ => Vec::new(),
}
}
fn array_of(h: &crate::host::JsHost, v: &Value) -> Vec<Value> {
match h.get(v) {
Some(JsObj::Array(items)) => items.clone(),
_ => Vec::new(),
}
}
fn object_of(h: &crate::host::JsHost, v: &Value) -> Vec<(String, Value)> {
match h.get(v) {
Some(JsObj::Object(p)) => p
.iter()
.filter(|(k, _)| !k.starts_with("@@") && !k.starts_with('#'))
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
_ => Vec::new(),
}
}