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 assertion errors #27781

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions doc/api/assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -1210,10 +1210,14 @@ assert.throws(
() => {
throw new Error('Wrong value');
},
function(err) {
if ((err instanceof Error) && /value/.test(err)) {
return true;
}
(err) => {
assert(err instanceof Error);
assert(/value/.test(err));
// Returning anything from validation functions besides `true` is not
// recommended. Doing so results in the caught error being thrown again.
// That is mostly not the desired outcome. Throw an error about the specific
BridgeAR marked this conversation as resolved.
Show resolved Hide resolved
// validation that failed instead (as done in this example).
return true;
},
'unexpected error'
);
Expand Down
126 changes: 88 additions & 38 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,24 +269,31 @@ function getErrMessage(message, fn) {
Error.prepareStackTrace = tmpPrepare;

const filename = call.getFileName();

if (!filename) {
return message;
}

const line = call.getLineNumber() - 1;
let column = call.getColumnNumber() - 1;
let identifier;
let code;

const identifier = `${filename}${line}${column}`;
if (filename) {
identifier = `${filename}${line}${column}`;

if (errorCache.has(identifier)) {
return errorCache.get(identifier);
// Skip Node.js modules!
if (filename.endsWith('.js') &&
NativeModule.exists(filename.slice(0, -3))) {
errorCache.set(identifier, undefined);
return;
}
} else {
const fn = call.getFunction();
if (!fn) {
return message;
}
code = String(fn);
identifier = `${code}${line}${column}`;
}

// Skip Node.js modules!
if (filename.endsWith('.js') && NativeModule.exists(filename.slice(0, -3))) {
errorCache.set(identifier, undefined);
return;
if (errorCache.has(identifier)) {
return errorCache.get(identifier);
}

let fd;
Expand All @@ -295,16 +302,22 @@ function getErrMessage(message, fn) {
// errors are handled faster.
Error.stackTraceLimit = 0;

if (decoder === undefined) {
const { StringDecoder } = require('string_decoder');
decoder = new StringDecoder('utf8');
if (filename) {
if (decoder === undefined) {
const { StringDecoder } = require('string_decoder');
decoder = new StringDecoder('utf8');
}
fd = openSync(filename, 'r', 0o666);
// Reset column and message.
[column, message] = getCode(fd, line, column);
// Flush unfinished multi byte characters.
decoder.end();
} else {
for (let i = 0; i < line; i++) {
code = code.slice(code.indexOf('\n') + 1);
}
[column, message] = parseCode(code, column);
}

fd = openSync(filename, 'r', 0o666);
// Reset column and message.
[column, message] = getCode(fd, line, column);
// Flush unfinished multi byte characters.
decoder.end();
// Always normalize indentation, otherwise the message could look weird.
if (message.includes('\n')) {
if (EOL === '\r\n') {
Expand Down Expand Up @@ -549,30 +562,38 @@ function compareExceptionKey(actual, expected, key, message, keys, fn) {
}
}

function expectedException(actual, expected, msg, fn) {
function expectedException(actual, expected, message, fn) {
if (typeof expected !== 'function') {
if (isRegExp(expected))
return expected.test(actual);
// assert.doesNotThrow does not accept objects.
if (arguments.length === 2) {
throw new ERR_INVALID_ARG_TYPE(
'expected', ['Function', 'RegExp'], expected
);
// Handle regular expressions.
if (isRegExp(expected)) {
const str = String(actual);
if (expected.test(str))
return;

throw new AssertionError({
actual,
expected,
message: message || 'The input did not match the regular expression ' +
`${inspect(expected)}. Input:\n\n${inspect(str)}\n`,
operator: fn.name,
stackStartFn: fn
});
}

// Handle primitives properly.
if (typeof actual !== 'object' || actual === null) {
const err = new AssertionError({
actual,
expected,
message: msg,
message,
operator: 'deepStrictEqual',
stackStartFn: fn
});
err.operator = fn.name;
throw err;
}

// Handle validation objects.
const keys = Object.keys(expected);
// Special handle errors to make sure the name and the message are compared
// as well.
Expand All @@ -589,18 +610,25 @@ function expectedException(actual, expected, msg, fn) {
expected[key].test(actual[key])) {
continue;
}
compareExceptionKey(actual, expected, key, msg, keys, fn);
compareExceptionKey(actual, expected, key, message, keys, fn);
}
return true;
return;
}

// Guard instanceof against arrow functions as they don't have a prototype.
// Check for matching Error classes.
if (expected.prototype !== undefined && actual instanceof expected) {
return true;
return;
}
if (Error.isPrototypeOf(expected)) {
return false;
throw actual;
}

// Check validation functions return value.
const res = expected.call({}, actual);
if (res !== true) {
throw actual;
}
return expected.call({}, actual) === true;
}

function getActual(fn) {
Expand Down Expand Up @@ -695,9 +723,31 @@ function expectsError(stackStartFn, actual, error, message) {
stackStartFn
});
}
if (error && !expectedException(actual, error, message, stackStartFn)) {
throw actual;

if (!error)
return;

expectedException(actual, error, message, stackStartFn);
}

function hasMatchingError(actual, expected) {
if (typeof expected !== 'function') {
if (isRegExp(expected)) {
const str = String(actual);
return expected.test(str);
}
throw new ERR_INVALID_ARG_TYPE(
'expected', ['Function', 'RegExp'], expected
);
}
// Guard instanceof against arrow functions as they don't have a prototype.
if (expected.prototype !== undefined && actual instanceof expected) {
return true;
}
if (Error.isPrototypeOf(expected)) {
return false;
}
return expected.call({}, actual) === true;
}

function expectsNoError(stackStartFn, actual, error, message) {
Expand All @@ -709,7 +759,7 @@ function expectsNoError(stackStartFn, actual, error, message) {
error = undefined;
}

if (!error || expectedException(actual, error)) {
if (!error || hasMatchingError(actual, error)) {
const details = message ? `: ${message}` : '.';
const fnType = stackStartFn.name === 'doesNotReject' ?
'rejection' : 'exception';
Expand Down
29 changes: 28 additions & 1 deletion test/parallel/test-assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,27 @@ assert.throws(
}

// Use a RegExp to validate the error message.
a.throws(() => thrower(TypeError), /\[object Object\]/);
{
a.throws(() => thrower(TypeError), /\[object Object\]/);

const symbol = Symbol('foo');
a.throws(() => {
throw symbol;
}, /foo/);

a.throws(() => {
a.throws(() => {
throw symbol;
}, /abc/);
}, {
message: 'The input did not match the regular expression /abc/. ' +
"Input:\n\n'Symbol(foo)'\n",
code: 'ERR_ASSERTION',
operator: 'throws',
actual: symbol,
expected: /abc/
});
}

// Use a fn to validate the error object.
a.throws(() => thrower(TypeError), (err) => {
Expand Down Expand Up @@ -834,6 +854,13 @@ common.expectsError(
{
code: 'ERR_ASSERTION',
type: assert.AssertionError,
message: 'The expression evaluated to a falsy value:\n\n assert(1 === 2)\n'
}
);
assert.throws(
() => eval('console.log("FOO");\nassert.ok(1 === 2);'),
{
code: 'ERR_ASSERTION',
message: 'false == true'
}
);
Expand Down