Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

assert: use same value equal for deepStrictEqual NaN #15036

Closed
wants to merge 2 commits into from

Conversation

BridgeAR
Copy link
Member

I think it makes sense to further improve assert a bit and to accept NaN as well.

I also fixed the documentation a bit by adding missing changelogs to the assert docs.

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines
Affected core subsystem(s)

assert, doc

@nodejs-github-bot nodejs-github-bot added the assert Issues and PRs related to the assert subsystem. label Aug 25, 2017
@BridgeAR BridgeAR added semver-major PRs that contain breaking changes and should be released in the next major version. and removed assert Issues and PRs related to the assert subsystem. labels Aug 25, 2017
comparison. (Which means they are free of the [caveats][]).
1. Primitive values besides NaN are compared using the [Strict Equality
Comparison][] ( `===` ). Set and Map values, Map keys and NaN are compared
using the [SameValueZero][] comparison (Which means they are free of the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Which -> which

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed

@vsemozhetbyt
Copy link
Contributor

Maybe this line with exposed markdown syntax in JS example worth fixing? It can be confusing a bit.

@vsemozhetbyt vsemozhetbyt added assert Issues and PRs related to the assert subsystem. doc Issues and PRs related to the documentations. labels Aug 25, 2017
@BridgeAR
Copy link
Member Author

@vsemozhetbyt thx, fixed

@@ -466,4 +466,10 @@ assertOnlyDeepEqual(
assertDeepAndStrictEqual(m3, m4);
}

// Handle NaN
{
assert.throws(() => assert.deepEqual(NaN, NaN), assert.AssertionError);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Leaving out the braces around the arrow function adds an implicit return. Since the returned value is not used, I prefer to add braces. Feel free to ignore, but my two cents on it.

// Handle NaN
{
assert.throws(() => assert.deepEqual(NaN, NaN), assert.AssertionError);
assert.doesNotThrow(() => assert.deepStrictEqual(NaN, NaN));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since tests are a form of documentation, maybe add an additional test case or two that demonstrates the utility a bit more:

assert.doesNotThrow(() => { assert.deepStrictEqual({ a: NaN }, { a: NaN });
assert.doesNotThrow(() => { assert.deepStrictEqual( [ 1, 2, NaN, 4 ], [ 1, 2, NaN, 4 ]);

Copy link
Member

@Trott Trott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (with or without my comments addressed) if CI passes

@BridgeAR
Copy link
Member Author

I updated the tests according to your suggestions @Trott

@@ -102,6 +102,9 @@ parameter is undefined, a default error message is assigned.
<!-- YAML
added: v1.2.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/12142
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the proper link?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch 😃

refack
refack previously requested changes Aug 26, 2017
Copy link
Contributor

@refack refack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More request comment, than request change.

@@ -466,4 +466,11 @@ assertOnlyDeepEqual(
assertDeepAndStrictEqual(m3, m4);
}

// Handle NaN
assert.throws(() => { assert.deepEqual(NaN, NaN); }, assert.AssertionError);
Copy link
Contributor

@refack refack Aug 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not intuitive. I'd assume that
image
(deepStrictEqual should be more strict than deepEqual).
What's the rationale for not allowing assert.deepEqual(NaN, NaN)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is the bad part about the wording with strict. The strict stands for more exact in this case. deepEqual is actually more like a looksSimilar and it actually fails at seeing the similarity in this case even though it exists. I can add that check to deepEqual as well but I think it would be weird to have it there (even though I would call deepEqual broken anyway - we actually also use a own lint rule to prevent usage of it!).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually that is IMHO a great idea, create a better named superset assertion. That would also make this change semver-minor.

sameValues might be a better name (that way neither the word "strict" nor "deep" are necessary)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is absolutely not the right thing to do. And as I tried to explain earlier deepStrictEqual is not strictly a superset of deepEqual because it was just a poor naming from the beginning on. Now we have to live with that naming. Most people do for example not know that NaN is not equal to itself when compared with strict equality. And they also do not know about the other existing equality comparisons in JS. They mostly do not even know how abstract equality is defined - at least that is what my experience tells me.
The comparisons in general are well described here. Adding a new API would not help anyone and would instead create more confusion.

Copy link
Contributor

@refack refack Aug 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that assert.deepStrictEqual(NaN,NaN) makes sense intuitively.
But ECMAScript abstract equal is "broken" in this sense ((NaN == NaN) === false) and strict equal is broken ((NaN === NaN) === false) is the exact same way.
So IMHO we should be consistent with the standard if we keep using the term equal in our end-point names.

That's why I think breaking away forfrom legacy with a same end-point might be a good idea.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deepEqual and deepStrictEqual functions have much more then just the "basic" comparison and therefore do not correspond to these properly. I would love if that would be the only distinction because in that case deepEqual would not be so badly broken but that is not the case and it will not change either.
Adding another random name makes the issue not less an issue but just added more problems.

if (actual === null || expected === null ||
typeof actual !== 'object' || typeof expected !== 'object') {
if (typeof actual !== 'object') {
return typeof actual === 'number' && Number.isNaN(actual) &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think typeof actual === 'number' can be removed, Number.isNaN(actual) is sufficient.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is correct but it is faster for all other types to check for number first and if it is a number it is not a real penalty because that check is super fast.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it makes sense to do the same for expected then?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could but I feel like that is overdoing it as it would only apply for the case where Number.isNaN(actual), especially as I would expect expected to also be NaN in that case more often then not being of type number.

@refack refack dismissed their stale review August 26, 2017 19:15

De-escalating to discuss

@BridgeAR
Copy link
Member Author

BridgeAR commented Aug 27, 2017

This will actually also fix a issue in a N-API test case that expects NaN to be equal to each other and they tested it by using deepStrictEqual (the error will only show up in #15050).

AssertionError [ERR_ASSERTION]: [Number: NaN] deepStrictEqual [Number: NaN]
    at Object.<anonymous> (/node/test/addons-napi/test_conversions/test.js:119:8)

I personally think this could be counted as a bug fix instead of a semver-major because I highly doubt that anyone relies on NaN !== NaN in a deepStrictEqual call. Especially as most people are not even aware that that is the case.
@jasnell @Trott what do you think about that?

@Trott
Copy link
Member

Trott commented Aug 27, 2017

I highly doubt that anyone relies on

Changing this behavior could cause tests to succeed erroneously where two calculations are expected to come up with the same number but a bug in the code causes them both to come up with NaN instead. The test may even include a guard using typeof but that will still turn up number.

I'd prefer it be treated as semver-major out of caution but it's a mild preference. If others feel more strongly that it shouldn't be treated as a breaking change, I'm OK with that.

Copy link
Contributor

@refack refack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to deprecate deepEqual eventually

@refack
Copy link
Contributor

refack commented Aug 27, 2017

IMHO it's a bug fix of the ad hoc spec (I see the current behavior of assert as a self defining spec) so I tend towards 'semver-major'.
On the other hand @BridgeAR pointed out to me that other 3rd party asset libraries already treat NaN as equal to itself.

@tniessen
Copy link
Member

But ECMAScript abstract equal is "broken" in this sense ((NaN == NaN) === false) and strict equal is broken ((NaN === NaN) === false) is the exact same way.

@refack No, it's not. I actually considered implementing assert support for NaN a couple of weeks ago, but came across this part of the famous IEEE-754 specs:

Four mutually exclusive relations are possible: less than, equal, greater than, and unordered. The last arises when at least one operand is NaN. Every NaN shall compare unordered with everything, including itself. Comparisons shall ignore the sign of zero (so +0 = −0). Infinite operands of the same sign shall compare equal.

( `===` ). Set values and Map keys are compared using the [SameValueZero][]
comparison. (Which means they are free of the [caveats][]).
1. Primitive values besides NaN are compared using the [Strict Equality
Comparison][] ( `===` ). Set and Map values, Map keys and NaN are compared
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually use monospacing for constants such as NaN in the docs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed

@refack
Copy link
Contributor

refack commented Aug 28, 2017

@refack No, it's not. I actually considered implementing assert support for NaN a couple of weeks ago, but came across this part of the famous IEEE-754 specs:

Four mutually exclusive relations are possible: less than, equal, greater than, and unordered. The last arises when at least one operand is NaN. Every NaN shall compare unordered with everything, including itself. Comparisons shall ignore the sign of zero (so +0 = −0). Infinite operands of the same sign shall compare equal.

@BridgeAR pointed me to that spec as well. IMHO that spec should not have been implemented verbatim in JS, but that's done (BTW: If I read this correctly then (NaN != NaN) should be false as well)

My point was that if (NaN == NaN) ≡ (NaN === NaN) then we should make deepEqual(NaN, NaN) ≡ deepStrictEqual(NaN, NaN), but @BridgeAR convinced me that deepEqual is not worth our hassle.

@BridgeAR
Copy link
Member Author

@Trott

Changing this behavior could cause tests to succeed erroneously where two calculations are expected to come up with the same number but a bug in the code causes them both to come up with NaN instead.

But in that case the test should not have existed yet as it would have otherwise failed and the bug got hopefully fixed? So it is only something for the future, right?

@BridgeAR
Copy link
Member Author

I rebased due to conflicts and addressed the monospacing.

@BridgeAR
Copy link
Member Author

CITGM https://ci.nodejs.org/view/Node.js-citgm/job/citgm-smoker/960/

@Trott
Copy link
Member

Trott commented Aug 31, 2017

But in that case the test should not have existed yet as it would have otherwise failed and the bug got hopefully fixed? So it is only something for the future, right?

I'm thinking of regressions where a bug is introduced while refactoring or something like that. Might be unlikely as you'd need to mess up two calculations that way simultaneously, I suppose.

@BridgeAR
Copy link
Member Author

BridgeAR commented Sep 1, 2017

Rebased due to conflicts. I also fixed a documentation bug from a former commit where the PR number was wrong.

@Trott I am not sure I can follow. If I read your comment correct you would rather keep it as semver-major, right?

@Trott
Copy link
Member

Trott commented Sep 2, 2017

I am not sure I can follow

@BridgeAR I'd prefer it be treated as semver-major out of caution but it's a very mild preference. If no one else agrees, I'm fine with my mild preference being disregarded. :-D

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM as semver-major

@BridgeAR
Copy link
Member Author

BridgeAR commented Sep 5, 2017

Landed in 1789dcf and ea2e636.

@BridgeAR BridgeAR closed this Sep 5, 2017
BridgeAR added a commit that referenced this pull request Sep 5, 2017
PR-URL: #15036
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
BridgeAR added a commit that referenced this pull request Sep 5, 2017
Comparing NaN will not throw anymore.

PR-URL: #15036
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
addaleax pushed a commit to addaleax/ayo that referenced this pull request Sep 5, 2017
PR-URL: nodejs/node#15036
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
addaleax pushed a commit to addaleax/ayo that referenced this pull request Sep 5, 2017
Comparing NaN will not throw anymore.

PR-URL: nodejs/node#15036
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Refael Ackermann <refack@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
@BridgeAR BridgeAR deleted the assert-nan branch April 1, 2019 23:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
assert Issues and PRs related to the assert subsystem. doc Issues and PRs related to the documentations. semver-major PRs that contain breaking changes and should be released in the next major version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants