mirror of
https://github.com/nodejs/node.git
synced 2025-04-28 13:40:37 +00:00
util: expose diff function used by the assertion errors
Some checks are pending
Coverage Linux (without intl) / coverage-linux-without-intl (push) Waiting to run
Coverage Linux / coverage-linux (push) Waiting to run
Coverage Windows / coverage-windows (push) Waiting to run
Test and upload documentation to artifacts / build-docs (push) Waiting to run
Linters / lint-addon-docs (push) Waiting to run
Linters / lint-cpp (push) Waiting to run
Linters / format-cpp (push) Waiting to run
Linters / lint-js-and-md (push) Waiting to run
Linters / lint-py (push) Waiting to run
Linters / lint-yaml (push) Waiting to run
Linters / lint-sh (push) Waiting to run
Linters / lint-codeowners (push) Waiting to run
Linters / lint-pr-url (push) Waiting to run
Linters / lint-readme (push) Waiting to run
Notify on Push / Notify on Force Push on `main` (push) Waiting to run
Notify on Push / Notify on Push on `main` that lacks metadata (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Some checks are pending
Coverage Linux (without intl) / coverage-linux-without-intl (push) Waiting to run
Coverage Linux / coverage-linux (push) Waiting to run
Coverage Windows / coverage-windows (push) Waiting to run
Test and upload documentation to artifacts / build-docs (push) Waiting to run
Linters / lint-addon-docs (push) Waiting to run
Linters / lint-cpp (push) Waiting to run
Linters / format-cpp (push) Waiting to run
Linters / lint-js-and-md (push) Waiting to run
Linters / lint-py (push) Waiting to run
Linters / lint-yaml (push) Waiting to run
Linters / lint-sh (push) Waiting to run
Linters / lint-codeowners (push) Waiting to run
Linters / lint-pr-url (push) Waiting to run
Linters / lint-readme (push) Waiting to run
Notify on Push / Notify on Force Push on `main` (push) Waiting to run
Notify on Push / Notify on Push on `main` that lacks metadata (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
fix: https://github.com/nodejs/node/issues/51740 PR-URL: https://github.com/nodejs/node/pull/57462 Fixes: https://github.com/nodejs/node/issues/51740 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
This commit is contained in:
parent
1fbe3351ba
commit
6b42554342
43
benchmark/util/diff.js
Normal file
43
benchmark/util/diff.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const util = require('util');
|
||||||
|
const common = require('../common');
|
||||||
|
|
||||||
|
const bench = common.createBenchmark(main, {
|
||||||
|
n: [1e3],
|
||||||
|
length: [1e3, 2e3],
|
||||||
|
scenario: ['identical', 'small-diff', 'medium-diff', 'large-diff'],
|
||||||
|
});
|
||||||
|
|
||||||
|
function main({ n, length, scenario }) {
|
||||||
|
const actual = Array.from({ length }, (_, i) => `${i}`);
|
||||||
|
let expected;
|
||||||
|
|
||||||
|
switch (scenario) {
|
||||||
|
case 'identical': // 0% difference
|
||||||
|
expected = Array.from({ length }, (_, i) => `${i}`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'small-diff': // ~5% difference
|
||||||
|
expected = Array.from({ length }, (_, i) => {
|
||||||
|
return Math.random() < 0.05 ? `modified-${i}` : `${i}`;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'medium-diff': // ~25% difference
|
||||||
|
expected = Array.from({ length }, (_, i) => {
|
||||||
|
return Math.random() < 0.25 ? `modified-${i}` : `${i}`;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'large-diff': // ~100% difference
|
||||||
|
expected = Array.from({ length }, (_, i) => `modified-${i}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bench.start();
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
util.diff(actual, expected);
|
||||||
|
}
|
||||||
|
bench.end(n);
|
||||||
|
}
|
@ -325,6 +325,70 @@ The `--throw-deprecation` command-line flag and `process.throwDeprecation`
|
|||||||
property take precedence over `--trace-deprecation` and
|
property take precedence over `--trace-deprecation` and
|
||||||
`process.traceDeprecation`.
|
`process.traceDeprecation`.
|
||||||
|
|
||||||
|
## `util.diff(actual, expected)`
|
||||||
|
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
> Stability: 1 - Experimental
|
||||||
|
|
||||||
|
* `actual` {Array|string} The first value to compare
|
||||||
|
|
||||||
|
* `expected` {Array|string} The second value to compare
|
||||||
|
|
||||||
|
* Returns: {Array} An array of difference entries. Each entry is an array with two elements:
|
||||||
|
* Index 0: {number} Operation code: `-1` for delete, `0` for no-op/unchanged, `1` for insert
|
||||||
|
* Index 1: {string} The value associated with the operation
|
||||||
|
|
||||||
|
* Algorithm complexity: O(N\*D), where:
|
||||||
|
|
||||||
|
* N is the total length of the two sequences combined (N = actual.length + expected.length)
|
||||||
|
|
||||||
|
* D is the edit distance (the minimum number of operations required to transform one sequence into the other).
|
||||||
|
|
||||||
|
[`util.diff()`][] compares two string or array values and returns an array of difference entries.
|
||||||
|
It uses the Myers diff algorithm to compute minimal differences, which is the same algorithm
|
||||||
|
used internally by assertion error messages.
|
||||||
|
|
||||||
|
If the values are equal, an empty array is returned.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { diff } = require('node:util');
|
||||||
|
|
||||||
|
// Comparing strings
|
||||||
|
const actualString = '12345678';
|
||||||
|
const expectedString = '12!!5!7!';
|
||||||
|
console.log(diff(actualString, expectedString));
|
||||||
|
// [
|
||||||
|
// [0, '1'],
|
||||||
|
// [0, '2'],
|
||||||
|
// [1, '3'],
|
||||||
|
// [1, '4'],
|
||||||
|
// [-1, '!'],
|
||||||
|
// [-1, '!'],
|
||||||
|
// [0, '5'],
|
||||||
|
// [1, '6'],
|
||||||
|
// [-1, '!'],
|
||||||
|
// [0, '7'],
|
||||||
|
// [1, '8'],
|
||||||
|
// [-1, '!'],
|
||||||
|
// ]
|
||||||
|
// Comparing arrays
|
||||||
|
const actualArray = ['1', '2', '3'];
|
||||||
|
const expectedArray = ['1', '3', '4'];
|
||||||
|
console.log(diff(actualArray, expectedArray));
|
||||||
|
// [
|
||||||
|
// [0, '1'],
|
||||||
|
// [1, '2'],
|
||||||
|
// [0, '3'],
|
||||||
|
// [-1, '4'],
|
||||||
|
// ]
|
||||||
|
// Equal values return empty array
|
||||||
|
console.log(diff('same', 'same'));
|
||||||
|
// []
|
||||||
|
```
|
||||||
|
|
||||||
## `util.format(format[, ...args])`
|
## `util.format(format[, ...args])`
|
||||||
|
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
@ -3622,6 +3686,7 @@ util.isArray({});
|
|||||||
[`napi_create_external()`]: n-api.md#napi_create_external
|
[`napi_create_external()`]: n-api.md#napi_create_external
|
||||||
[`target` and `handler`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Terminology
|
[`target` and `handler`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Terminology
|
||||||
[`tty.hasColors()`]: tty.md#writestreamhascolorscount-env
|
[`tty.hasColors()`]: tty.md#writestreamhascolorscount-env
|
||||||
|
[`util.diff()`]: #utildiffactual-expected
|
||||||
[`util.format()`]: #utilformatformat-args
|
[`util.format()`]: #utilformatformat-args
|
||||||
[`util.inspect()`]: #utilinspectobject-options
|
[`util.inspect()`]: #utilinspectobject-options
|
||||||
[`util.promisify()`]: #utilpromisifyoriginal
|
[`util.promisify()`]: #utilpromisifyoriginal
|
||||||
|
@ -9,6 +9,11 @@ const {
|
|||||||
const colors = require('internal/util/colors');
|
const colors = require('internal/util/colors');
|
||||||
|
|
||||||
const kNopLinesToCollapse = 5;
|
const kNopLinesToCollapse = 5;
|
||||||
|
const kOperations = {
|
||||||
|
DELETE: -1,
|
||||||
|
NOP: 0,
|
||||||
|
INSERT: 1,
|
||||||
|
};
|
||||||
|
|
||||||
function areLinesEqual(actual, expected, checkCommaDisparity) {
|
function areLinesEqual(actual, expected, checkCommaDisparity) {
|
||||||
if (actual === expected) {
|
if (actual === expected) {
|
||||||
@ -87,16 +92,16 @@ function backtrack(trace, actual, expected, checkCommaDisparity) {
|
|||||||
while (x > prevX && y > prevY) {
|
while (x > prevX && y > prevY) {
|
||||||
const actualItem = actual[x - 1];
|
const actualItem = actual[x - 1];
|
||||||
const value = checkCommaDisparity && !StringPrototypeEndsWith(actualItem, ',') ? expected[y - 1] : actualItem;
|
const value = checkCommaDisparity && !StringPrototypeEndsWith(actualItem, ',') ? expected[y - 1] : actualItem;
|
||||||
ArrayPrototypePush(result, { __proto__: null, type: 'nop', value });
|
ArrayPrototypePush(result, [ kOperations.NOP, value ]);
|
||||||
x--;
|
x--;
|
||||||
y--;
|
y--;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (diffLevel > 0) {
|
if (diffLevel > 0) {
|
||||||
if (x > prevX) {
|
if (x > prevX) {
|
||||||
ArrayPrototypePush(result, { __proto__: null, type: 'insert', value: actual[--x] });
|
ArrayPrototypePush(result, [ kOperations.INSERT, actual[--x] ]);
|
||||||
} else {
|
} else {
|
||||||
ArrayPrototypePush(result, { __proto__: null, type: 'delete', value: expected[--y] });
|
ArrayPrototypePush(result, [ kOperations.DELETE, expected[--y] ]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,12 +113,12 @@ function printSimpleMyersDiff(diff) {
|
|||||||
let message = '';
|
let message = '';
|
||||||
|
|
||||||
for (let diffIdx = diff.length - 1; diffIdx >= 0; diffIdx--) {
|
for (let diffIdx = diff.length - 1; diffIdx >= 0; diffIdx--) {
|
||||||
const { type, value } = diff[diffIdx];
|
const { 0: operation, 1: value } = diff[diffIdx];
|
||||||
let color = colors.white;
|
let color = colors.white;
|
||||||
|
|
||||||
if (type === 'insert') {
|
if (operation === kOperations.INSERT) {
|
||||||
color = colors.green;
|
color = colors.green;
|
||||||
} else if (type === 'delete') {
|
} else if (operation === kOperations.DELETE) {
|
||||||
color = colors.red;
|
color = colors.red;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,33 +134,33 @@ function printMyersDiff(diff, operator) {
|
|||||||
let nopCount = 0;
|
let nopCount = 0;
|
||||||
|
|
||||||
for (let diffIdx = diff.length - 1; diffIdx >= 0; diffIdx--) {
|
for (let diffIdx = diff.length - 1; diffIdx >= 0; diffIdx--) {
|
||||||
const { type, value } = diff[diffIdx];
|
const { 0: operation, 1: value } = diff[diffIdx];
|
||||||
const previousType = diffIdx < diff.length - 1 ? diff[diffIdx + 1].type : null;
|
const previousOperation = diffIdx < diff.length - 1 ? diff[diffIdx + 1][0] : null;
|
||||||
|
|
||||||
// Avoid grouping if only one line would have been grouped otherwise
|
// Avoid grouping if only one line would have been grouped otherwise
|
||||||
if (previousType === 'nop' && type !== previousType) {
|
if (previousOperation === kOperations.NOP && operation !== previousOperation) {
|
||||||
if (nopCount === kNopLinesToCollapse + 1) {
|
if (nopCount === kNopLinesToCollapse + 1) {
|
||||||
message += `${colors.white} ${diff[diffIdx + 1].value}\n`;
|
message += `${colors.white} ${diff[diffIdx + 1][1]}\n`;
|
||||||
} else if (nopCount === kNopLinesToCollapse + 2) {
|
} else if (nopCount === kNopLinesToCollapse + 2) {
|
||||||
message += `${colors.white} ${diff[diffIdx + 2].value}\n`;
|
message += `${colors.white} ${diff[diffIdx + 2][1]}\n`;
|
||||||
message += `${colors.white} ${diff[diffIdx + 1].value}\n`;
|
message += `${colors.white} ${diff[diffIdx + 1][1]}\n`;
|
||||||
} else if (nopCount >= kNopLinesToCollapse + 3) {
|
} else if (nopCount >= kNopLinesToCollapse + 3) {
|
||||||
message += `${colors.blue}...${colors.white}\n`;
|
message += `${colors.blue}...${colors.white}\n`;
|
||||||
message += `${colors.white} ${diff[diffIdx + 1].value}\n`;
|
message += `${colors.white} ${diff[diffIdx + 1][1]}\n`;
|
||||||
skipped = true;
|
skipped = true;
|
||||||
}
|
}
|
||||||
nopCount = 0;
|
nopCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'insert') {
|
if (operation === kOperations.INSERT) {
|
||||||
if (operator === 'partialDeepStrictEqual') {
|
if (operator === 'partialDeepStrictEqual') {
|
||||||
message += `${colors.gray}${colors.hasColors ? ' ' : '+'} ${value}${colors.white}\n`;
|
message += `${colors.gray}${colors.hasColors ? ' ' : '+'} ${value}${colors.white}\n`;
|
||||||
} else {
|
} else {
|
||||||
message += `${colors.green}+${colors.white} ${value}\n`;
|
message += `${colors.green}+${colors.white} ${value}\n`;
|
||||||
}
|
}
|
||||||
} else if (type === 'delete') {
|
} else if (operation === kOperations.DELETE) {
|
||||||
message += `${colors.red}-${colors.white} ${value}\n`;
|
message += `${colors.red}-${colors.white} ${value}\n`;
|
||||||
} else if (type === 'nop') {
|
} else if (operation === kOperations.NOP) {
|
||||||
if (nopCount < kNopLinesToCollapse) {
|
if (nopCount < kNopLinesToCollapse) {
|
||||||
message += `${colors.white} ${value}\n`;
|
message += `${colors.white} ${value}\n`;
|
||||||
}
|
}
|
||||||
|
42
lib/internal/util/diff.js
Normal file
42
lib/internal/util/diff.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const {
|
||||||
|
ArrayIsArray,
|
||||||
|
ArrayPrototypeReverse,
|
||||||
|
} = primordials;
|
||||||
|
|
||||||
|
const { validateStringArray, validateString } = require('internal/validators');
|
||||||
|
const { myersDiff } = require('internal/assert/myers_diff');
|
||||||
|
|
||||||
|
function validateInput(value, name) {
|
||||||
|
if (!ArrayIsArray(value)) {
|
||||||
|
validateString(value, name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateStringArray(value, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a difference report between two values
|
||||||
|
* @param {Array | string} actual - The first value to compare
|
||||||
|
* @param {Array | string} expected - The second value to compare
|
||||||
|
* @returns {Array} - An array of differences between the two values.
|
||||||
|
* The returned data is an array of arrays, where each sub-array has two elements:
|
||||||
|
* 1. The operation to perform: -1 for delete, 0 for no-op, 1 for insert
|
||||||
|
* 2. The value to perform the operation on
|
||||||
|
*/
|
||||||
|
function diff(actual, expected) {
|
||||||
|
if (actual === expected) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
validateInput(actual, 'actual');
|
||||||
|
validateInput(expected, 'expected');
|
||||||
|
|
||||||
|
return ArrayPrototypeReverse(myersDiff(actual, expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
diff,
|
||||||
|
};
|
@ -483,3 +483,9 @@ defineLazyProperties(
|
|||||||
'internal/mime',
|
'internal/mime',
|
||||||
['MIMEType', 'MIMEParams'],
|
['MIMEType', 'MIMEParams'],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
defineLazyProperties(
|
||||||
|
module.exports,
|
||||||
|
'internal/util/diff',
|
||||||
|
['diff'],
|
||||||
|
);
|
||||||
|
80
test/parallel/test-diff.js
Normal file
80
test/parallel/test-diff.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
'use strict';
|
||||||
|
require('../common');
|
||||||
|
|
||||||
|
const { describe, it } = require('node:test');
|
||||||
|
const { deepStrictEqual, throws } = require('node:assert');
|
||||||
|
|
||||||
|
const { diff } = require('util');
|
||||||
|
|
||||||
|
describe('diff', () => {
|
||||||
|
it('throws because actual is nor an array nor a string', () => {
|
||||||
|
const actual = {};
|
||||||
|
const expected = 'foo';
|
||||||
|
|
||||||
|
throws(() => diff(actual, expected), {
|
||||||
|
message: 'The "actual" argument must be of type string. Received an instance of Object'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws because expected is nor an array nor a string', () => {
|
||||||
|
const actual = 'foo';
|
||||||
|
const expected = {};
|
||||||
|
|
||||||
|
throws(() => diff(actual, expected), {
|
||||||
|
message: 'The "expected" argument must be of type string. Received an instance of Object'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('throws because the actual array does not only contain string', () => {
|
||||||
|
const actual = ['1', { b: 2 }];
|
||||||
|
const expected = ['1', '2'];
|
||||||
|
|
||||||
|
throws(() => diff(actual, expected), {
|
||||||
|
message: 'The "actual[1]" argument must be of type string. Received an instance of Object'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty array because actual and expected are the same', () => {
|
||||||
|
const actual = 'foo';
|
||||||
|
const expected = 'foo';
|
||||||
|
|
||||||
|
const result = diff(actual, expected);
|
||||||
|
deepStrictEqual(result, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the diff for strings', () => {
|
||||||
|
const actual = '12345678';
|
||||||
|
const expected = '12!!5!7!';
|
||||||
|
const result = diff(actual, expected);
|
||||||
|
|
||||||
|
deepStrictEqual(result, [
|
||||||
|
[0, '1'],
|
||||||
|
[0, '2'],
|
||||||
|
[1, '3'],
|
||||||
|
[1, '4'],
|
||||||
|
[-1, '!'],
|
||||||
|
[-1, '!'],
|
||||||
|
[0, '5'],
|
||||||
|
[1, '6'],
|
||||||
|
[-1, '!'],
|
||||||
|
[0, '7'],
|
||||||
|
[1, '8'],
|
||||||
|
[-1, '!'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the diff for arrays', () => {
|
||||||
|
const actual = ['1', '2', '3'];
|
||||||
|
const expected = ['1', '3', '4'];
|
||||||
|
const result = diff(actual, expected);
|
||||||
|
|
||||||
|
deepStrictEqual(result, [
|
||||||
|
[0, '1'],
|
||||||
|
[1, '2'],
|
||||||
|
[0, '3'],
|
||||||
|
[-1, '4'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user