Skip to content

Commit 4ba31b7

Browse files
puskin94pmarchini
authored andcommittedOct 19, 2024
assert: make assertion_error use Myers diff algorithm
Fixes: #51733 Co-Authored-By: Pietro Marchini <pietro.marchini94@gmail.com> PR-URL: #54862 Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 0130780 commit 4ba31b7

8 files changed

+731
-424
lines changed
 

‎lib/internal/assert/assertion_error.js

+138-242
Large diffs are not rendered by default.

‎lib/internal/assert/myers_diff.js

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
'use strict';
2+
3+
const {
4+
Array,
5+
ArrayPrototypeFill,
6+
ArrayPrototypePush,
7+
ArrayPrototypeSlice,
8+
StringPrototypeEndsWith,
9+
} = primordials;
10+
11+
const colors = require('internal/util/colors');
12+
13+
const kNopLinesToCollapse = 5;
14+
15+
function areLinesEqual(actual, expected, checkCommaDisparity) {
16+
if (actual === expected) {
17+
return true;
18+
}
19+
if (checkCommaDisparity) {
20+
return `${actual},` === expected || actual === `${expected},`;
21+
}
22+
return false;
23+
}
24+
25+
function myersDiff(actual, expected, checkCommaDisparity = false) {
26+
const actualLength = actual.length;
27+
const expectedLength = expected.length;
28+
const max = actualLength + expectedLength;
29+
const v = ArrayPrototypeFill(Array(2 * max + 1), 0);
30+
31+
const trace = [];
32+
33+
for (let diffLevel = 0; diffLevel <= max; diffLevel++) {
34+
const newTrace = ArrayPrototypeSlice(v);
35+
ArrayPrototypePush(trace, newTrace);
36+
37+
for (let diagonalIndex = -diffLevel; diagonalIndex <= diffLevel; diagonalIndex += 2) {
38+
let x;
39+
if (diagonalIndex === -diffLevel ||
40+
(diagonalIndex !== diffLevel && v[diagonalIndex - 1 + max] < v[diagonalIndex + 1 + max])) {
41+
x = v[diagonalIndex + 1 + max];
42+
} else {
43+
x = v[diagonalIndex - 1 + max] + 1;
44+
}
45+
46+
let y = x - diagonalIndex;
47+
48+
while (x < actualLength && y < expectedLength && areLinesEqual(actual[x], expected[y], checkCommaDisparity)) {
49+
x++;
50+
y++;
51+
}
52+
53+
v[diagonalIndex + max] = x;
54+
55+
if (x >= actualLength && y >= expectedLength) {
56+
return backtrack(trace, actual, expected, checkCommaDisparity);
57+
}
58+
}
59+
}
60+
}
61+
62+
function backtrack(trace, actual, expected, checkCommaDisparity) {
63+
const actualLength = actual.length;
64+
const expectedLength = expected.length;
65+
const max = actualLength + expectedLength;
66+
67+
let x = actualLength;
68+
let y = expectedLength;
69+
const result = [];
70+
71+
for (let diffLevel = trace.length - 1; diffLevel >= 0; diffLevel--) {
72+
const v = trace[diffLevel];
73+
const diagonalIndex = x - y;
74+
let prevDiagonalIndex;
75+
76+
if (diagonalIndex === -diffLevel ||
77+
(diagonalIndex !== diffLevel && v[diagonalIndex - 1 + max] < v[diagonalIndex + 1 + max])) {
78+
prevDiagonalIndex = diagonalIndex + 1;
79+
} else {
80+
prevDiagonalIndex = diagonalIndex - 1;
81+
}
82+
83+
const prevX = v[prevDiagonalIndex + max];
84+
const prevY = prevX - prevDiagonalIndex;
85+
86+
while (x > prevX && y > prevY) {
87+
const value = !checkCommaDisparity ||
88+
StringPrototypeEndsWith(actual[x - 1], ',') ? actual[x - 1] : expected[y - 1];
89+
ArrayPrototypePush(result, { __proto__: null, type: 'nop', value });
90+
x--;
91+
y--;
92+
}
93+
94+
if (diffLevel > 0) {
95+
if (x > prevX) {
96+
ArrayPrototypePush(result, { __proto__: null, type: 'insert', value: actual[x - 1] });
97+
x--;
98+
} else {
99+
ArrayPrototypePush(result, { __proto__: null, type: 'delete', value: expected[y - 1] });
100+
y--;
101+
}
102+
}
103+
}
104+
105+
return result;
106+
}
107+
108+
function printSimpleMyersDiff(diff) {
109+
let message = '';
110+
111+
for (let diffIdx = diff.length - 1; diffIdx >= 0; diffIdx--) {
112+
const { type, value } = diff[diffIdx];
113+
if (type === 'insert') {
114+
message += `${colors.green}${value}${colors.white}`;
115+
} else if (type === 'delete') {
116+
message += `${colors.red}${value}${colors.white}`;
117+
} else {
118+
message += `${colors.white}${value}${colors.white}`;
119+
}
120+
}
121+
122+
return `\n${message}`;
123+
}
124+
125+
function printMyersDiff(diff, simple = false) {
126+
let message = '';
127+
let skipped = false;
128+
let nopCount = 0;
129+
130+
for (let diffIdx = diff.length - 1; diffIdx >= 0; diffIdx--) {
131+
const { type, value } = diff[diffIdx];
132+
const previousType = (diffIdx < (diff.length - 1)) ? diff[diffIdx + 1].type : null;
133+
const typeChanged = previousType && (type !== previousType);
134+
135+
if (typeChanged && previousType === 'nop') {
136+
// Avoid grouping if only one line would have been grouped otherwise
137+
if (nopCount === kNopLinesToCollapse + 1) {
138+
message += `${colors.white} ${diff[diffIdx + 1].value}\n`;
139+
} else if (nopCount === kNopLinesToCollapse + 2) {
140+
message += `${colors.white} ${diff[diffIdx + 2].value}\n`;
141+
message += `${colors.white} ${diff[diffIdx + 1].value}\n`;
142+
} if (nopCount >= (kNopLinesToCollapse + 3)) {
143+
message += `${colors.blue}...${colors.white}\n`;
144+
message += `${colors.white} ${diff[diffIdx + 1].value}\n`;
145+
skipped = true;
146+
}
147+
nopCount = 0;
148+
}
149+
150+
if (type === 'insert') {
151+
message += `${colors.green}+${colors.white} ${value}\n`;
152+
} else if (type === 'delete') {
153+
message += `${colors.red}-${colors.white} ${value}\n`;
154+
} else if (type === 'nop') {
155+
if (nopCount < kNopLinesToCollapse) {
156+
message += `${colors.white} ${value}\n`;
157+
}
158+
nopCount++;
159+
}
160+
}
161+
162+
message = message.trimEnd();
163+
164+
return { message: `\n${message}`, skipped };
165+
}
166+
167+
module.exports = { myersDiff, printMyersDiff, printSimpleMyersDiff };

‎test/parallel/test-assert-checktag.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,20 @@ test('', { skip: !hasCrypto }, () => {
2929
() => assert.deepStrictEqual(date, fake),
3030
{
3131
message: 'Expected values to be strictly deep-equal:\n' +
32-
'+ actual - expected\n\n+ 2016-01-01T00:00:00.000Z\n- Date {}'
32+
'+ actual - expected\n' +
33+
'\n' +
34+
'+ 2016-01-01T00:00:00.000Z\n' +
35+
'- Date {}\n'
3336
}
3437
);
3538
assert.throws(
3639
() => assert.deepStrictEqual(fake, date),
3740
{
3841
message: 'Expected values to be strictly deep-equal:\n' +
39-
'+ actual - expected\n\n+ Date {}\n- 2016-01-01T00:00:00.000Z'
42+
'+ actual - expected\n' +
43+
'\n' +
44+
'+ Date {}\n' +
45+
'- 2016-01-01T00:00:00.000Z\n'
4046
}
4147
);
4248
}

0 commit comments

Comments
 (0)