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: improve assert rejects #19885

Closed
wants to merge 4 commits into from

Conversation

BridgeAR
Copy link
Member

@BridgeAR BridgeAR commented Apr 9, 2018

Commit 1 (assert: add direct promises support in rejects):

This adds direct promise support to `assert.rejects` and
`assert.doesNotReject`. It will now accept both, functions and ES2015
promises as input.

Besides this the documentation was updated to reflect the latest
changes.

It also refactors the tests to a non blocking way to improve the
execution performance and improves the coverage.

Commit 2 (tools: add eslintrc rule for assert.rejects):

This makes sure `assert.rejects` is always called with a second
argument. Besides that it is also changes a eslint error message
to suggest objects instead of a regular expression.

Commit 3 (doc: improve assert documentation):

This adds a example to `assert.throws` to document checking against
error instances. It also makes it clearer what some arguments can
receive as input.
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

@nodejs-github-bot nodejs-github-bot added assert Issues and PRs related to the assert subsystem. tools Issues and PRs related to the tools directory. labels Apr 9, 2018
@BridgeAR BridgeAR added the semver-minor PRs that contain new features and should be released in the next minor version. label Apr 9, 2018
Copy link
Contributor

@vsemozhetbyt vsemozhetbyt left a comment

Choose a reason for hiding this comment

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

The doc LGTM with some nits)

.eslintrc.js Outdated
{
selector: `CallExpression[callee.object.name='assert'][callee.property.name='throws'][arguments.1.type='Literal']:not([arguments.1.regex])`,
message: 'use a regular expression for second argument of assert.throws()',
message: 'Use a object as second argument of assert.throws()',
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: a object -> an object.

function, or an object where each property will be tested for.
If specified, `error` can be a [`Class`][], [`RegExp`][], a validation function,
an object where each property will be tested for or an instance of error where
each property will be tested for including the non-enumerable message and name
Copy link
Contributor

Choose a reason for hiding this comment

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

message and name -> `message` and `name`?

Error
).then(() => {
// ...
});
```

Note that `error` can not be a string. If a string is provided as the second
Copy link
Contributor

Choose a reason for hiding this comment

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

can not -> cannot?

function, or an object where each property will be tested for.
If specified, `error` can be a [`Class`][], [`RegExp`][], a validation function,
an object where each property will be tested for or an instance of error where
each property will be tested for including the non-enumerable message and name
Copy link
Contributor

Choose a reason for hiding this comment

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

message and name -> `message` and `name`?

- selector: "CallExpression[callee.object.name='assert'][callee.property.name='throws'][arguments.1.type='Literal']:not([arguments.1.regex])"
message: "use a regular expression for second argument of assert.throws()"
message: "Use a object as second argument of assert.throws()"
Copy link
Contributor

Choose a reason for hiding this comment

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

a object -> an object?

@BridgeAR
Copy link
Member Author

BridgeAR commented Apr 9, 2018

@BridgeAR BridgeAR force-pushed the improve-assert-rejects branch from 7789f2d to 85ef34a Compare April 10, 2018 02:00
@BridgeAR
Copy link
Member Author

Rebased due to conflicts.

New CI https://ci.nodejs.org/job/node-test-pull-request/14165/

@BridgeAR
Copy link
Member Author

Copy link
Contributor

@not-an-aardvark not-an-aardvark left a comment

Choose a reason for hiding this comment

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

I'm not really convinced that this is worthwhile -- it seems unnecessary to switch behavior based on the argument type when we could just use another method instead. (It also seems like the behavior isn't well-defined in edge cases where the value is both a thenable and a function simultaneously.) But I'm not strongly opposed if you think this is simpler.

lib/assert.js Outdated
@@ -448,13 +448,24 @@ function getActual(block) {
return NO_EXCEPTION_SENTINEL;
}

function checkIsPromise(obj) {
return isPromise(obj) ||
Copy link
Contributor

Choose a reason for hiding this comment

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

I would recommend just checking for the presence of a .then function here, without doing any of the other checks. That would make it compatible with Promise libraries and the Promise.resolve method and Promise libraries, as well as the mental model of "anything that can be await-ed to get a different value".

Copy link
Member Author

Choose a reason for hiding this comment

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

My intention was not to support Promises/A+ but ES6 promises. And if I read the spec correct, the check is fine as I implemented it. Only checking for obj !== null && typeof obj.then === 'function' would allow multiple false positives, so I would rather stick to the current check.

I can remove the isPromise check though as any real promise will also adhere to the later checks.

Copy link
Member

Choose a reason for hiding this comment

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

If you only want to support ES6 promises, why do you need the checks below?

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, I do not only want to support native promises. Every implementation that is spec compliant should be supported out of my perspective. That is not the case for all thenables. I do not have a strong opinion on this though, so I can go for what ever is suggested. Only having the upper check is definitely not enough though. The very least that should also be checked for is typeof obj === 'object' || typeof obj === 'function and I would argue also `typeof obj.catch === 'function'. In that case we should be relatively safe.

The difference with that check to the current implementation would be to also accept thenables that use a function instead a object as Promise.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not against the current check, but it would be nice to have a small comment explaining this in the code.

Copy link
Contributor

@not-an-aardvark not-an-aardvark Apr 14, 2018

Choose a reason for hiding this comment

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

There are two categories of "promise-like" objects that have special behavior in the language: native promises and thenables. By doing extra checks (e.g. for a .catch method), we would effectively be adding a third category (the categories would be "native promises", "thenables", and "thenables which also have a .catch method") This seems like it would complicate the model by adding an arbitrary additional check, and it's not clear to me that there is a substantial benefit to adding a .catch check. So in my opinion, it would be slightly better to just detect thenables for API simplicity.

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.

Can you only include the changes to assert.rejects?

Using the big red cross because I'm -1 to make assert.throws aware of any async behavior.

@BridgeAR BridgeAR force-pushed the improve-assert-rejects branch from 85ef34a to c7ef1b9 Compare April 10, 2018 10:50
@BridgeAR
Copy link
Member Author

@mcollina oh, I had a wrong description, it was never about assert.throws. I force pushed and updated the PR description. PTAL

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 with a nit

When `assert.rejects()` is called, it will immediately call the `block`
function, and awaits for completion.
If `block` is a function and it throws synchronously, that error will be
rejected directly without checking the `error` handler.
Copy link
Member

Choose a reason for hiding this comment

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

what do you mean with this?

Copy link
Member Author

Choose a reason for hiding this comment

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

Synchronously thrown errors (due to the block() call) will directly cause a rejected promise to be returned (with that error). That is different in the way that a rejection caused by calling block() will trigger the potential error handler (second argument). If the rejected error does not adhere to the error handler, then it will cause a rejection. Otherwise not.

I do see that this is not well worded yet and it would be great to get a suggestion for improvement. Looping in @nodejs/documentation

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

I cannot think of anything more clear(

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe something like this?

If block is a function and it throws an error synchronously, assert.rejects will return a rejected Promise with that error without checking the error handler.

@BridgeAR
Copy link
Member Author

@not-an-aardvark my intention was not to support thenables but only ES6 Promises. I personally think it is best this way but I do not feel that strongly about it. So if others think we should support thenables as well, I am fine to accept those as well.

@BridgeAR
Copy link
Member Author

BridgeAR commented Apr 12, 2018

@targos you reviewed #19886 and were involved in a former discussion about adding support for passing in a promise directly. So it would be nice if you could have a look :-)

@TimothyGu I saw in a comment that you expected assert.rejects to accept promises directly, so it would also be nice if you could have a look :-).

@BridgeAR BridgeAR requested a review from TimothyGu April 12, 2018 11:18
This adds direct promise support to `assert.rejects` and
`assert.doesNotReject`. It will now accept both, functions and ES2015
promises as input.

Besides this the documentation was updated to reflect the latest
changes.

It also refactors the tests to a non blocking way to improve the
execution performance and improves the coverage.
This makes sure `assert.rejects` is always called with a second
argument. Besides that it is also changes a eslint error message
to suggest objects instead of a regular expression.
This adds a example to `assert.throws` to document checking against
error instances. It also makes it clearer what some arguments can
receive as input.
@BridgeAR BridgeAR force-pushed the improve-assert-rejects branch from c7ef1b9 to fa6ac5f Compare April 12, 2018 12:10
@BridgeAR
Copy link
Member Author

Rebased due to conflicts.

New CI https://ci.nodejs.org/job/node-test-pull-request/14218/

