Skip to content

Commit 16a54cb

Browse files
puskinpmarchini
andcommitted
assert: make assertion_error user mysers diff algorithm
Fixes: #51733 Co-Authored-By: Pietro Marchini <pietro.marchini94@gmail.com>
1 parent 9404d3a commit 16a54cb

File tree

9 files changed

+718
-438
lines changed

9 files changed

+718
-438
lines changed

lib/internal/assert/assertion_error.js

Lines changed: 57 additions & 253 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,27 @@
11
'use strict';
22

33
const {
4+
ArrayIsArray,
45
ArrayPrototypeJoin,
56
ArrayPrototypePop,
67
Error,
78
ErrorCaptureStackTrace,
8-
MathMax,
9+
Map,
10+
Object,
911
ObjectAssign,
1012
ObjectDefineProperty,
1113
ObjectGetPrototypeOf,
14+
ObjectKeys,
1215
String,
13-
StringPrototypeEndsWith,
14-
StringPrototypeRepeat,
1516
StringPrototypeSlice,
1617
StringPrototypeSplit,
1718
} = primordials;
1819

1920
const { inspect } = require('internal/util/inspect');
20-
const {
21-
removeColors,
22-
} = require('internal/util');
2321
const colors = require('internal/util/colors');
24-
const {
25-
validateObject,
26-
} = require('internal/validators');
22+
const { validateObject } = require('internal/validators');
2723
const { isErrorStackTraceLimitWritable } = require('internal/errors');
28-
24+
const { myersDiff, printMyersDiff } = require('internal/assert/myers_diff');
2925

3026
const kReadableOperator = {
3127
deepStrictEqual: 'Expected values to be strictly deep-equal:',
@@ -41,269 +37,77 @@ const kReadableOperator = {
4137
notDeepEqualUnequal: 'Expected values not to be loosely deep-equal:',
4238
};
4339

44-
// Comparing short primitives should just show === / !== instead of using the
45-
// diff.
46-
const kMaxShortLength = 12;
47-
4840
function copyError(source) {
49-
const target = ObjectAssign({ __proto__: ObjectGetPrototypeOf(source) }, source);
50-
ObjectDefineProperty(target, 'message', { __proto__: null, value: source.message });
41+
const target = ObjectAssign(
42+
{ __proto__: ObjectGetPrototypeOf(source) },
43+
source,
44+
);
45+
ObjectDefineProperty(target, 'message', {
46+
__proto__: null,
47+
value: source.message,
48+
});
5149
return target;
5250
}
5351

5452
function inspectValue(val) {
5553
// The util.inspect default values could be changed. This makes sure the
5654
// error messages contain the necessary information nevertheless.
57-
return inspect(
58-
val,
59-
{
60-
compact: false,
61-
customInspect: false,
62-
depth: 1000,
63-
maxArrayLength: Infinity,
64-
// Assert compares only enumerable properties (with a few exceptions).
65-
showHidden: false,
66-
// Assert does not detect proxies currently.
67-
showProxy: false,
68-
sorted: true,
69-
// Inspect getters as we also check them when comparing entries.
70-
getters: true,
71-
},
72-
);
55+
return inspect(val, {
56+
compact: false,
57+
customInspect: false,
58+
depth: 1000,
59+
maxArrayLength: Infinity,
60+
// Assert compares only enumerable properties (with a few exceptions).
61+
showHidden: false,
62+
// Assert does not detect proxies currently.
63+
showProxy: false,
64+
sorted: true,
65+
// Inspect getters as we also check them when comparing entries.
66+
getters: true,
67+
});
7368
}
7469

75-
function createErrDiff(actual, expected, operator) {
76-
let other = '';
77-
let res = '';
78-
let end = '';
79-
let skipped = false;
80-
const actualInspected = inspectValue(actual);
81-
const actualLines = StringPrototypeSplit(actualInspected, '\n');
82-
const expectedLines = StringPrototypeSplit(inspectValue(expected), '\n');
70+
function showSimpleDiff(test) {
71+
const isPrimitive = test !== Object(test);
72+
const isEmptyArray = ArrayIsArray(test) && test.length === 0;
73+
const isSimpleObject = !isPrimitive && !(test instanceof Map) && ObjectKeys(test).length === 0;
8374

84-
let i = 0;
85-
let indicator = '';
75+
return isPrimitive || isEmptyArray || isSimpleObject;
76+
}
8677

78+
function checkOperator(actual, expected, operator) {
8779
// In case both values are objects or functions explicitly mark them as not
8880
// reference equal for the `strictEqual` operator.
89-
if (operator === 'strictEqual' &&
90-
((typeof actual === 'object' && actual !== null &&
91-
typeof expected === 'object' && expected !== null) ||
92-
(typeof actual === 'function' && typeof expected === 'function'))) {
81+
if (
82+
operator === 'strictEqual' &&
83+
((typeof actual === 'object' &&
84+
actual !== null &&
85+
typeof expected === 'object' &&
86+
expected !== null) ||
87+
(typeof actual === 'function' && typeof expected === 'function'))
88+
) {
9389
operator = 'strictEqualObject';
9490
}
9591

96-
// If "actual" and "expected" fit on a single line and they are not strictly
97-
// equal, check further special handling.
98-
if (actualLines.length === 1 && expectedLines.length === 1 &&
99-
actualLines[0] !== expectedLines[0]) {
100-
// Check for the visible length using the `removeColors()` function, if
101-
// appropriate.
102-
const c = inspect.defaultOptions.colors;
103-
const actualRaw = c ? removeColors(actualLines[0]) : actualLines[0];
104-
const expectedRaw = c ? removeColors(expectedLines[0]) : expectedLines[0];
105-
const inputLength = actualRaw.length + expectedRaw.length;
106-
// If the character length of "actual" and "expected" together is less than
107-
// kMaxShortLength and if neither is an object and at least one of them is
108-
// not `zero`, use the strict equal comparison to visualize the output.
109-
if (inputLength <= kMaxShortLength) {
110-
if ((typeof actual !== 'object' || actual === null) &&
111-
(typeof expected !== 'object' || expected === null) &&
112-
(actual !== 0 || expected !== 0)) { // -0 === +0
113-
return `${kReadableOperator[operator]}\n\n` +
114-
`${actualLines[0]} !== ${expectedLines[0]}\n`;
115-
}
116-
} else if (operator !== 'strictEqualObject') {
117-
// If the stderr is a tty and the input length is lower than the current
118-
// columns per line, add a mismatch indicator below the output. If it is
119-
// not a tty, use a default value of 80 characters.
120-
const maxLength = process.stderr.isTTY ? process.stderr.columns : 80;
121-
if (inputLength < maxLength) {
122-
while (actualRaw[i] === expectedRaw[i]) {
123-
i++;
124-
}
125-
// Ignore the first characters.
126-
if (i > 2) {
127-
// Add position indicator for the first mismatch in case it is a
128-
// single line and the input length is less than the column length.
129-
indicator = `\n ${StringPrototypeRepeat(' ', i)}^`;
130-
i = 0;
131-
}
132-
}
133-
}
134-
}
135-
136-
// Remove all ending lines that match (this optimizes the output for
137-
// readability by reducing the number of total changed lines).
138-
let a = actualLines[actualLines.length - 1];
139-
let b = expectedLines[expectedLines.length - 1];
140-
while (a === b) {
141-
if (i++ < 3) {
142-
end = `\n ${a}${end}`;
143-
} else {
144-
other = a;
145-
}
146-
ArrayPrototypePop(actualLines);
147-
ArrayPrototypePop(expectedLines);
148-
if (actualLines.length === 0 || expectedLines.length === 0)
149-
break;
150-
a = actualLines[actualLines.length - 1];
151-
b = expectedLines[expectedLines.length - 1];
152-
}
153-
154-
const maxLines = MathMax(actualLines.length, expectedLines.length);
155-
// Strict equal with identical objects that are not identical by reference.
156-
// E.g., assert.deepStrictEqual({ a: Symbol() }, { a: Symbol() })
157-
if (maxLines === 0) {
158-
// We have to get the result again. The lines were all removed before.
159-
const actualLines = StringPrototypeSplit(actualInspected, '\n');
160-
161-
// Only remove lines in case it makes sense to collapse those.
162-
// TODO: Accept env to always show the full error.
163-
if (actualLines.length > 50) {
164-
actualLines[46] = `${colors.blue}...${colors.white}`;
165-
while (actualLines.length > 47) {
166-
ArrayPrototypePop(actualLines);
167-
}
168-
}
169-
170-
return `${kReadableOperator.notIdentical}\n\n` +
171-
`${ArrayPrototypeJoin(actualLines, '\n')}\n`;
172-
}
173-
174-
// There were at least five identical lines at the end. Mark a couple of
175-
// skipped.
176-
if (i >= 5) {
177-
end = `\n${colors.blue}...${colors.white}${end}`;
178-
skipped = true;
179-
}
180-
if (other !== '') {
181-
end = `\n ${other}${end}`;
182-
other = '';
183-
}
92+
return operator;
93+
}
18494

185-
let printedLines = 0;
186-
let identical = 0;
187-
const msg = kReadableOperator[operator] +
188-
`\n${colors.green}+ actual${colors.white} ${colors.red}- expected${colors.white}`;
189-
const skippedMsg = ` ${colors.blue}...${colors.white} Lines skipped`;
95+
function createErrDiff(actual, expected, operator) {
96+
operator = checkOperator(actual, expected, operator);
97+
const nopSkippedMessage = `\n${colors.blue}...${colors.white} Lines skipped which didn't differ`;
98+
const insertedSkippedMessage = `\n${colors.green}...${colors.white} Lines skipped which were inserted`;
99+
const deletedSkippedMessage = `\n${colors.red}...${colors.white} Lines skipped which were deleted`;
190100

191-
let lines = actualLines;
192-
let plusMinus = `${colors.green}+${colors.white}`;
193-
let maxLength = expectedLines.length;
194-
if (actualLines.length < maxLines) {
195-
lines = expectedLines;
196-
plusMinus = `${colors.red}-${colors.white}`;
197-
maxLength = actualLines.length;
198-
}
101+
const simpleDiff = showSimpleDiff(actual) && showSimpleDiff(expected);
102+
const isStringComparison = typeof actual === 'string' && typeof expected === 'string';
103+
const header = simpleDiff ? '' : `${colors.green}+ actual${colors.white} ${colors.red}- expected${colors.white}`;
104+
const headerMessage = `${kReadableOperator[operator]}\n${header}`;
199105

200-
for (i = 0; i < maxLines; i++) {
201-
if (maxLength < i + 1) {
202-
// If more than two former lines are identical, print them. Collapse them
203-
// in case more than five lines were identical.
204-
if (identical > 2) {
205-
if (identical > 3) {
206-
if (identical > 4) {
207-
if (identical === 5) {
208-
res += `\n ${lines[i - 3]}`;
209-
printedLines++;
210-
} else {
211-
res += `\n${colors.blue}...${colors.white}`;
212-
skipped = true;
213-
}
214-
}
215-
res += `\n ${lines[i - 2]}`;
216-
printedLines++;
217-
}
218-
res += `\n ${lines[i - 1]}`;
219-
printedLines++;
220-
}
221-
// No identical lines before.
222-
identical = 0;
223-
// Add the expected line to the cache.
224-
if (lines === actualLines) {
225-
res += `\n${plusMinus} ${lines[i]}`;
226-
} else {
227-
other += `\n${plusMinus} ${lines[i]}`;
228-
}
229-
printedLines++;
230-
// Only extra actual lines exist
231-
// Lines diverge
232-
} else {
233-
const expectedLine = expectedLines[i];
234-
let actualLine = actualLines[i];
235-
// If the lines diverge, specifically check for lines that only diverge by
236-
// a trailing comma. In that case it is actually identical and we should
237-
// mark it as such.
238-
let divergingLines =
239-
actualLine !== expectedLine &&
240-
(!StringPrototypeEndsWith(actualLine, ',') ||
241-
StringPrototypeSlice(actualLine, 0, -1) !== expectedLine);
242-
// If the expected line has a trailing comma but is otherwise identical,
243-
// add a comma at the end of the actual line. Otherwise the output could
244-
// look weird as in:
245-
//
246-
// [
247-
// 1 // No comma at the end!
248-
// + 2
249-
// ]
250-
//
251-
if (divergingLines &&
252-
StringPrototypeEndsWith(expectedLine, ',') &&
253-
StringPrototypeSlice(expectedLine, 0, -1) === actualLine) {
254-
divergingLines = false;
255-
actualLine += ',';
256-
}
257-
if (divergingLines) {
258-
// If more than two former lines are identical, print them. Collapse
259-
// them in case more than five lines were identical.
260-
if (identical > 2) {
261-
if (identical > 3) {
262-
if (identical > 4) {
263-
if (identical === 5) {
264-
res += `\n ${actualLines[i - 3]}`;
265-
printedLines++;
266-
} else {
267-
res += `\n${colors.blue}...${colors.white}`;
268-
skipped = true;
269-
}
270-
}
271-
res += `\n ${actualLines[i - 2]}`;
272-
printedLines++;
273-
}
274-
res += `\n ${actualLines[i - 1]}`;
275-
printedLines++;
276-
}
277-
// No identical lines before.
278-
identical = 0;
279-
// Add the actual line to the result and cache the expected diverging
280-
// line so consecutive diverging lines show up as +++--- and not +-+-+-.
281-
res += `\n${colors.green}+${colors.white} ${actualLine}`;
282-
other += `\n${colors.red}-${colors.white} ${expectedLine}`;
283-
printedLines += 2;
284-
// Lines are identical
285-
} else {
286-
// Add all cached information to the result before adding other things
287-
// and reset the cache.
288-
res += other;
289-
other = '';
290-
identical++;
291-
// The very first identical line since the last diverging line is be
292-
// added to the result.
293-
if (identical <= 2) {
294-
res += `\n ${actualLine}`;
295-
printedLines++;
296-
}
297-
}
298-
}
299-
// Inspected object to big (Show ~50 rows max)
300-
if (printedLines > 50 && i < maxLines - 2) {
301-
return `${msg}${skippedMsg}\n${res}\n${colors.blue}...${colors.white}${other}\n` +
302-
`${colors.blue}...${colors.white}`;
303-
}
304-
}
106+
const diff = myersDiff(actual, expected);
107+
const { message, skipped } = printMyersDiff(diff, simpleDiff, isStringComparison);
108+
const skippedMessahe = skipped ? `${nopSkippedMessage}${insertedSkippedMessage}${deletedSkippedMessage}` : '';
305109

306-
return `${msg}${skipped ? skippedMsg : ''}\n${res}${other}${end}${indicator}`;
110+
return `${headerMessage}${skippedMessahe}\n${message}\n`;
307111
}
308112

309113
function addEllipsis(string) {

0 commit comments

Comments
 (0)