use crate::host::with_host;
use fusevm::Value;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Op {
Delete,
Nop,
Insert,
}
const NOP_LINES_TO_COLLAPSE: usize = 5;
const MAX_SHORT_STRING_LENGTH: usize = 12;
fn inspect_value(v: &Value) -> String {
crate::host::set_inspect_compact(0);
crate::host::set_inspect_sorted(true);
crate::host::set_inspect_max_depth(1000);
crate::host::set_inspect_max_array_length(usize::MAX);
crate::host::set_inspect_custom(false);
let s = with_host(|h| h.inspect(v));
crate::host::set_inspect_compact(3);
crate::host::set_inspect_sorted(false);
crate::host::set_inspect_max_depth(2);
crate::host::set_inspect_max_array_length(crate::host::DEFAULT_MAX_ARRAY_LENGTH);
crate::host::set_inspect_custom(true);
s
}
fn lines_equal(actual: &str, expected: &str, check_comma_disparity: bool) -> bool {
if actual == expected {
return true;
}
if check_comma_disparity {
return expected.strip_suffix(',').is_some_and(|e| e == actual)
|| actual.strip_suffix(',').is_some_and(|a| a == expected);
}
false
}
fn myers_diff(
actual: &[&str],
expected: &[&str],
check_comma_disparity: bool,
) -> Vec<(Op, String)> {
let actual_len = actual.len() as i64;
let expected_len = expected.len() as i64;
let max = actual_len + expected_len;
if max == 0 {
return Vec::new();
}
let mut v = vec![0i64; (2 * max + 1) as usize];
let mut trace: Vec<Vec<i64>> = Vec::new();
for diff_level in 0..=max {
trace.push(v.clone());
let mut diagonal = -diff_level;
while diagonal <= diff_level {
let offset = (diagonal + max) as usize;
let mut x = if diagonal == -diff_level
|| (diagonal != diff_level && v[offset - 1] < v[offset + 1])
{
v[offset + 1]
} else {
v[offset - 1] + 1
};
let mut y = x - diagonal;
while x < actual_len
&& y < expected_len
&& lines_equal(
actual[x as usize],
expected[y as usize],
check_comma_disparity,
)
{
x += 1;
y += 1;
}
v[offset] = x;
if x >= actual_len && y >= expected_len {
return backtrack(&trace, actual, expected, check_comma_disparity);
}
diagonal += 2;
}
}
Vec::new()
}
fn backtrack(
trace: &[Vec<i64>],
actual: &[&str],
expected: &[&str],
check_comma_disparity: bool,
) -> Vec<(Op, String)> {
let actual_len = actual.len() as i64;
let expected_len = expected.len() as i64;
let max = actual_len + expected_len;
let mut x = actual_len;
let mut y = expected_len;
let mut result: Vec<(Op, String)> = Vec::new();
for diff_level in (0..trace.len() as i64).rev() {
let v = &trace[diff_level as usize];
let diagonal = x - y;
let offset = (diagonal + max) as usize;
let prev_diagonal = if diagonal == -diff_level
|| (diagonal != diff_level && v[offset - 1] < v[offset + 1])
{
diagonal + 1
} else {
diagonal - 1
};
let idx = prev_diagonal + max;
let prev = if idx >= 0 && (idx as usize) < v.len() {
let prev_x = v[idx as usize];
Some((prev_x, prev_x - prev_diagonal))
} else {
None
};
if let Some((prev_x, prev_y)) = prev {
while x > prev_x && y > prev_y {
let actual_item = actual[(x - 1) as usize];
let value = if check_comma_disparity && !actual_item.ends_with(',') {
expected[(y - 1) as usize]
} else {
actual_item
};
result.push((Op::Nop, value.to_string()));
x -= 1;
y -= 1;
}
}
if diff_level > 0 {
if prev.is_some_and(|(prev_x, _)| x > prev_x) {
x -= 1;
result.push((Op::Insert, actual[x as usize].to_string()));
} else {
y -= 1;
result.push((Op::Delete, expected[y as usize].to_string()));
}
}
}
result
}
fn print_myers_diff(diff: &[(Op, String)]) -> (String, bool) {
let mut message = String::new();
let mut skipped = false;
let mut nop_count = 0usize;
for idx in (0..diff.len()).rev() {
let (operation, value) = (&diff[idx].0, &diff[idx].1);
let previous_operation = if idx + 1 < diff.len() {
Some(diff[idx + 1].0)
} else {
None
};
if previous_operation == Some(Op::Nop) && *operation != Op::Nop {
if nop_count == NOP_LINES_TO_COLLAPSE + 1 {
message.push_str(&format!(" {}\n", diff[idx + 1].1));
} else if nop_count == NOP_LINES_TO_COLLAPSE + 2 {
message.push_str(&format!(" {}\n", diff[idx + 2].1));
message.push_str(&format!(" {}\n", diff[idx + 1].1));
} else if nop_count >= NOP_LINES_TO_COLLAPSE + 3 {
message.push_str("...\n");
message.push_str(&format!(" {}\n", diff[idx + 1].1));
skipped = true;
}
nop_count = 0;
}
match operation {
Op::Insert => message.push_str(&format!("+ {value}\n")),
Op::Delete => message.push_str(&format!("- {value}\n")),
Op::Nop => {
if nop_count < NOP_LINES_TO_COLLAPSE {
message.push_str(&format!(" {value}\n"));
}
nop_count += 1;
}
}
}
(message.trim_end().to_string(), skipped)
}
fn readable_operator(op: &str) -> &'static str {
match op {
"deepStrictEqual" => "Expected values to be strictly deep-equal:",
"partialDeepStrictEqual" => "Expected values to be partially and strictly deep-equal:",
"strictEqual" => "Expected values to be strictly equal:",
"strictEqualObject" => "Expected \"actual\" to be reference-equal to \"expected\":",
"notIdentical" => "Values have same structure but are not reference-equal:",
_ => "Expected values to be strictly deep-equal:",
}
}
fn is_object(v: &Value) -> bool {
with_host(|h| h.type_of(v) == "object" && !h.is_null(v))
}
fn check_operator(actual: &Value, expected: &Value, operator: &str) -> String {
if operator == "strictEqual" && is_object(actual) && is_object(expected) {
return "strictEqualObject".to_string();
}
operator.to_string()
}
fn stacked_diff(actual: &str, expected: &str) -> String {
let mut message = format!("\n+ {actual}\n- {expected}");
let strings_len = actual.chars().count() + expected.chars().count();
if strings_len > 80 {
return message;
}
let a: Vec<char> = actual.chars().collect();
let e: Vec<char> = expected.chars().collect();
let mut indicator_idx: Option<usize> = None;
for (i, ch) in a.iter().enumerate() {
if e.get(i) != Some(ch) {
if i >= 3 {
indicator_idx = Some(i);
}
break;
}
}
if let Some(i) = indicator_idx {
message.push('\n');
message.push_str(&" ".repeat(i + 2));
message.push('^');
}
message
}
fn simple_diff(
original_actual: &Value,
actual: &str,
original_expected: &Value,
expected: &str,
) -> (String, Option<String>) {
let is_str = |v: &Value| with_host(|h| h.type_of(v) == "string");
let mut strings_len = actual.chars().count() + expected.chars().count();
if is_str(original_actual) {
strings_len = strings_len.saturating_sub(2);
}
if is_str(original_expected) {
strings_len = strings_len.saturating_sub(2);
}
let both_zero = with_host(|h| {
h.type_of(original_actual) == "number"
&& h.type_of(original_expected) == "number"
&& h.to_number(original_actual) == 0.0
&& h.to_number(original_expected) == 0.0
});
if strings_len <= MAX_SHORT_STRING_LENGTH && !both_zero {
return (format!("{actual} !== {expected}"), Some(String::new()));
}
(stacked_diff(actual, expected), None)
}
pub fn create_err_diff(
actual: &Value,
expected: &Value,
operator: &str,
custom_message: Option<&str>,
) -> String {
let mut operator = check_operator(actual, expected, operator);
let mut skipped = false;
let message;
let inspected_actual = inspect_value(actual);
let inspected_expected = inspect_value(expected);
let split_actual: Vec<&str> = inspected_actual.split('\n').collect();
let split_expected: Vec<&str> = inspected_expected.split('\n').collect();
let mut header = "+ actual - expected".to_string();
let show_simple = if split_actual.len() > 1 || split_expected.len() > 1 {
false
} else {
!is_object(actual) || !is_object(expected)
};
if show_simple {
let (m, h) = simple_diff(actual, split_actual[0], expected, split_expected[0]);
message = m;
if let Some(h) = h {
header = h;
}
} else if inspected_actual == inspected_expected {
operator = "notIdentical".to_string();
if split_actual.len() > 50 {
message = format!("{}\n...}}", split_actual[..50].join("\n"));
skipped = true;
} else {
message = split_actual.join("\n");
}
header = String::new();
} else {
let check_comma_disparity = is_object(actual);
let diff = myers_diff(&split_actual, &split_expected, check_comma_disparity);
let (body, was_skipped) = print_myers_diff(&diff);
message = format!("\n{body}");
if was_skipped {
skipped = true;
}
}
let heading = custom_message
.map(str::to_string)
.unwrap_or_else(|| readable_operator(&operator).to_string());
let skipped_message = if skipped { "\n... Skipped lines" } else { "" };
format!("{heading}\n{header}{skipped_message}\n{message}\n")
}
pub fn inspect_operand(v: &Value) -> String {
inspect_value(v)
}