Repository navigation
deepStrictEqual comparison output is unhelpful on prototype-only differences #22141
Copy link
Copy link
Closed
Labels
assertIssues and PRs related to the assert subsystem.Issues and PRs related to the assert subsystem.confirmed-bugIssues and PRs for confirmed bugs.Issues and PRs for confirmed bugs.
Description
$ node -v
v10.8.0
$ node test.js
//test.js
const assert = require('assert').strict;
const DEBUG = process.env.DEBUG_ROUTER;
const pathnameToMatches = (pathname, template) => {
DEBUG && console.log(pathname, '~', template);
const regexp = templateToRegexp(template);
const matches = pathname.match(regexp);
DEBUG && console.log(pathname, '~', regexp, '=>', !matches ? false : (matches.groups || {}));
if (!matches)
return false;
const result = matches.groups || {};
for (const key in result)
result[key] = result[key] || null;
return result;
};
const templateToRegexp = template => {
let pattern = template;
DEBUG && console.log('processing =>', template);
pattern = pattern.replace(/\/:(?<param>[\w\d]+?)(?=\/|$)/gi, '\\/(?<$<param>>[\\w\\d]+?)(?=\\/|$)');
DEBUG && console.log('after required =>', pattern);
pattern = pattern.replace(/\/:(?<param>[\w\d]+?)\?/gi, '(\\/(?<=\\/)(?<$<param>>[\\w\\d]+?)(?=\\/|$))?');
DEBUG && console.log('after optional =>', pattern);
pattern = '^' + pattern + '$';
DEBUG && console.log('after boundaries =>', pattern);
const regexp = new RegExp(pattern, 'i');
DEBUG && console.log(template, '=>', pattern, '=>', regexp);
return regexp;
};
try {
assert.deepStrictEqual(pathnameToMatches('/user', '/user'), {});
assert.deepStrictEqual(pathnameToMatches('/not-user', '/user'), false);
assert.deepStrictEqual(pathnameToMatches('/user/123', '/user/:id'), { id: '123' });
assert.deepStrictEqual(pathnameToMatches('/user/123/456', '/user/:id'), false);
assert.deepStrictEqual(pathnameToMatches('/user', '/user/:id'), false);
assert.deepStrictEqual(pathnameToMatches('/user', '/user/:id?'), { id: null });
assert.deepStrictEqual(pathnameToMatches('/user/123', '/user/:id?'), { id: '123' });
assert.deepStrictEqual(pathnameToMatches('/user/123/male', '/user/:id/:sex'), { id: '123', sex: 'male' });
assert.deepStrictEqual(pathnameToMatches('/user/123', '/user/:id/:sex?'), { id: '123', sex: null });
assert.deepStrictEqual(pathnameToMatches('/user/123', '/user/:id/:sex'), false);
assert.deepStrictEqual(pathnameToMatches('/user/123', '/user/:id?/:sex?'), { id: '123', sex: null });
assert.deepStrictEqual(pathnameToMatches('/user/123/female', '/user/:id?/:sex?'), { id: '123', sex: 'female' });
assert.deepStrictEqual(pathnameToMatches('/user', '/user/:id?/:sex?'), { id: null, sex: null });
console.log('🎉🎉🎉');
}
catch (error) {
console.error(error);//.message, { actual: error.actual, expected: error.expected });
}
Throws a misterious:
{ AssertionError [ERR_ASSERTION]: Input objects not identical:
{
id: '123'
}
at Object.<anonymous> (/Users/damz/Desktop/react-router-xs/specs/pathnameToMatches.spec.js:41:12)
at Module._compile (internal/modules/cjs/loader.js:689:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
at Module.load (internal/modules/cjs/loader.js:599:32)
at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
at Function.Module._load (internal/modules/cjs/loader.js:530:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)
at startup (internal/bootstrap/node.js:266:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:596:3)
generatedMessage: true,
name: 'AssertionError [ERR_ASSERTION]',
code: 'ERR_ASSERTION',
actual: { id: '123' },
expected: { id: '123' },
operator: 'deepStrictEqual' }
What does this mean? Where's the difference? 🤨
actual: { id: '123' },
expected: { id: '123' },
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
assertIssues and PRs related to the assert subsystem.Issues and PRs related to the assert subsystem.confirmed-bugIssues and PRs for confirmed bugs.Issues and PRs for confirmed bugs.
Activity
ChALkeR commentedon Aug 5, 2018
@damianobarbati in your test, their prototypes differ. The object that is returned from the regexp in
matches.groupsdoes not have a prototype,{ id: '123' }does.A simplified version of your testcase:
Note that only the first of those two tests fails.
ChALkeR commentedon Aug 5, 2018
That is documented: https://nodejs.org/api/assert.html#assert_comparison_details
Moreover, that is what the main difference between
assert.deepStrictEqual/assert.strict.deepEqualand the legacy deprecatedassert.deepEqualis.ChALkeR commentedon Aug 5, 2018
Replacing
return result;withreturn Object.assign({}, result);fixes your tests.damianobarbati commentedon Aug 5, 2018
Oh I see now,
groupshas a prototype attached!The 2 misleading factors:
Only enumerable "own" properties are considered.but here prototype is considered for matchingThanks @ChALkeR!
ChALkeR commentedon Aug 5, 2018
@damianobarbati No,
matches.groupsdoesn't have a prototype (hasnull),{ id: '123' }does.assert.strict.deepEqual(Object.create(null), {})throws because the prototypes are not equal.ChALkeR commentedon Aug 5, 2018
matches.groupsalso wouldn't have a.toString()method, but{ id: '123' }would, because its prototype has it.BridgeAR commentedon Aug 5, 2018
As far as I understand the main issue it is not about the comparison details but about the output and that is indeed not ideal. The main difficulty here is how to visualize the difference between such objects. I struggled with this as well and I would like to fix this. I just could not yet come up with a great solution so far.
I am going to look into improving that though.
2 remaining items
ChALkeR commentedon Aug 6, 2018
@BridgeAR
Ok, that makes sense. I renamed the issue then.
antsmartian commentedon Aug 13, 2018
Hey @BridgeAR @ChALkeR , thought of giving a crack at this. I was seeing through the source code, and I found the issue is we are not showing up the error message properly when two objects are compared and one of those object's
prototypeisnull.The fix should be straight forward , if we have a object whose prototype is
null, we need to build up thebracesobject so that it can catch up the data about prototype beingnull(so that we can use these data to print relevant information on failure). But definitely we need to come up with some text that says it has empty prototype (I had kept it asproto-nullwhile I was building locally). I did those changes and I ran the example given by the OP and got the below message:The
actual,expectedlooked:which seems good to me -- clearly indicates prototype is not equal. Well, of course, if we do these changes, we need to work on
utils.inspecttest case as well (on the text that we might add), ex : https://github.com/nodejs/node/blob/master/test/parallel/test-util-inspect.js#L256Let me know your thoughts on this. Thanks!
BridgeAR commentedon Aug 13, 2018
@antsmartian I think it is actually a good idea to change the output in
util.inspect()to visualize the null prototype. My first thought was to only fix this inassertbut it should be useful in general to know if the entry has a prototype or not.If the
inspectoutput provides the necessary information the output is automatically fixed forassert. The question is how it should be visualized.proto-nullseems sub-par.[Object: null prototype]would work. The same as e.g.,[Promise: null prototype].antsmartian commentedon Aug 13, 2018
@BridgeAR
Yes, as I have shown above.
Yes, using
[Object: null prototype]should be good enough as we will get the followingasserterror:I will work on this and raise a PR.
Note: You have actually mentioned the wrong person in your reply , instead of me ;)
util: handle null prototype on inspect
util: handle null prototype on inspect
util: handle null prototype on inspect
util: handle null prototype on inspect
util: handle null prototype on inspect