You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
assert.deepStrictEqual is fooled by strange dates #10258
assert.deepStrictEqual (and strictEqual) is fooled by the following code:
functionFakeDate(){};FakeDate.prototype=Date.prototype;constf=newFakeDate();constd=newDate();f.toString();//TypeError: Method Date.prototype.toString called on incompatible receiver [object Object]d.toString();//'Wed Dec 14 2016 09:53:28 GMT+1100 (AEDT)'assert.deepStrictEqual(d,f);// does not throw any error
I think it's reasonable to expect that assert.deepStrictEqual should consider d and f as unequal.
I have not tested on Node master, however there are no relevant changes in assert.js between 6.9.1 and master.
assert.js:85
throw new assert.AssertionError({
^
AssertionError: 2016-12-14T04:59:37.022Z === Date {}
at Object.<anonymous> (/Users/joyee/projects/node-test/fake-date.js:16:8)
at Module._compile (module.js:571:32)
at Object.Module._extensions..js (module.js:580:10)
at Module.load (module.js:488:32)
at tryModuleLoad (module.js:447:12)
at Function.Module._load (module.js:439:3)
at Module.runMain (module.js:605:10)
at run (bootstrap_node.js:420:7)
at startup (bootstrap_node.js:139:9)
at bootstrap_node.js:535:3
But deepStrictEqual does not throw indeed(I think you mean deepEqual & deepStrictEqual?). That's because Dates are not primitive values and don't have their own properties, and here f and d have the same [[Prototype]]. I think this behavior does not contradict what the doc says
assert.deepEqual(actual, expected[, message])
Tests for deep equality between the actual and expected parameters. Primitive values are compared with the equal comparison operator ( == ).
Only enumerable "own" properties are considered. The deepEqual() implementation does not test object prototypes, attached symbols, or non-enumerable properties.
Generally identical to assert.deepEqual() with two exceptions. First, primitive values are compared using the strict equality operator ( === ). Second, object comparisons include a strict equality check of their prototypes.
But in this case the more reasonable behavior should throw IMHO. I can open a PR adding a special case in assert it that seems reasonable to others.
If we decide that Date is special in this regard and should be treated differently than other objects, is there an easy way to distinguish between the situation described in this issue and a more prosaic case where two Date objects should be deepStrictEqual? I guess we can run toString() and wrap it in a try/catch, but that seems problematic.
It's not quite the same as your example, as d has actual date information that f lacks (in my example, the Date constructor is never called. If f was initialised with super() then the examples would be more comparable). I guess the issue is that some types have important "comparable" data that is not directly accessible to js. I haven't tested, but I guess that the primitive wrapper objects (String etc) might show similar annoying behaviour.
#valueOf()seems to be a standard way of getting a comparable value out of such objects (i.e. not just Date), might it be worth calling this on comparisons?
Just did a quick research across some popular assertion libraries that I can think of(omitted power-assert because IIRC they just reuse assert underneath). Turns out everybody throws for this...expect Node core.
'use strict';constassert=require('assert');constchaiAssert=require('chai').assert;constchaiExpect=require('chai').expect;constexpect=require('expect.js');constshould=require('should');constcodeExpect=require('code').expect;constmustExpect=require('must');constunexpectedExpect=require('unexpected');functionFakeDate(){};FakeDate.prototype=Date.prototype;constd=newDate();constf=newFakeDate();// to make sure I use these libs properly// const d = { a: 1, b: 2 };// const f = { a: 1, b: 2 };lettests={'assert': ()=>assert.deepEqual(d,f),'chaiAssert': ()=>chaiAssert.deepEqual(d,f),'should': ()=>should.deepEqual(d,f),'codeExpect': ()=>codeExpect(d).to.equal(f),'chaiExpect': ()=>chaiExpect(d).to.deep.equal(f),'mustExpect': ()=>mustExpect(d).to.eql(f),'unexpectedExpect': ()=>unexpectedExpect(d,'to equal',f),'expect': ()=>expect(d).to.eql(f)}for(constnameintests){try{tests[name]();console.log('\n### '+name+' does not throw');}catch(e){console.log('\n### '+name+' throws:');console.log(e.message);}}
The results are:
### assert does not throw
### chaiAssert throws:
expected Wed, 14 Dec 2016 05:58:25 GMT to deeply equal {}
### should throws:
expected 2016-12-14 13:58:25.447 +0800 deepEqual Date {}
### codeExpect throws:
this is not a Date object.
### chaiExpect throws:
expected Wed, 14 Dec 2016 05:58:25 GMT to deeply equal {}
### mustExpect throws:
2016-12-14T05:58:25.447Z must be equivalent to {}
### unexpectedExpect throws:
expected new Date('Wed, 14 Dec 2016 05:58:25.447 GMT') to equal Date({})
### expect throws:
this is not a Date object.
@Trott And for the FooBar example nobody throws. So other libraries treat Dates somewhat differently I guess? I think it's more about making sure fake dates are not equal to a real Date, no matter conceptually what the time underneath should be.
classMyDateextendsDate{constructor(...args){super(...args)this.myProp='value'}}vardate1=newDate('2016')console.log(date1)// 2016-01-01T00:00:00.000Zvardate2=newMyDate('2016')console.log(date2)// { 2016-01-01T00:00:00.000Z myProp: 'value' }assert.deepStrictEqual(date1,date2)// does not throw
Not quite relavent to this issue, this just came up to me while I am digging around the code:
assert.deepEqual(newSet([1,2]),newSet([1,2,3]));// does not throwassert.deepEqual(newMap([['a',1],['b',2]]),newMap([['a',1],['b',3]]));// does not throw
I know assert has a stability index of 3, so it's just a thought :P. Since we are using more and more ES6 in core and V8 have started to optimize iterators, adding checks for iterables might be a nice-to-have in the future.
I looked into the source code of other libraries, turns out
Some of them use Object.prototype.toString.call(obj) to check if the type of the two objects are the same first. A fake date would have [object Object] but a real date would have [object Date] in our current version of V8. IIRC an older version of v8 would return invalid Date but that was fixed.
Some of them just call getTime() on Date-like objects(and throw this is not a Date object.).
And in the user-land modules, deepEqual is usually closer to the semantics of deepStrictEqual in node core.
I think in addition to checking the prototype, we need to check the types returned by Object.prototype.toString.call(obj) too (it's already done for [object Arguments], but not for other built-in types).
Activity
joyeecheung commentedon Dec 14, 2016
Tested on master, for
strictEqualI getBut
deepStrictEqualdoes not throw indeed(I think you meandeepEqual&deepStrictEqual?). That's becauseDates are not primitive values and don't have their own properties, and herefanddhave the same[[Prototype]]. I think this behavior does not contradict what the doc saysBut in this case the more reasonable behavior should throw IMHO. I can open a PR adding a special case in assert it that seems reasonable to others.
Trott commentedon Dec 14, 2016
I'm open to being persuaded on this, but given what we have here so far, I'm not convinced this is a bug.
The two objects have identical properties, including the
toString()function. ThetoString()function is itself sufficiently aware of its context to throw an error on the FakeDate object. But that's a C++-land check. ThetoString()functions are in fact identical, even though the result of calling it are different on the two objects.This seems basically the same as this:
If we decide that
Dateis special in this regard and should be treated differently than other objects, is there an easy way to distinguish between the situation described in this issue and a more prosaic case where two Date objects should be deepStrictEqual? I guess we can runtoString()and wrap it in a try/catch, but that seems problematic.akdor1154 commentedon Dec 14, 2016
It's not quite the same as your example, as
dhas actual date information thatflacks (in my example, the Date constructor is never called. Iffwas initialised withsuper()then the examples would be more comparable). I guess the issue is that some types have important "comparable" data that is not directly accessible to js. I haven't tested, but I guess that the primitive wrapper objects (Stringetc) might show similar annoying behaviour.#valueOf()seems to be a standard way of getting a comparable value out of such objects (i.e. not justDate), might it be worth calling this on comparisons?@joyeecheung, yep I meant
deepEqual, sorry.joyeecheung commentedon Dec 14, 2016
Just did a quick research across some popular assertion libraries that I can think of(omitted
power-assertbecause IIRC they just reuseassertunderneath). Turns out everybody throws for this...expect Node core.The results are:
joyeecheung commentedon Dec 14, 2016
@Trott And for the
FooBarexample nobody throws. So other libraries treatDates somewhat differently I guess? I think it's more about making sure fake dates are not equal to a realDate, no matter conceptually what the time underneath should be.Trott commentedon Dec 14, 2016
Hmmm... On the one hand: For both objects, it's all tucked away behind getters and setters, and those getters and setters are all identical.
But on the other hand: You certainly have a point that this behavior is kind of surprising and non-intuitive, at least at first blush.
And the fact that six out of six userland assertion libraries throw on this would seem to back that up.
I can go either way on this one right now. A PR might help clarify things. So I'll stop getting in the way. :-D
joyeecheung commentedon Dec 14, 2016
OK, I think I will try to read other libraries' code first and figure out why they all choose(or perhaps accidentally choose) to throw :).
targos commentedon Dec 14, 2016
Another unexpected result:
Relevant lines from assert:
node/lib/assert.js
Lines 152 to 153 in 4f97a14
joyeecheung commentedon Dec 14, 2016
Not quite relavent to this issue, this just came up to me while I am digging around the code:
I know assert has a stability index of 3, so it's just a thought :P. Since we are using more and more ES6 in core and V8 have started to optimize iterators, adding checks for iterables might be a nice-to-have in the future.
Fishrock123 commentedon Dec 14, 2016
@joyeecheung That last example is why
deepStrictEqualis preferred overdeepEqual. :Djoyeecheung commentedon Dec 14, 2016
@Fishrock123 Uh, which one do you mean?
joyeecheung commentedon Dec 14, 2016
I looked into the source code of other libraries, turns out
Object.prototype.toString.call(obj)to check if the type of the two objects are the same first. A fake date would have[object Object]but a real date would have[object Date]in our current version of V8. IIRC an older version of v8 would returninvalid Datebut that was fixed.getTime()on Date-like objects(and throwthis is not a Date object.).And in the user-land modules,
deepEqualis usually closer to the semantics ofdeepStrictEqualin node core.I think in addition to checking the prototype, we need to check the types returned by
Object.prototype.toString.call(obj)too (it's already done for[object Arguments], but not for other built-in types).joyeecheung commentedon Dec 14, 2016
I'm trying to put a PR together but there are always some really bizarre cases that I could break, like these introduced by 00a7456:
akdor1154 commentedon Dec 14, 2016
@joyeecheung those are really weird tests, I would assume those should throw.
@teppeis, @bnoordhuis is this really desired behaviour?
18 remaining items
assert: fix premature deep strict comparison
assert: fix premature deep strict comparison
assert: enforce type check in deepStrictEqual
assert: enforce type check in deepStrictEqual