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

Update: More detailed assert message for rule-tester #9769

Merged
merged 4 commits into from Jan 4, 2018

Conversation

starkwang
Copy link

What is the purpose of this pull request? (put an "X" next to item)

[ ] Documentation update
[ ] Bug fix (template)
[ ] New rule (template)
[ ] Changes an existing rule (template)
[ ] Add autofixing to a rule
[ ] Add a CLI option
[X] Add something to the core
[ ] Other, please explain:

What changes did you make? (Give an overview)
The assert message now is a little bit confused. It only tells you "Output is incorrect.", but doesn't specificly show the received value and the expected value.

This change is to make the assert message more detailed.

Is there anything you'd like reviewers to focus on?
Not yet.

@not-an-aardvark
Copy link
Member

Thanks for the pull request!

This was actually proposed before in #8173. I think the reason you're seeing confusing error messages is because you're using RuleTester without a test runner. For example, when using mocha, the error message looks like this:

This is because mocha creates a diff based on the actual and expected properties of the AssertionError objects.

I don't think it would make sense to have the error message change for people using mocha, since it already ends up fairly descriptive. However, I agree that it would be a good idea to improve the error message in the case where no test runner is being used (by looking at the actual and expected properties like mocha does). Would you be able to update this PR to just change that case instead?

This function gets called when no test runner is active. (The first argument is the name of the test case, and the second argument is a function that may or may not throw an AssertionError.) I think this could be fixed by adding a try-catch to that function, and rethrowing any thrown AssertionError with a more descriptive message.

@starkwang starkwang force-pushed the detailed-assert-msg branch 2 times, most recently from 549e40a to 7cfa106 Compare December 25, 2017 08:48
@starkwang
Copy link
Author

@not-an-aardvark Thanks for your comment!
I've just updated this PR to address comment.

Copy link
Member

@platinumazure platinumazure left a comment

Choose a reason for hiding this comment

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

Could you please add or modify a unit test demonstrating the changes (lack of message, explicitly check for actual/expected/operator, etc.)? Thanks!

@starkwang
Copy link
Author

starkwang commented Dec 25, 2017

@platinumazure Thanks for review.

This change is only for the case where no test runner is being used. But our unit tests seem based on some runners (hope I don't miss something), so it actually doesn't break any unit test now.

Is there any test for the no-test-runner case to add in?

@platinumazure
Copy link
Member

Are there any tests for RuleTester.describe and RuleTester.it now, with them being called directly rather than overwritten? If so, it seems like those would be a good place to test. If there aren't tests (possible since the default behavior is to proxy into the method passed in), we should add some.

actual: e.actual,
expected: e.expected,
operator: e.operator
});
Copy link
Member

Choose a reason for hiding this comment

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

I had been envisioning something more like this:

try {
    return method.apply(this);
} catch (err) {
    if (err instanceof assert.AssertionError) {
        throw Object.assign({}, new assert.AssertionError, { message: `${e.actual} ${e.operator} ${e.expected}` })
    }
    throw err;
}

That way, any unrelated errors (e.g. crashes in a rule) would still get passed through unmodified, and the original message (e.g. "output is incorrect") would still be present for additional context.

Copy link
Contributor

@BridgeAR BridgeAR Dec 26, 2017

Choose a reason for hiding this comment

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

@not-an-aardvark using a plain object as error object is not the same as throwing a new AssertionError. Using Object.assign is also not sufficient because stack and message are non-enumerable and would not be copied.

Copy link
Member

Choose a reason for hiding this comment

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

Oops, you're right; I had forgotten that the thrown object would no longer be an AssertionError. I think something like this could work:

try {
    return method.apply(this);
} catch (err) {
    if (err instanceof assert.AssertionError) {
        err.message += `${util.inspect(err.actual)} ${err.operator} ${util.inspect(err.expected)}`;
    }
    throw err;
}

To me, the important constraints are:

  • We don't add redundant info in the case where a test runner is being used (this is by far the most common case)
  • The error message still contains information about what the problem is (e.g. it should still say something like "output is incorrect" when the output is incorrect, rather than just displaying a diff between two strings)

@not-an-aardvark
Copy link
Member