let resultPromise;
if (typeof block === 'function') {
// Return a rejected promise if `block` throws synchronously.
resultPromise = block();
Copy link
Member

Choose a reason for hiding this comment

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

Should we check that block() effectively returns a Promise?

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 have a follow up PR in #19886 that does exactly that :-)

@BridgeAR
Copy link
Member Author

I updated the comments according to the suggestion and added a comment next to the promise check.

New CI https://ci.nodejs.org/job/node-test-pull-request/14264/

@BridgeAR BridgeAR added the author ready PRs that have at least one approval, no pending requests for changes, and a CI started. label Apr 13, 2018
* `error` {RegExp|Function}
* `message` {any}

Awaits for the promise returned by function `block` to complete and not be
rejected.
Awaits the `block` promise or, if `block` is a function, immediately call 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: immediately calls the function and awaits the returned promise
Ditto on line 927

If specified, `error` can be a constructor, [`RegExp`][], a validation
function, or an object where each property will be tested for.
If specified, `error` can be a [`Class`][], [`RegExp`][], a validation function,
an object where each property will be tested for or an instance of error where
Copy link
Member

Choose a reason for hiding this comment

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

nit: an object where each property will be tested for, or an instance
Ditto on line 1027

BridgeAR added a commit to BridgeAR/node that referenced this pull request Apr 16, 2018
This adds direct promise support to `assert.rejects` and
`assert.doesNotReject`. It will now accept both, functions and ES2015
promises as input.

Besides this the documentation was updated to reflect the latest
changes.

It also refactors the tests to a non blocking way to improve the
execution performance and improves the coverage.

PR-URL: nodejs#19885
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Michaël Zasso <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Trivikram Kamat <[email protected]>
BridgeAR added a commit to BridgeAR/node that referenced this pull request Apr 16, 2018
This makes sure `assert.rejects` is always called with a second
argument. Besides that it is also changes a eslint error message
to suggest objects instead of a regular expression.

PR-URL: nodejs#19885
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Michaël Zasso <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Trivikram Kamat <[email protected]>
BridgeAR added a commit to BridgeAR/node that referenced this pull request Apr 16, 2018
This adds a example to `assert.throws` to document checking against
error instances. It also makes it clearer what some arguments can
receive as input.

PR-URL: nodejs#19885
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Michaël Zasso <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Trivikram Kamat <[email protected]>
@BridgeAR
Copy link
Member Author

Landed in 11819c7...1d6cdfc

Nits addressed while landing.

@BridgeAR BridgeAR closed this Apr 16, 2018
jasnell pushed a commit that referenced this pull request Apr 16, 2018
This adds direct promise support to `assert.rejects` and
`assert.doesNotReject`. It will now accept both, functions and ES2015
promises as input.

Besides this the documentation was updated to reflect the latest
changes.

It also refactors the tests to a non blocking way to improve the
execution performance and improves the coverage.

PR-URL: #19885
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Michaël Zasso <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Trivikram Kamat <[email protected]>
jasnell pushed a commit that referenced this pull request Apr 16, 2018
This makes sure `assert.rejects` is always called with a second
argument. Besides that it is also changes a eslint error message
to suggest objects instead of a regular expression.

PR-URL: #19885
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Michaël Zasso <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Trivikram Kamat <[email protected]>
jasnell pushed a commit that referenced this pull request Apr 16, 2018
This adds a example to `assert.throws` to document checking against
error instances. It also makes it clearer what some arguments can
receive as input.

PR-URL: #19885
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Michaël Zasso <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Trivikram Kamat <[email protected]>
BridgeAR added a commit to BridgeAR/node that referenced this pull request May 1, 2018
This adds direct promise support to `assert.rejects` and
`assert.doesNotReject`. It will now accept both, functions and ES2015
promises as input.

Besides this the documentation was updated to reflect the latest
changes.

It also refactors the tests to a non blocking way to improve the
execution performance and improves the coverage.

PR-URL: nodejs#19885
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Michaël Zasso <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Trivikram Kamat <[email protected]>
BridgeAR added a commit to BridgeAR/node that referenced this pull request May 1, 2018
This makes sure `assert.rejects` is always called with a second
argument. Besides that it is also changes a eslint error message
to suggest objects instead of a regular expression.

PR-URL: nodejs#19885
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Michaël Zasso <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Trivikram Kamat <[email protected]>
BridgeAR added a commit to BridgeAR/node that referenced this pull request May 1, 2018
This adds a example to `assert.throws` to document checking against
error instances. It also makes it clearer what some arguments can
receive as input.

PR-URL: nodejs#19885
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Michaël Zasso <[email protected]>
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Trivikram Kamat <[email protected]>
@BridgeAR BridgeAR deleted the improve-assert-rejects branch April 1, 2019 23:40
@BridgeAR BridgeAR restored the improve-assert-rejects branch April 1, 2019 23:40
@BridgeAR BridgeAR deleted the improve-assert-rejects branch April 1, 2019 23:40
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. author ready PRs that have at least one approval, no pending requests for changes, and a CI started. semver-minor PRs that contain new features and should be released in the next minor version. tools Issues and PRs related to the tools directory.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants