Skip to content

Commit

Permalink
assert: make assertion_error use Myers diff algorithm
Browse files Browse the repository at this point in the history
Fixes: nodejs#51733

Co-Authored-By: Pietro Marchini <pietro.marchini94@gmail.com>
  • Loading branch information
puskin94 and pmarchini committed Oct 8, 2024
1 parent d24c731 commit c9ffc67
Show file tree
Hide file tree
Showing 8 changed files with 715 additions and 426 deletions.
398 changes: 161 additions & 237 deletions lib/internal/assert/assertion_error.js

Large diffs are not rendered by default.

168 changes: 168 additions & 0 deletions lib/internal/assert/myers_diff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
'use strict';

const {
Array,
ArrayPrototypeFill,
ArrayPrototypeJoin,
ArrayPrototypePush,
ArrayPrototypeSlice,
ArrayPrototypeSplice,
StringPrototypeEndsWith,
StringPrototypeSlice,
} = primordials;

const colors = require('internal/util/colors');

const kMaxStringLength = 512;
const kNopLinesToCollapse = 5;

function areLinesEqual(actual, expected, checkCommaDisparity) {
if (!checkCommaDisparity) {
return actual === expected;
}
return actual === expected || `${actual},` === expected || actual === `${expected},`;
}

function myersDiff(actual, expected, checkCommaDisparity = false) {
const actualLength = actual.length;
const expectedLength = expected.length;
const max = actualLength + expectedLength;
const v = ArrayPrototypeFill(Array(2 * max + 1), 0);

const trace = [];

for (let diffLevel = 0; diffLevel <= max; diffLevel++) {
const newTrace = ArrayPrototypeSlice(v);
ArrayPrototypePush(trace, newTrace);

for (let diagonalIndex = -diffLevel; diagonalIndex <= diffLevel; diagonalIndex += 2) {
let x;
if (diagonalIndex === -diffLevel ||
(diagonalIndex !== diffLevel && v[diagonalIndex - 1 + max] < v[diagonalIndex + 1 + max])) {
x = v[diagonalIndex + 1 + max];
} else {
x = v[diagonalIndex - 1 + max] + 1;
}

let y = x - diagonalIndex;

while (x < actualLength && y < expectedLength && areLinesEqual(actual[x], expected[y], checkCommaDisparity)) {
x++;
y++;
}

v[diagonalIndex + max] = x;

if (x >= actualLength && y >= expectedLength) {
return backtrack(trace, actual, expected, checkCommaDisparity);
}
}
}
}

function backtrack(trace, actual, expected, checkCommaDisparity) {
const actualLength = actual.length;
const expectedLength = expected.length;
const max = actualLength + expectedLength;

let x = actualLength;
let y = expectedLength;
const result = [];

for (let diffLevel = trace.length - 1; diffLevel >= 0; diffLevel--) {
const v = trace[diffLevel];
const diagonalIndex = x - y;
let prevDiagonalIndex;

if (diagonalIndex === -diffLevel ||
(diagonalIndex !== diffLevel && v[diagonalIndex - 1 + max] < v[diagonalIndex + 1 + max])) {
prevDiagonalIndex = diagonalIndex + 1;
} else {
prevDiagonalIndex = diagonalIndex - 1;
}

const prevX = v[prevDiagonalIndex + max];
const prevY = prevX - prevDiagonalIndex;

while (x > prevX && y > prevY) {
const value = !checkCommaDisparity ||
StringPrototypeEndsWith(actual[x - 1], ',') ? actual[x - 1] : expected[y - 1];
ArrayPrototypePush(result, { __proto__: null, type: 'nop', value });
x--;
y--;
}

if (diffLevel > 0) {
if (x > prevX) {
ArrayPrototypePush(result, { __proto__: null, type: 'insert', value: actual[x - 1] });
x--;
} else {
ArrayPrototypePush(result, { __proto__: null, type: 'delete', value: expected[y - 1] });
y--;
}
}
}

return result.reverse();
}

function formatValue(value) {
if (value.length > kMaxStringLength) {
return `${StringPrototypeSlice(value, 0, kMaxStringLength + 1)}...`;
}
return value;
}

function printSimpleMyersDiff(diff) {
let message = [];

for (let diffIdx = 0; diffIdx < diff.length; diffIdx++) {
const { type, value } = diff[diffIdx];
if (type === 'insert') {
message += `${colors.green}${value}${colors.white}`;
} else if (type === 'delete') {
message += `${colors.red}${value}${colors.white}`;
} else {
message += `${colors.white}${value}${colors.white}`;
}
}

return `\n${message}`;
}

function printMyersDiff(diff, simple = false) {
const message = [];
let skipped = false;
let previousType = 'null';
let nopCount = 0;

for (let diffIdx = 0; diffIdx < diff.length; diffIdx++) {
const { type, value } = diff[diffIdx];
const typeChanged = previousType && (type !== previousType);

if (type === 'insert') {
ArrayPrototypePush(message, `${colors.green}+${colors.white} ${formatValue(value)}`);
} else if (type === 'delete') {
ArrayPrototypePush(message, `${colors.red}-${colors.white} ${formatValue(value)}`);
} else if (type === 'nop') {
if (nopCount <= kNopLinesToCollapse) {
ArrayPrototypePush(message, `${colors.white} ${formatValue(value)}`);
}
nopCount++;
}

if (typeChanged && previousType === 'nop') {
if (nopCount > kNopLinesToCollapse) {
ArrayPrototypeSplice(message, message.length - 1, 0, `${colors.blue}...${colors.white}`);
skipped = true;
}
nopCount = 0;
}

previousType = type;
}

return { __proto__: null, message: `\n${ArrayPrototypeJoin(message, '\n')}`, skipped };
}

module.exports = { myersDiff, printMyersDiff, printSimpleMyersDiff };
10 changes: 8 additions & 2 deletions test/parallel/test-assert-checktag.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,20 @@ test('', { skip: !hasCrypto }, () => {
() => assert.deepStrictEqual(date, fake),
{
message: 'Expected values to be strictly deep-equal:\n' +
'+ actual - expected\n\n+ 2016-01-01T00:00:00.000Z\n- Date {}'
'+ actual - expected\n' +
'\n' +
'+ 2016-01-01T00:00:00.000Z\n' +
'- Date {}\n'
}
);
assert.throws(
() => assert.deepStrictEqual(fake, date),
{
message: 'Expected values to be strictly deep-equal:\n' +
'+ actual - expected\n\n+ Date {}\n- 2016-01-01T00:00:00.000Z'
'+ actual - expected\n' +
'\n' +
'+ Date {}\n' +
'- 2016-01-01T00:00:00.000Z\n'
}
);
}
Expand Down
Loading

0 comments on commit c9ffc67

Please sign in to comment.