@platinumazure I agree that it would be good to have tests like that, although I don't know how we would actually create them. RuleTester selects a behavior based on the presence of describe and it globals, and those globals are already present when we are running our tests.

@j-f1
Copy link
Contributor

j-f1 commented Dec 25, 2017

You could remove the globals, require RuleTester, then reinstate them.

@starkwang
Copy link
Author

Pushed commit to address comment.
@not-an-aardvark @platinumazure Please take another review : )

Copy link
Member

@platinumazure platinumazure left a comment

Choose a reason for hiding this comment

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

LGTM, just one small thing but not a blocker at all. Thanks for adding a test, as well.

it = null;
describe = null;

const ruleTester = new RuleTester();
Copy link
Member

Choose a reason for hiding this comment

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

I'd feel better if this logic (from here to the end of the assertion) were wrapped in a try/finally so that the globals are definitely reassigned even if an exception is thrown.

Copy link
Author

Choose a reason for hiding this comment

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

I've added the try/finally : )

Copy link
Member

@platinumazure platinumazure left a comment

Choose a reason for hiding this comment

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

LGTM! Note that you could omit the catch clause (try/finally are legal without catch. A try statement just needs at least one catch or finally).

return method.apply(this);
} catch (err) {
if (err instanceof assert.AssertionError) {
err.message = `${util.inspect(err.actual)} ${err.operator} ${util.inspect(err.expected)}`;
Copy link
Member

Choose a reason for hiding this comment

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

Can you make this append to the existing message rather than overwriting it? I think it would be good to retain the "Output is incorrect" text to make the source of the error clearer.

Copy link
Author

Choose a reason for hiding this comment

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

Oh, I forgot to explain here.
The defaultHandler is called nestedly somehow (sorry I don't dive into the code but its behaviour shows that), so the thrown error will be caught and err message
will be appended in multiple times. It will finally become like

"Output is incorrect 'a' == 'b' 'a' == 'b' 'a' == 'b'"

That's the reason for overwriting it here.

Another solution is adding a flag like appended on error object to avoid multiple appending. But is it worth to do it?

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I see. I think that's happening because defaultHandler is used for both the "describe" and the "it" methods. So effectively something like this is happening:

defaultHandler("foo", () => {
    defaultHandler("bar", () => {
        // throw assertion error
    });
});

One solution would be to use the old default handler (which just calls the function in the second argument) as the default for describe, and use the handler that appends a diff as the default for it.

@starkwang
Copy link
Author

starkwang commented Dec 27, 2017

Actually I have another idea to implement this (I think this is better):

This is the code we using now:

assert.equal(fixResult.output, item.output, "Output is incorrect.");

We can change it into:

const message = "Output is incorrect."
const inTestRunner = typeof describe === "function" || typeof it === "function"
if (!inTestRunner) {
    // if not using test runner, the assert message should be appended by diff message.
    message += ` Expected: ${item.output} Received: ${fixResult.output}`
}
assert.equal(fixResult.output, item.output, message);

@not-an-aardvark What do you think about this? : )

@platinumazure
Copy link
Member

platinumazure commented Dec 27, 2017 via email

@starkwang
Copy link
Author

Updated PR to address comment. Please take another look. 😀

Copy link
Member

@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.

LGTM, thanks!

Copy link
Member

@platinumazure platinumazure left a comment

Choose a reason for hiding this comment

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

LGTM except for one small issue with the exception message. Thanks!

return method.apply(this);
} catch (err) {
if (err instanceof assert.AssertionError) {
err.message += ` ${util.inspect(err.actual)} ${err.operator} ${util.inspect(err.expected)}`;
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 surrounding the comparison in parentheses would be useful and make the error message more readable: (${util.inspect(err.actual)} ${err.operator} ${util.inspect(err.expected)})

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for review. I've added the parentheses :-)

Copy link
Member

@platinumazure platinumazure left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

Copy link
Member

@ilyavolodin ilyavolodin left a comment

Choose a reason for hiding this comment

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

Thanks for PR

@ilyavolodin ilyavolodin merged commit c64195f into eslint:master Jan 4, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
archived due to age This issue has been archived; please open a new issue for any further discussion
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants