GeezerVison is showing github.com — assert: implement partial error comparison · nodejs/node@64b0867
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: lib/internal/util/comparisons.js +44-61Lines changed: 44 additions & 61 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -10,17 +10,17 @@ const { | 10 | 10 | Error, | 11 | 11 | NumberIsNaN, | 12 | 12 | NumberPrototypeValueOf, | 13 | | - ObjectGetOwnPropertySymbols, | | 13 | + ObjectGetOwnPropertySymbols: getOwnSymbols, | 14 | 14 | ObjectGetPrototypeOf, | 15 | 15 | ObjectIs, | 16 | 16 | ObjectKeys, | 17 | | - ObjectPrototypeHasOwnProperty, | 18 | | - ObjectPrototypePropertyIsEnumerable, | | 17 | + ObjectPrototypeHasOwnProperty: hasOwn, | | 18 | + ObjectPrototypePropertyIsEnumerable: hasEnumerable, | 19 | 19 | ObjectPrototypeToString, | 20 | 20 | SafeSet, | 21 | 21 | StringPrototypeValueOf, | 22 | 22 | SymbolPrototypeValueOf, | 23 | | - TypedArrayPrototypeGetByteLength, | | 23 | + TypedArrayPrototypeGetByteLength: getByteLength, | 24 | 24 | TypedArrayPrototypeGetSymbolToStringTag, | 25 | 25 | Uint8Array, | 26 | 26 | } = primordials; | @@ -78,8 +78,8 @@ function areSimilarRegExps(a, b) { | 78 | 78 | } | 79 | 79 |
| 80 | 80 | function isPartialUint8Array(a, b) { | 81 | | - const lenA = TypedArrayPrototypeGetByteLength(a); | 82 | | - const lenB = TypedArrayPrototypeGetByteLength(b); | | 81 | + const lenA = getByteLength(a); | | 82 | + const lenB = getByteLength(b); | 83 | 83 | if (lenA < lenB) { | 84 | 84 | return false; | 85 | 85 | } | @@ -107,10 +107,10 @@ function isPartialArrayBufferView(a, b) { | 107 | 107 | } | 108 | 108 |
| 109 | 109 | function areSimilarFloatArrays(a, b) { | 110 | | - if (a.byteLength !== b.byteLength) { | | 110 | + const len = getByteLength(a); | | 111 | + if (len !== getByteLength(b)) { | 111 | 112 | return false; | 112 | 113 | } | 113 | | - const len = TypedArrayPrototypeGetByteLength(a); | 114 | 114 | for (let offset = 0; offset < len; offset++) { | 115 | 115 | if (a[offset] !== b[offset]) { | 116 | 116 | return false; | @@ -158,6 +158,12 @@ function isEqualBoxedPrimitive(val1, val2) { | 158 | 158 | assert.fail(`Unknown boxed type ${val1}`); | 159 | 159 | } | 160 | 160 |
| | 161 | +function isEnumerableOrIdentical(val1, val2, prop, mode, memos, method) { | | 162 | + return hasEnumerable(val2, prop) || // This is handled by Object.keys() | | 163 | + (mode === kPartial && (val2[prop] === undefined || (prop === 'message' && val2[prop] === ''))) || | | 164 | + innerDeepEqual(val1[prop], val2[prop], mode, memos); | | 165 | +} | | 166 | + | 161 | 167 | // Notes: Type tags are historical [[Class]] properties that can be set by | 162 | 168 | // FunctionTemplate::SetClassName() in C++ or Symbol.toStringTag in JS | 163 | 169 | // and retrieved using Object.prototype.toString.call(obj) in JS | @@ -263,8 +269,7 @@ function innerDeepEqual(val1, val2, mode, memos) { | 263 | 269 | (val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size))) { | 264 | 270 | return false; | 265 | 271 | } | 266 | | - const result = keyCheck(val1, val2, mode, memos, kIsSet); | 267 | | - return result; | | 272 | + return keyCheck(val1, val2, mode, memos, kIsSet); | 268 | 273 | } else if (isMap(val1)) { | 269 | 274 | if (!isMap(val2) || | 270 | 275 | (val1.size !== val2.size && (mode !== kPartial || val1.size < val2.size))) { | @@ -285,26 +290,15 @@ function innerDeepEqual(val1, val2, mode, memos) { | 285 | 290 | } else if (isError(val1)) { | 286 | 291 | // Do not compare the stack as it might differ even though the error itself | 287 | 292 | // is otherwise identical. | 288 | | - if (!isError(val2)) { | | 293 | + if (!isError(val2) || | | 294 | + !isEnumerableOrIdentical(val1, val2, 'message', mode, memos) || | | 295 | + !isEnumerableOrIdentical(val1, val2, 'name', mode, memos) || | | 296 | + !isEnumerableOrIdentical(val1, val2, 'cause', mode, memos) || | | 297 | + !isEnumerableOrIdentical(val1, val2, 'errors', mode, memos)) { | 289 | 298 | return false; | 290 | 299 | } | 291 | | - | 292 | | - const message1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'message'); | 293 | | - const name1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'name'); | 294 | | - // TODO(BridgeAR): Adjust cause and errors properties for partial mode. | 295 | | - const cause1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'cause'); | 296 | | - const errors1Enumerable = ObjectPrototypePropertyIsEnumerable(val1, 'errors'); | 297 | | - | 298 | | - if ((message1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'message') || | 299 | | - (!message1Enumerable && val1.message !== val2.message)) || | 300 | | - (name1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'name') || | 301 | | - (!name1Enumerable && val1.name !== val2.name)) || | 302 | | - (cause1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'cause') || | 303 | | - (!cause1Enumerable && ( | 304 | | - ObjectPrototypeHasOwnProperty(val1, 'cause') !== ObjectPrototypeHasOwnProperty(val2, 'cause') || | 305 | | - !innerDeepEqual(val1.cause, val2.cause, mode, memos)))) || | 306 | | - (errors1Enumerable !== ObjectPrototypePropertyIsEnumerable(val2, 'errors') || | 307 | | - (!errors1Enumerable && !innerDeepEqual(val1.errors, val2.errors, mode, memos)))) { | | 300 | + const hasOwnVal2Cause = hasOwn(val2, 'cause'); | | 301 | + if ((hasOwnVal2Cause !== hasOwn(val1, 'cause') && (mode !== kPartial || hasOwnVal2Cause))) { | 308 | 302 | return false; | 309 | 303 | } | 310 | 304 | } else if (isBoxedPrimitive(val1)) { | @@ -348,10 +342,7 @@ function innerDeepEqual(val1, val2, mode, memos) { | 348 | 342 | } | 349 | 343 |
| 350 | 344 | function getEnumerables(val, keys) { | 351 | | - return ArrayPrototypeFilter( | 352 | | - keys, | 353 | | - (k) => ObjectPrototypePropertyIsEnumerable(val, k), | 354 | | - ); | | 345 | + return ArrayPrototypeFilter(keys, (key) => hasEnumerable(val, key)); | 355 | 346 | } | 356 | 347 |
| 357 | 348 | function keyCheck(val1, val2, mode, memos, iterationType, keys2) { | @@ -371,7 +362,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) { | 371 | 362 | // Cheap key test | 372 | 363 | if (keys2.length > 0) { | 373 | 364 | for (const key of keys2) { | 374 | | - if (!ObjectPrototypePropertyIsEnumerable(val1, key)) { | | 365 | + if (!hasEnumerable(val1, key)) { | 375 | 366 | return false; | 376 | 367 | } | 377 | 368 | } | @@ -380,11 +371,11 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) { | 380 | 371 | if (!isArrayLikeObject) { | 381 | 372 | // The pair must have the same number of owned properties. | 382 | 373 | if (mode === kPartial) { | 383 | | - const symbolKeys = ObjectGetOwnPropertySymbols(val2); | | 374 | + const symbolKeys = getOwnSymbols(val2); | 384 | 375 | if (symbolKeys.length !== 0) { | 385 | 376 | for (const key of symbolKeys) { | 386 | | - if (ObjectPrototypePropertyIsEnumerable(val2, key)) { | 387 | | - if (!ObjectPrototypePropertyIsEnumerable(val1, key)) { | | 377 | + if (hasEnumerable(val2, key)) { | | 378 | + if (!hasEnumerable(val1, key)) { | 388 | 379 | return false; | 389 | 380 | } | 390 | 381 | ArrayPrototypePush(keys2, key); | @@ -396,27 +387,27 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) { | 396 | 387 | } | 397 | 388 |
| 398 | 389 | if (mode === kStrict) { | 399 | | - const symbolKeysA = ObjectGetOwnPropertySymbols(val1); | | 390 | + const symbolKeysA = getOwnSymbols(val1); | 400 | 391 | if (symbolKeysA.length !== 0) { | 401 | 392 | let count = 0; | 402 | 393 | for (const key of symbolKeysA) { | 403 | | - if (ObjectPrototypePropertyIsEnumerable(val1, key)) { | 404 | | - if (!ObjectPrototypePropertyIsEnumerable(val2, key)) { | | 394 | + if (hasEnumerable(val1, key)) { | | 395 | + if (!hasEnumerable(val2, key)) { | 405 | 396 | return false; | 406 | 397 | } | 407 | 398 | ArrayPrototypePush(keys2, key); | 408 | 399 | count++; | 409 | | - } else if (ObjectPrototypePropertyIsEnumerable(val2, key)) { | | 400 | + } else if (hasEnumerable(val2, key)) { | 410 | 401 | return false; | 411 | 402 | } | 412 | 403 | } | 413 | | - const symbolKeysB = ObjectGetOwnPropertySymbols(val2); | | 404 | + const symbolKeysB = getOwnSymbols(val2); | 414 | 405 | if (symbolKeysA.length !== symbolKeysB.length && | 415 | 406 | getEnumerables(val2, symbolKeysB).length !== count) { | 416 | 407 | return false; | 417 | 408 | } | 418 | 409 | } else { | 419 | | - const symbolKeysB = ObjectGetOwnPropertySymbols(val2); | | 410 | + const symbolKeysB = getOwnSymbols(val2); | 420 | 411 | if (symbolKeysB.length !== 0 && | 421 | 412 | getEnumerables(val2, symbolKeysB).length !== 0) { | 422 | 413 | return false; | @@ -441,7 +432,6 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) { | 441 | 432 | c: undefined, | 442 | 433 | d: undefined, | 443 | 434 | deep: false, | 444 | | - deleteFailures: false, | 445 | 435 | }; | 446 | 436 | return objEquiv(val1, val2, mode, keys2, memos, iterationType); | 447 | 437 | } | @@ -476,27 +466,21 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) { | 476 | 466 |
| 477 | 467 | const areEq = objEquiv(val1, val2, mode, keys2, memos, iterationType); | 478 | 468 |
| 479 | | - if (areEq || memos.deleteFailures) { | 480 | | - set.delete(val1); | 481 | | - set.delete(val2); | 482 | | - } | | 469 | + set.delete(val1); | | 470 | + set.delete(val2); | 483 | 471 |
| 484 | 472 | return areEq; | 485 | 473 | } | 486 | 474 |
| 487 | 475 | function setHasEqualElement(set, val1, mode, memo) { | 488 | | - const { deleteFailures } = memo; | 489 | | - memo.deleteFailures = true; | 490 | 476 | for (const val2 of set) { | 491 | 477 | if (innerDeepEqual(val1, val2, mode, memo)) { | 492 | 478 | // Remove the matching element to make sure we do not check that again. | 493 | 479 | set.delete(val2); | 494 | | - memo.deleteFailures = deleteFailures; | 495 | 480 | return true; | 496 | 481 | } | 497 | 482 | } | 498 | 483 |
| 499 | | - memo.deleteFailures = deleteFailures; | 500 | 484 | return false; | 501 | 485 | } | 502 | 486 |
| @@ -557,6 +541,8 @@ function partialObjectSetEquiv(a, b, mode, set, memo) { | 557 | 541 | return false; | 558 | 542 | } | 559 | 543 | } | | 544 | + /* c8 ignore next */ | | 545 | + assert.fail('Unreachable code'); | 560 | 546 | } | 561 | 547 |
| 562 | 548 | function setObjectEquiv(a, b, mode, set, memo) { | @@ -623,18 +609,14 @@ function mapHasEqualEntry(set, map, key1, item1, mode, memo) { | 623 | 609 | // To be able to handle cases like: | 624 | 610 | // Map([[{}, 'a'], [{}, 'b']]) vs Map([[{}, 'b'], [{}, 'a']]) | 625 | 611 | // ... we need to consider *all* matching keys, not just the first we find. | 626 | | - const { deleteFailures } = memo; | 627 | | - memo.deleteFailures = true; | 628 | 612 | for (const key2 of set) { | 629 | 613 | if (innerDeepEqual(key1, key2, mode, memo) && | 630 | 614 | innerDeepEqual(item1, map.get(key2), mode, memo)) { | 631 | 615 | set.delete(key2); | 632 | | - memo.deleteFailures = deleteFailures; | 633 | 616 | return true; | 634 | 617 | } | 635 | 618 | } | 636 | 619 |
| 637 | | - memo.deleteFailures = deleteFailures; | 638 | 620 | return false; | 639 | 621 | } | 640 | 622 |
| @@ -652,6 +634,8 @@ function partialObjectMapEquiv(a, b, mode, set, memo) { | 652 | 634 | return false; | 653 | 635 | } | 654 | 636 | } | | 637 | + /* c8 ignore next */ | | 638 | + assert.fail('Unreachable code'); | 655 | 639 | } | 656 | 640 |
| 657 | 641 | function mapObjectEquivalence(a, b, mode, set, memo) { | @@ -747,11 +731,11 @@ function partialSparseArrayEquiv(a, b, mode, memos, startA, startB) { | 747 | 731 | function partialArrayEquiv(a, b, mode, memos) { | 748 | 732 | let aPos = 0; | 749 | 733 | for (let i = 0; i < b.length; i++) { | 750 | | - let isSparse = b[i] === undefined && !ObjectPrototypeHasOwnProperty(b, i); | | 734 | + let isSparse = b[i] === undefined && !hasOwn(b, i); | 751 | 735 | if (isSparse) { | 752 | 736 | return partialSparseArrayEquiv(a, b, mode, memos, aPos, i); | 753 | 737 | } | 754 | | - while (!(isSparse = a[aPos] === undefined && !ObjectPrototypeHasOwnProperty(a, aPos)) && | | 738 | + while (!(isSparse = a[aPos] === undefined && !hasOwn(a, aPos)) && | 755 | 739 | !innerDeepEqual(a[aPos], b[i], mode, memos)) { | 756 | 740 | aPos++; | 757 | 741 | if (aPos > a.length - b.length + i) { | @@ -776,8 +760,7 @@ function sparseArrayEquiv(a, b, mode, memos, i) { | 776 | 760 | } | 777 | 761 | for (; i < keysA.length; i++) { | 778 | 762 | const key = keysA[i]; | 779 | | - if (!ObjectPrototypeHasOwnProperty(b, key) || | 780 | | - !innerDeepEqual(a[key], b[key], mode, memos)) { | | 763 | + if (!hasOwn(b, key) || !innerDeepEqual(a[key], b[key], mode, memos)) { | 781 | 764 | return false; | 782 | 765 | } | 783 | 766 | } | @@ -802,8 +785,8 @@ function objEquiv(a, b, mode, keys2, memos, iterationType) { | 802 | 785 | if (!innerDeepEqual(a[i], b[i], mode, memos)) { | 803 | 786 | return false; | 804 | 787 | } | 805 | | - const isSparseA = a[i] === undefined && !ObjectPrototypeHasOwnProperty(a, i); | 806 | | - const isSparseB = b[i] === undefined && !ObjectPrototypeHasOwnProperty(b, i); | | 788 | + const isSparseA = a[i] === undefined && !hasOwn(a, i); | | 789 | + const isSparseB = b[i] === undefined && !hasOwn(b, i); | 807 | 790 | if (isSparseA !== isSparseB) { | 808 | 791 | return false; | 809 | 792 | } | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-assert-deep-with-error.js +7Lines changed: 7 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -3,6 +3,13 @@ require('../common'); | 3 | 3 | const assert = require('assert'); | 4 | 4 | const { test } = require('node:test'); | 5 | 5 |
| | 6 | +// Disable colored output to prevent color codes from breaking assertion | | 7 | +// message comparisons. This should only be an issue when process.stdout | | 8 | +// is a TTY. | | 9 | +if (process.stdout.isTTY) { | | 10 | + process.env.NODE_DISABLE_COLORS = '1'; | | 11 | +} | | 12 | + | 6 | 13 | const defaultStartMessage = 'Expected values to be strictly deep-equal:\n' + | 7 | 14 | '+ actual - expected\n' + | 8 | 15 | '\n'; | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-assert-partial-deep-equal.js +43-1Lines changed: 43 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -176,10 +176,32 @@ describe('Object Comparison Tests', () => { | 176 | 176 | }, | 177 | 177 | { | 178 | 178 | description: | 179 | | - 'throws when comparing two objects with different Error instances', | | 179 | + 'throws when comparing two objects with different Error message', | 180 | 180 | actual: { error: new Error('Test error 1') }, | 181 | 181 | expected: { error: new Error('Test error 2') }, | 182 | 182 | }, | | 183 | + { | | 184 | + description: | | 185 | + 'throws when comparing two objects with missing cause on the actual Error', | | 186 | + actual: { error: new Error('Test error 1') }, | | 187 | + expected: { error: new Error('Test error 1', { cause: 42 }) }, | | 188 | + }, | | 189 | + { | | 190 | + description: | | 191 | + 'throws when comparing two objects with missing message on the actual Error', | | 192 | + actual: { error: new Error() }, | | 193 | + expected: { error: new Error('Test error 1') }, | | 194 | + }, | | 195 | + { | | 196 | + description: 'throws when comparing two Errors with missing cause on the actual Error', | | 197 | + actual: { error: new Error('Test error 1') }, | | 198 | + expected: { error: new Error('Test error 1', { cause: undefined }) }, | | 199 | + }, | | 200 | + { | | 201 | + description: 'throws when comparing two AggregateErrors with missing message on the actual Error', | | 202 | + actual: { error: new AggregateError([], 'Test error 1') }, | | 203 | + expected: { error: new AggregateError([new Error()], 'Test error 1') }, | | 204 | + }, | 183 | 205 | { | 184 | 206 | description: | 185 | 207 | 'throws when comparing two objects with different TypedArray instances and content', | @@ -1105,6 +1127,26 @@ describe('Object Comparison Tests', () => { | 1105 | 1127 | } | 1106 | 1128 | }] | 1107 | 1129 | }, | | 1130 | + { | | 1131 | + description: 'comparing two Errors with missing cause on the expected Error', | | 1132 | + actual: { error: new Error('Test error 1', { cause: 42 }) }, | | 1133 | + expected: { error: new Error('Test error 1') }, | | 1134 | + }, | | 1135 | + { | | 1136 | + description: 'comparing two Errors with cause set to undefined on the actual Error', | | 1137 | + actual: { error: new Error('Test error 1', { cause: undefined }) }, | | 1138 | + expected: { error: new Error('Test error 1') }, | | 1139 | + }, | | 1140 | + { | | 1141 | + description: 'comparing two Errors with missing message on the expected Error', | | 1142 | + actual: { error: new Error('Test error 1') }, | | 1143 | + expected: { error: new Error() }, | | 1144 | + }, | | 1145 | + { | | 1146 | + description: 'comparing two AggregateErrors with no message or errors on the expected Error', | | 1147 | + actual: { error: new AggregateError([new Error(), 123]) }, | | 1148 | + expected: { error: new AggregateError([]) }, | | 1149 | + }, | 1108 | 1150 | ].forEach(({ description, actual, expected }) => { | 1109 | 1151 | it(description, () => { | 1110 | 1152 | assert.partialDeepStrictEqual(actual, expected); | |
|