Skip to content

Commit 480d3c7

Browse files
committed
util: expose diff function used by the assertion errors
fix: #51740
1 parent 5d9b63d commit 480d3c7

File tree

6 files changed

+355
-163
lines changed

6 files changed

+355
-163
lines changed

doc/api/util.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,60 @@ The `--throw-deprecation` command-line flag and `process.throwDeprecation`
325325
property take precedence over `--trace-deprecation` and
326326
`process.traceDeprecation`.
327327

328+
## `util.diff(actual, expected)`
329+
330+
<!-- YAML
331+
added: REPLACEME
332+
-->
333+
334+
* `actual` {any}
335+
* `expected` {any}
336+
* Returns: {string | null} A string or `null` if there is difference
337+
between `actual` and `expected`
338+
339+
[`util.diff()`][] is a utility function that compares two values and returns a
340+
string that represents the difference between them. It is useful for debugging
341+
and testing.
342+
The output is exactly the same as the one used by all the assertion errors, therefore
343+
it is internally using the Myer's diff algorithm to compute the differences.
344+
345+
```js
346+
const { diff } = require('node:util');
347+
348+
console.log(diff({ a: 1, b: 2 }, { a: 1, b: 3 }));
349+
// above will log:
350+
/*
351+
+ actual - expected
352+
353+
{
354+
a: 1,
355+
+ b: 2
356+
- b: 3
357+
}
358+
*/
359+
360+
// if the sum of the strings length is less than 12 characters, a short diff will be returned
361+
console.log(diff('foo', 'bar'));
362+
// above will log:
363+
/*
364+
'foo' !== 'bar'
365+
*/
366+
367+
// if the string comparison is too long and the first difference is after the second character
368+
// the diff will also indicate with the ^ character where the difference is.
369+
// this is only happening when the terminal you are using does not support colors;
370+
// if the terminal supports colors, the diff will be colored and the ^ character will not be used.
371+
console.log(diff('123456789ABCDEFGHI', '12!!5!7!9!BC!!!GHI'));
372+
// above will log:
373+
/*
374+
+ actual - expected
375+
376+
+ '123456789ABCDEFGHI'
377+
- '12!!5!7!9!BC!!!GHI'
378+
^
379+
*/
380+
```
381+
328382
## `util.format(format[, ...args])`
329383

330384
<!-- YAML
@@ -3622,6 +3676,7 @@ util.isArray({});
36223676
[`napi_create_external()`]: n-api.md#napi_create_external
36233677
[`target` and `handler`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Terminology
36243678
[`tty.hasColors()`]: tty.md#writestreamhascolorscount-env
3679+
[`util.diff()`]: #utildiffactual-expected
36253680
[`util.format()`]: #utilformatformat-args
36263681
[`util.inspect()`]: #utilinspectobject-options
36273682
[`util.promisify()`]: #utilpromisifyoriginal

lib/internal/assert/assertion_error.js

Lines changed: 6 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,29 @@
33
const {
44
ArrayPrototypeJoin,
55
ArrayPrototypePop,
6-
ArrayPrototypeSlice,
76
Error,
87
ErrorCaptureStackTrace,
98
ObjectAssign,
109
ObjectDefineProperty,
1110
ObjectGetPrototypeOf,
1211
ObjectPrototypeHasOwnProperty,
1312
String,
14-
StringPrototypeRepeat,
1513
StringPrototypeSlice,
1614
StringPrototypeSplit,
1715
} = primordials;
1816

1917
const { isError } = require('internal/util');
18+
const { diff } = require('util');
2019

2120
const { inspect } = require('internal/util/inspect');
2221
const colors = require('internal/util/colors');
22+
const {
23+
inspectValue,
24+
} = require('internal/util/diff');
2325
const { validateObject } = require('internal/validators');
2426
const { isErrorStackTraceLimitWritable } = require('internal/errors');
25-
const { myersDiff, printMyersDiff, printSimpleMyersDiff } = require('internal/assert/myers_diff');
26-
27-
const kReadableOperator = {
28-
deepStrictEqual: 'Expected values to be strictly deep-equal:',
29-
partialDeepStrictEqual: 'Expected values to be partially and strictly deep-equal:',
30-
strictEqual: 'Expected values to be strictly equal:',
31-
strictEqualObject: 'Expected "actual" to be reference-equal to "expected":',
32-
deepEqual: 'Expected values to be loosely deep-equal:',
33-
notDeepStrictEqual: 'Expected "actual" not to be strictly deep-equal to:',
34-
notStrictEqual: 'Expected "actual" to be strictly unequal to:',
35-
notStrictEqualObject:
36-
'Expected "actual" not to be reference-equal to "expected":',
37-
notDeepEqual: 'Expected "actual" not to be loosely deep-equal to:',
38-
notIdentical: 'Values have same structure but are not reference-equal:',
39-
notDeepEqualUnequal: 'Expected values not to be loosely deep-equal:',
40-
};
27+
const { kReadableOperator } = require('internal/assert/consts');
4128

42-
const kMaxShortStringLength = 12;
4329
const kMaxLongStringLength = 512;
4430

4531
const kMethodsWithCustomMessageDiff = ['deepStrictEqual', 'strictEqual', 'partialDeepStrictEqual'];
@@ -65,28 +51,6 @@ function copyError(source) {
6551
return target;
6652
}
6753

68-
function inspectValue(val) {
69-
// The util.inspect default values could be changed. This makes sure the
70-
// error messages contain the necessary information nevertheless.
71-
return inspect(val, {
72-
compact: false,
73-
customInspect: false,
74-
depth: 1000,
75-
maxArrayLength: Infinity,
76-
// Assert compares only enumerable properties (with a few exceptions).
77-
showHidden: false,
78-
// Assert does not detect proxies currently.
79-
showProxy: false,
80-
sorted: true,
81-
// Inspect getters as we also check them when comparing entries.
82-
getters: true,
83-
});
84-
}
85-
86-
function getErrorMessage(operator, message) {
87-
return message || kReadableOperator[operator];
88-
}
89-
9054
function checkOperator(actual, expected, operator) {
9155
// In case both values are objects or functions explicitly mark them as not
9256
// reference equal for the `strictEqual` operator.
@@ -104,131 +68,10 @@ function checkOperator(actual, expected, operator) {
10468
return operator;
10569
}
10670

107-
function getColoredMyersDiff(actual, expected) {
108-
const header = `${colors.green}actual${colors.white} ${colors.red}expected${colors.white}`;
109-
const skipped = false;
110-
111-
const diff = myersDiff(StringPrototypeSplit(actual, ''), StringPrototypeSplit(expected, ''));
112-
let message = printSimpleMyersDiff(diff);
113-
114-
if (skipped) {
115-
message += '...';
116-
}
117-
118-
return { message, header, skipped };
119-
}
120-
121-
function getStackedDiff(actual, expected) {
122-
const isStringComparison = typeof actual === 'string' && typeof expected === 'string';
123-
124-
let message = `\n${colors.green}+${colors.white} ${actual}\n${colors.red}- ${colors.white}${expected}`;
125-
const stringsLen = actual.length + expected.length;
126-
const maxTerminalLength = process.stderr.isTTY ? process.stderr.columns : 80;
127-
const showIndicator = isStringComparison && (stringsLen <= maxTerminalLength);
128-
129-
if (showIndicator) {
130-
let indicatorIdx = -1;
131-
132-
for (let i = 0; i < actual.length; i++) {
133-
if (actual[i] !== expected[i]) {
134-
// Skip the indicator for the first 2 characters because the diff is immediately apparent
135-
// It is 3 instead of 2 to account for the quotes
136-
if (i >= 3) {
137-
indicatorIdx = i;
138-
}
139-
break;
140-
}
141-
}
142-
143-
if (indicatorIdx !== -1) {
144-
message += `\n${StringPrototypeRepeat(' ', indicatorIdx + 2)}^`;
145-
}
146-
}
147-
148-
return { message };
149-
}
150-
151-
function getSimpleDiff(originalActual, actual, originalExpected, expected) {
152-
let stringsLen = actual.length + expected.length;
153-
// Accounting for the quotes wrapping strings
154-
if (typeof originalActual === 'string') {
155-
stringsLen -= 2;
156-
}
157-
if (typeof originalExpected === 'string') {
158-
stringsLen -= 2;
159-
}
160-
if (stringsLen <= kMaxShortStringLength && (originalActual !== 0 || originalExpected !== 0)) {
161-
return { message: `${actual} !== ${expected}`, header: '' };
162-
}
163-
164-
const isStringComparison = typeof originalActual === 'string' && typeof originalExpected === 'string';
165-
// colored myers diff
166-
if (isStringComparison && colors.hasColors) {
167-
return getColoredMyersDiff(actual, expected);
168-
}
169-
170-
return getStackedDiff(actual, expected);
171-
}
172-
173-
function isSimpleDiff(actual, inspectedActual, expected, inspectedExpected) {
174-
if (inspectedActual.length > 1 || inspectedExpected.length > 1) {
175-
return false;
176-
}
177-
178-
return typeof actual !== 'object' || actual === null || typeof expected !== 'object' || expected === null;
179-
}
180-
18171
function createErrDiff(actual, expected, operator, customMessage) {
18272
operator = checkOperator(actual, expected, operator);
18373

184-
let skipped = false;
185-
let message = '';
186-
const inspectedActual = inspectValue(actual);
187-
const inspectedExpected = inspectValue(expected);
188-
const inspectedSplitActual = StringPrototypeSplit(inspectedActual, '\n');
189-
const inspectedSplitExpected = StringPrototypeSplit(inspectedExpected, '\n');
190-
const showSimpleDiff = isSimpleDiff(actual, inspectedSplitActual, expected, inspectedSplitExpected);
191-
let header = `${colors.green}+ actual${colors.white} ${colors.red}- expected${colors.white}`;
192-
193-
if (showSimpleDiff) {
194-
const simpleDiff = getSimpleDiff(actual, inspectedSplitActual[0], expected, inspectedSplitExpected[0]);
195-
message = simpleDiff.message;
196-
if (typeof simpleDiff.header !== 'undefined') {
197-
header = simpleDiff.header;
198-
}
199-
if (simpleDiff.skipped) {
200-
skipped = true;
201-
}
202-
} else if (inspectedActual === inspectedExpected) {
203-
// Handles the case where the objects are structurally the same but different references
204-
operator = 'notIdentical';
205-
if (inspectedSplitActual.length > 50) {
206-
message = `${ArrayPrototypeJoin(ArrayPrototypeSlice(inspectedSplitActual, 0, 50), '\n')}\n...}`;
207-
skipped = true;
208-
} else {
209-
message = ArrayPrototypeJoin(inspectedSplitActual, '\n');
210-
}
211-
header = '';
212-
} else {
213-
const checkCommaDisparity = actual != null && typeof actual === 'object';
214-
const diff = myersDiff(inspectedSplitActual, inspectedSplitExpected, checkCommaDisparity);
215-
216-
const myersDiffMessage = printMyersDiff(diff, operator);
217-
message = myersDiffMessage.message;
218-
219-
if (operator === 'partialDeepStrictEqual') {
220-
header = `${colors.gray}${colors.hasColors ? '' : '+ '}actual${colors.white} ${colors.red}- expected${colors.white}`;
221-
}
222-
223-
if (myersDiffMessage.skipped) {
224-
skipped = true;
225-
}
226-
}
227-
228-
const headerMessage = `${getErrorMessage(operator, customMessage)}\n${header}`;
229-
const skippedMessage = skipped ? '\n... Skipped lines' : '';
230-
231-
return `${headerMessage}${skippedMessage}\n${message}\n`;
74+
return diff(actual, expected, operator, customMessage);
23275
}
23376

23477
function addEllipsis(string) {

lib/internal/assert/consts.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict';
2+
3+
const kReadableOperator = {
4+
deepStrictEqual: 'Expected values to be strictly deep-equal:',
5+
partialDeepStrictEqual: 'Expected values to be partially and strictly deep-equal:',
6+
strictEqual: 'Expected values to be strictly equal:',
7+
strictEqualObject: 'Expected "actual" to be reference-equal to "expected":',
8+
deepEqual: 'Expected values to be loosely deep-equal:',
9+
notDeepStrictEqual: 'Expected "actual" not to be strictly deep-equal to:',
10+
notStrictEqual: 'Expected "actual" to be strictly unequal to:',
11+
notStrictEqualObject:
12+
'Expected "actual" not to be reference-equal to "expected":',
13+
notDeepEqual: 'Expected "actual" not to be loosely deep-equal to:',
14+
notIdentical: 'Values have same structure but are not reference-equal:',
15+
notDeepEqualUnequal: 'Expected values not to be loosely deep-equal:',
16+
};
17+
18+
module.exports = {
19+
kReadableOperator,
20+
};

0 commit comments

Comments
 (0)