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 ifError #18247

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
21 changes: 14 additions & 7 deletions doc/api/assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,25 +433,32 @@ suppressFrame();
## assert.ifError(value)
<!-- YAML
added: v0.1.97
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/18247
description: Instead of throwing the original error it is now wrapped into
a AssertionError that contains the full stack trace.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/18247
description: Value may now only be `undefined` or `null`. Before any truthy
input was accepted.
-->
* `value` {any}

Throws `value` if `value` is truthy. This is useful when testing the `error`
argument in callbacks.
Throws `value` if `value` is not `undefined` or `null`. This is useful when
testing the `error` argument in callbacks.

```js
const assert = require('assert').strict;

assert.ifError(null);
// OK
assert.ifError(0);
// OK
assert.ifError(1);
// Throws 1
// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 0
assert.ifError('error');
// Throws 'error'
// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 'error'
assert.ifError(new Error());
// Throws Error
// AssertionError [ERR_ASSERTION]: ifError got unwanted exception: Error
```

## assert.notDeepEqual(actual, expected[, message])
Expand Down
72 changes: 59 additions & 13 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const {
isDeepEqual,
isDeepStrictEqual
} = require('internal/util/comparisons');
const errors = require('internal/errors');
const { AssertionError, TypeError } = require('internal/errors');
const { openSync, closeSync, readSync } = require('fs');
const { parseExpressionAt } = require('internal/deps/acorn/dist/acorn');
const { inspect } = require('util');
Expand Down Expand Up @@ -65,7 +65,7 @@ const NO_EXCEPTION_SENTINEL = {};
function innerFail(obj) {
if (obj.message instanceof Error) throw obj.message;

throw new errors.AssertionError(obj);
throw new AssertionError(obj);
}

function fail(actual, expected, message, operator, stackStartFn) {
Expand Down Expand Up @@ -95,7 +95,7 @@ assert.fail = fail;
// new assert.AssertionError({ message: message,
// actual: actual,
// expected: expected });
assert.AssertionError = errors.AssertionError;
assert.AssertionError = AssertionError;

function getBuffer(fd, assertLine) {
var lines = 0;
Expand Down Expand Up @@ -134,7 +134,7 @@ function innerOk(args, fn) {
var [value, message] = args;

if (args.length === 0)
throw new errors.TypeError('ERR_MISSING_ARGS', 'value');
throw new TypeError('ERR_MISSING_ARGS', 'value');

if (!value) {
if (message == null) {
Expand Down Expand Up @@ -338,8 +338,8 @@ function expectedException(actual, expected, msg) {
return expected.test(actual);
// assert.doesNotThrow does not accept objects.
if (arguments.length === 2) {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'expected',
['Function', 'RegExp'], expected);
throw new TypeError('ERR_INVALID_ARG_TYPE', 'expected',
['Function', 'RegExp'], expected);
}
// The name and message could be non enumerable. Therefore test them
// explicitly.
Expand Down Expand Up @@ -376,8 +376,8 @@ function expectedException(actual, expected, msg) {

function getActual(block) {
if (typeof block !== 'function') {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'block', 'Function',
block);
throw new TypeError('ERR_INVALID_ARG_TYPE', 'block', 'Function',
block);
}
try {
block();
Expand All @@ -393,10 +393,10 @@ assert.throws = function throws(block, error, message) {

if (typeof error === 'string') {
if (arguments.length === 3)
throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
'error',
['Function', 'RegExp'],
error);
throw new TypeError('ERR_INVALID_ARG_TYPE',
'error',
['Function', 'RegExp'],
error);

message = error;
error = null;
Expand Down Expand Up @@ -444,7 +444,53 @@ assert.doesNotThrow = function doesNotThrow(block, error, message) {
throw actual;
};

assert.ifError = function ifError(err) { if (err) throw err; };
assert.ifError = function ifError(err) {
if (err !== null && err !== undefined) {
let message = 'ifError got unwanted exception: ';
if (typeof err === 'object' && typeof err.message === 'string') {
if (err.message.length === 0 && err.constructor) {
message += err.constructor.name;
} else {
message += err.message;
}
} else {
message += inspect(err);
}

const newErr = new AssertionError({
actual: err,
expected: null,
operator: 'ifError',
message,
stackStartFn: ifError
});

// Make sure we actually have a stack trace!
const origStack = err.stack;

if (typeof origStack === 'string') {
// This will remove any duplicated frames from the error frames taken
// from within `ifError` and add the original error frames to the newly
// created ones.
const tmp2 = origStack.split('\n');
tmp2.shift();
// Filter all frames existing in err.stack.
let tmp1 = newErr.stack.split('\n');
for (var i = 0; i < tmp2.length; i++) {
// Find the first occurrence of the frame.
const pos = tmp1.indexOf(tmp2[i]);
if (pos !== -1) {
// Only keep new frames.
tmp1 = tmp1.slice(0, pos);
break;
}
}
newErr.stack = `${tmp1.join('\n')}\n${tmp2.join('\n')}`;
}
Copy link
Member

Choose a reason for hiding this comment

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

the goal of this code is not really clear, can you add some comments?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.


throw newErr;
}
};

// Expose a strict only variant of assert
function strict(...args) {
Expand Down
6 changes: 3 additions & 3 deletions test/fixtures/uncaught-exceptions/callbackify2.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ const { callbackify } = require('util');
{
const sentinel = new Error(__filename);
process.once('uncaughtException', (err) => {
assert.strictEqual(err, sentinel);
assert.notStrictEqual(err, sentinel);
// Calling test will use `stdout` to assert value of `err.message`
console.log(err.message);
});

async function fn() {
return await Promise.reject(sentinel);
function fn() {
return Promise.reject(sentinel);
}

const cbFn = callbackify(fn);
Expand Down
2 changes: 1 addition & 1 deletion test/message/error_exit.out
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Exiting with code=1
assert.js:*
throw new errors.AssertionError(obj);
throw new AssertionError(obj);
^

AssertionError [ERR_ASSERTION]: 1 strictEqual 2
Expand Down
22 changes: 22 additions & 0 deletions test/message/if-error-has-good-stack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

require('../common');
const assert = require('assert');

let err;
// Create some random error frames.
(function a() {
(function b() {
(function c() {
err = new Error('test error');
})();
})();
})();

(function x() {
(function y() {
(function z() {
assert.ifError(err);
})();
})();
})();
19 changes: 19 additions & 0 deletions test/message/if-error-has-good-stack.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
assert.js:*
throw newErr;
^

AssertionError [ERR_ASSERTION]: ifError got unwanted exception: test error
at z (*/if-error-has-good-stack.js:*:*
at y (*/if-error-has-good-stack.js:*:*)
at x (*/if-error-has-good-stack.js:*:*)
at Object.<anonymous> (*/if-error-has-good-stack.js:*:*)
at c (*/if-error-has-good-stack.js:*:*)
at b (*/if-error-has-good-stack.js:*:*)
at a (*/if-error-has-good-stack.js:*:*)
at Object.<anonymous> (*/if-error-has-good-stack.js:*:*)
at Module._compile (module.js:*:*)
at Object.Module._extensions..js (module.js:*:*)
at Module.load (module.js:*:*)
at tryModuleLoad (module.js:*:*)
at Function.Module._load (module.js:*:*)
at Function.Module.runMain (module.js:*:*)
88 changes: 88 additions & 0 deletions test/parallel/test-assert-if-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use strict';

require('../common');
const assert = require('assert').strict;
/* eslint-disable no-restricted-properties */

// Test that assert.ifError has the correct stack trace of both stacks.

let err;
// Create some random error frames.
(function a() {
(function b() {
(function c() {
err = new Error('test error');
})();
})();
})();

const msg = err.message;
const stack = err.stack;

(function x() {
(function y() {
(function z() {
let threw = false;
try {
assert.ifError(err);
} catch (e) {
assert.equal(e.message, 'ifError got unwanted exception: test error');
assert.equal(err.message, msg);
assert.equal(e.actual, err);
assert.equal(e.actual.stack, stack);
assert.equal(e.expected, null);
assert.equal(e.operator, 'ifError');
threw = true;
}
assert(threw);
})();
})();
})();
Copy link
Member

Choose a reason for hiding this comment

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

I would prefer a test showing a full stack trace, rather than this check. It would mean future edits are easier to understand and fix.

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 added a separate test to check for the stack trace and minimized this test to the necessary parts.


assert.throws(
() => assert.ifError(new TypeError()),
{
message: 'ifError got unwanted exception: TypeError'
}
);

assert.throws(
() => assert.ifError({ stack: false }),
{
message: 'ifError got unwanted exception: { stack: false }'
}
);

assert.throws(
() => assert.ifError({ constructor: null, message: '' }),
{
message: 'ifError got unwanted exception: '
}
);

assert.throws(
() => { assert.ifError(false); },
{
message: 'ifError got unwanted exception: false'
}
);

assert.doesNotThrow(() => { assert.ifError(null); });
assert.doesNotThrow(() => { assert.ifError(); });
assert.doesNotThrow(() => { assert.ifError(undefined); });

// https://github.com/nodejs/node-v0.x-archive/issues/2893
{
let threw = false;
try {
// eslint-disable-next-line no-restricted-syntax
assert.throws(() => {
assert.ifError(null);
});
} catch (e) {
threw = true;
assert.strictEqual(e.message, 'Missing expected exception.');
assert(!e.stack.includes('throws'), e.stack);
}
assert(threw);
}
21 changes: 0 additions & 21 deletions test/parallel/test-assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -452,11 +452,6 @@ assert.throws(makeBlock(thrower, TypeError));
'a.doesNotThrow is not catching type matching errors');
}

assert.throws(() => { assert.ifError(new Error('test error')); },
/^Error: test error$/);
assert.doesNotThrow(() => { assert.ifError(null); });
assert.doesNotThrow(() => { assert.ifError(); });

common.expectsError(
() => assert.doesNotThrow(makeBlock(thrower, Error), 'user message'),
{
Expand Down Expand Up @@ -602,22 +597,6 @@ testAssertionMessage({ a: undefined, b: null }, '{ a: undefined, b: null }');
testAssertionMessage({ a: NaN, b: Infinity, c: -Infinity },
'{ a: NaN, b: Infinity, c: -Infinity }');

// https://github.com/nodejs/node-v0.x-archive/issues/2893
{
let threw = false;
try {
// eslint-disable-next-line no-restricted-syntax
assert.throws(() => {
assert.ifError(null);
});
} catch (e) {
threw = true;
assert.strictEqual(e.message, 'Missing expected exception.');
assert.ok(!e.stack.includes('throws'), e.stack);
}
assert.ok(threw);
}

// https://github.com/nodejs/node-v0.x-archive/issues/5292
try {
assert.strictEqual(1, 2);
Expand Down
6 changes: 3 additions & 3 deletions test/parallel/test-domain-implicit-fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ d.on('error', common.mustCall(function(er) {
assert.strictEqual(er.domain, d);
assert.strictEqual(er.domainThrown, true);
assert.ok(!er.domainEmitter);
assert.strictEqual(er.code, 'ENOENT');
assert.ok(/\bthis file does not exist\b/i.test(er.path));
assert.strictEqual(typeof er.errno, 'number');
assert.strictEqual(er.actual.code, 'ENOENT');
assert.ok(/\bthis file does not exist\b/i.test(er.actual.path));
assert.strictEqual(typeof er.actual.errno, 'number');
}));


Expand Down
4 changes: 3 additions & 1 deletion test/parallel/test-util-callbackify.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,9 @@ const values = [
[fixture],
common.mustCall((err, stdout, stderr) => {
assert.ifError(err);
assert.strictEqual(stdout.trim(), fixture);
assert.strictEqual(
stdout.trim(),
`ifError got unwanted exception: ${fixture}`);
assert.strictEqual(stderr, '');
})
);
Expand Down
4 changes: 2 additions & 2 deletions test/sequential/test-inspector-port-zero.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ function test(arg, port = '') {
let stderr = '';
proc.stdout.on('data', (data) => stdout += data);
proc.stderr.on('data', (data) => stderr += data);
proc.stdout.on('close', assert.ifError);
proc.stderr.on('close', assert.ifError);
proc.stdout.on('close', (hadErr) => assert(!hadErr));
proc.stderr.on('close', (hadErr) => assert(!hadErr));
proc.stderr.on('data', () => {
if (!stderr.includes('\n')) return;
assert(/Debugger listening on (.+)/.test(stderr));
Expand Down