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: ensure .rejects() disallows sync throws #19650

Closed

Conversation

not-an-aardvark
Copy link
Contributor

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines

This PR has two commits; the first commit fixes an issue with the assert tests, and the second commit fixes #19646.

First commit:

test: ensure failed assertions cause build to fail

This updates the test in `test/parallel/test-assert-async.js` to add an
assertion that the Promises used in the test end up fulfilled.
Previously, if an assertion failure occurred, the Promises would have
rejected and a warning would have been logged, but the test would still
have exit code 0.

Second commit:

assert: ensure .rejects() disallows sync throws

This updates `assert.rejects()` to disallow any errors that are thrown
synchronously from the given function. Previously, throwing an error
would cause the same behavior as returning a rejected Promise.

Fixes: https://github.com/nodejs/node/issues/19646

Note that the second commit would be semver-major, but it modifies an API (introduced in #18023) that has not been backported to a release branch yet. As a result, the second commit could be backported as long as it's applied at the same time as #18023.

@nodejs-github-bot nodejs-github-bot added the assert Issues and PRs related to the assert subsystem. label Mar 28, 2018
Copy link
Member

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

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

LGTM

common.mustNotCall(),
common.mustCall((err) => {
assert.strictEqual(err, THROWN_ERROR);
})
Copy link
Member

Choose a reason for hiding this comment

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

Suggestion: use catch(common.mustCall(err) => { assert.strictEqual(err, THROWN_ERROR); })

common.expectsError({
code: 'ERR_ASSERTION',
type: assert.AssertionError,
message: 'Failed'
})
);
).then(common.mustCall());
Copy link
Member

Choose a reason for hiding this comment

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

Nit: this test should now be redundant if I am not mistaken.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm assuming you mean that this test is redundant with the other test here. However, this test is a bit different from the other one because it throws asynchronously, and so the promise returned by assert.rejects fulfills (whereas in the other test, the function throws synchronously and the promise returned by assert.rejects rejects and needs to be caught separately).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, I just realized what you meant: this test is now redundant with this other test. I agree, I'll remove the other one.

edit: Removed.

@@ -29,7 +29,7 @@ assert.doesNotReject(() => {});
type: assert.AssertionError,
message: 'Failed'
}));
assert.doesNotReject(() => promise);
assert.doesNotReject(() => promise).then(common.mustCall());
Copy link
Member

Choose a reason for hiding this comment

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

Nit: use common.crashOnUnhandledRejection() at the top of the file instead of adding then(common.mustCall()) everywhere.

@targos
Copy link
Member

targos commented Mar 28, 2018

@BridgeAR about .then(common.mustCall()):

It is necessary to make sure the promise eventually fulfills (does not stay forever in a pending state).

I think the best would be to combine common.crashOnUnhandledRejection() at the top and an async IIFE:

common.crashOnUnhandledRejection();

(async () => {
  {
    // test 1
    await assert.rejects(something);
  }

  {
    // test 2
    await assert.rejects(somethingElse);
  }
})().then(common.mustCall());

We should maybe add something like common.runAsync(async () => {}) that does both?

@BridgeAR
Copy link
Member

@targos fair point. But should the test not time out in case the promise does not fulfill?

@TimothyGu
Copy link
Member

TimothyGu commented Mar 28, 2018

@BridgeAR Here's a case where the .then(common.mustCall()) is absolutely necessary:

// Never fulfills.
const prom = new Promise(() => {});

(async () => {
  await prom;
  console.log('Never printed');
})();

The process does not stay alive since promise states are not part of the event loop.

@not-an-aardvark
Copy link
Contributor Author

Updated to follow the suggestions in #19650 (comment) and #19650 (comment).

This updates the test in `test/parallel/test-assert-async.js` to add an
assertion that the Promises used in the test end up fulfilled.
Previously, if an assertion failure occurred, the Promises would have
rejected and a warning would have been logged, but the test would still
have exit code 0.
This updates `assert.rejects()` to disallow any errors that are thrown
synchronously from the given function. Previously, throwing an error
would cause the same behavior as returning a rejected Promise.

Fixes: nodejs#19646
@not-an-aardvark
Copy link
Contributor Author

@BridgeAR
Copy link
Member

@TimothyGu thanks for the heads up!

@not-an-aardvark to have feature parity, it might make sense to check in assert.doesNotReject that the passed in function will actually return a promise. From that point on the function has some legitimacy. Right now I do not see any legit use case. What do you think?

@targos
Copy link
Member

targos commented Mar 29, 2018

Just thinking about it now: wouldn't it be better if assert.rejects expected a Promise instead of a function?

@BridgeAR
Copy link
Member

BridgeAR commented Mar 29, 2018

@targos I agree. That solves that dilemma. Since we did not yet publish the functionality, we can still change it. [update] Thinking about it again: It might still be better to only handle functions:

  • We use functions in assert.throws and assert.doesNotThrow
  • It has the benefit to test a specific functionality to really return a promise

I am going to give it another thought when I have some more time later on.

@targos
Copy link
Member

targos commented Mar 29, 2018

In favor of changing it: existing assertion libraries that I know (Should.js, Chai as Promised, jest) work directly with promises.

@not-an-aardvark
Copy link
Contributor Author

Arguably, this is different from assert.throws because assert.throws makes an assertion about how a function behaves, whereas assert.rejects only makes an assertion about the function's return value. So it would make sense for it to just accept a Promise rather than a function.

On the other hand, one potential argument for the current assert.rejects behavior is that it could be simpler for users who don't know how Promises work. They might have a mental model of "always use await when calling an async function to avoid weird behavior". With that mental model, assert.rejects(someAsyncFunction) would make sense, but assert.rejects(someAsyncFunction()) would not make sense. They might try to use assert.rejects(await someAsyncFunction()) instead, but this would cause other problems. The current behavior of assert.rejects would allow for a mental model of "this is just like assert.throws, but for async functions."

@not-an-aardvark
Copy link
Contributor Author

@BridgeAR @targos What would you recommend doing with this PR? If the behavior of assert.rejects is going to be changed to accept Promises rather than functions, then I'm fine with closing this. However, if we don't do anything, then it seems likely that the current behavior will end up landing in a release, after which it will be much harder to change. (I haven't landed this PR yet because I'm not sure if you wanted to change the API signature instead.)

@BridgeAR
Copy link
Member

BridgeAR commented Apr 3, 2018

I am actually considering supporting to accept both: passing in a function that returns a promise or directly passing in a promise. That would ease the user experience. I do not really see a downside accepting both.

@not-an-aardvark
Copy link
Contributor Author

I'm going to land this since we seem to be in agreement that it's an improvement over the current behavior, but I'd be fine with the behavior being changed in a future PR.

@not-an-aardvark
Copy link
Contributor Author

Landed in 8995125 and fdb35d8

not-an-aardvark added a commit that referenced this pull request Apr 4, 2018
This updates the test in `test/parallel/test-assert-async.js` to add an
assertion that the Promises used in the test end up fulfilled.
Previously, if an assertion failure occurred, the Promises would have
rejected and a warning would have been logged, but the test would still
have exit code 0.

PR-URL: #19650
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
not-an-aardvark added a commit that referenced this pull request Apr 4, 2018
This updates `assert.rejects()` to disallow any errors that are thrown
synchronously from the given function. Previously, throwing an error
would cause the same behavior as returning a rejected Promise.

Fixes: #19646
PR-URL: #19650
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
@not-an-aardvark not-an-aardvark deleted the assert-rejects-sync-error branch April 4, 2018 02:48
BridgeAR pushed a commit to BridgeAR/node that referenced this pull request May 1, 2018
This updates the test in `test/parallel/test-assert-async.js` to add an
assertion that the Promises used in the test end up fulfilled.
Previously, if an assertion failure occurred, the Promises would have
rejected and a warning would have been logged, but the test would still
have exit code 0.

PR-URL: nodejs#19650
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
BridgeAR pushed a commit to BridgeAR/node that referenced this pull request May 1, 2018
This updates `assert.rejects()` to disallow any errors that are thrown
synchronously from the given function. Previously, throwing an error
would cause the same behavior as returning a rejected Promise.

Fixes: nodejs#19646
PR-URL: nodejs#19650
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
MylesBorins pushed a commit to MylesBorins/node that referenced this pull request Nov 2, 2018
This updates the test in `test/parallel/test-assert-async.js` to add an
assertion that the Promises used in the test end up fulfilled.
Previously, if an assertion failure occurred, the Promises would have
rejected and a warning would have been logged, but the test would still
have exit code 0.

PR-URL: nodejs#19650
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
MylesBorins pushed a commit to MylesBorins/node that referenced this pull request Nov 2, 2018
This updates `assert.rejects()` to disallow any errors that are thrown
synchronously from the given function. Previously, throwing an error
would cause the same behavior as returning a rejected Promise.

Fixes: nodejs#19646
PR-URL: nodejs#19650
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
MylesBorins pushed a commit that referenced this pull request Nov 4, 2018
This updates the test in `test/parallel/test-assert-async.js` to add an
assertion that the Promises used in the test end up fulfilled.
Previously, if an assertion failure occurred, the Promises would have
rejected and a warning would have been logged, but the test would still
have exit code 0.

Backport-PR-URL: #24019
PR-URL: #19650
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
MylesBorins pushed a commit that referenced this pull request Nov 4, 2018
This updates `assert.rejects()` to disallow any errors that are thrown
synchronously from the given function. Previously, throwing an error
would cause the same behavior as returning a rejected Promise.

Fixes: #19646
Backport-PR-URL: #24019
PR-URL: #19650
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
@BethGriggs BethGriggs mentioned this pull request Nov 12, 2018
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.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

assert.rejects passes if the given function throws synchronously
7 participants