assert: improve simple assert
This improves the error message in simple asserts by using the real call information instead of the already evaluated part. PR-URL: https://github.com/nodejs/node/pull/17581 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Ron Korving <ron@ronkorving.nl>
This commit is contained in:
parent
27925c4086
commit
f76ef50432
@ -658,6 +658,9 @@ parameter is `undefined`, a default error message is assigned. If the `message`
|
|||||||
parameter is an instance of an [`Error`][] then it will be thrown instead of the
|
parameter is an instance of an [`Error`][] then it will be thrown instead of the
|
||||||
`AssertionError`.
|
`AssertionError`.
|
||||||
|
|
||||||
|
Be aware that in the `repl` the error message will be different to the one
|
||||||
|
thrown in a file! See below for further details.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const assert = require('assert').strict;
|
const assert = require('assert').strict;
|
||||||
|
|
||||||
@ -665,12 +668,40 @@ assert.ok(true);
|
|||||||
// OK
|
// OK
|
||||||
assert.ok(1);
|
assert.ok(1);
|
||||||
// OK
|
// OK
|
||||||
assert.ok(false);
|
|
||||||
// throws "AssertionError: false == true"
|
|
||||||
assert.ok(0);
|
|
||||||
// throws "AssertionError: 0 == true"
|
|
||||||
assert.ok(false, 'it\'s false');
|
assert.ok(false, 'it\'s false');
|
||||||
// throws "AssertionError: it's false"
|
// throws "AssertionError: it's false"
|
||||||
|
|
||||||
|
// In the repl:
|
||||||
|
assert.ok(typeof 123 === 'string');
|
||||||
|
// throws:
|
||||||
|
// "AssertionError: false == true
|
||||||
|
|
||||||
|
// In a file (e.g. test.js):
|
||||||
|
assert.ok(typeof 123 === 'string');
|
||||||
|
// throws:
|
||||||
|
// "AssertionError: The expression evaluated to a falsy value:
|
||||||
|
//
|
||||||
|
// assert.ok(typeof 123 === 'string')
|
||||||
|
|
||||||
|
assert.ok(false);
|
||||||
|
// throws:
|
||||||
|
// "AssertionError: The expression evaluated to a falsy value:
|
||||||
|
//
|
||||||
|
// assert.ok(false)
|
||||||
|
|
||||||
|
assert.ok(0);
|
||||||
|
// throws:
|
||||||
|
// "AssertionError: The expression evaluated to a falsy value:
|
||||||
|
//
|
||||||
|
// assert.ok(0)
|
||||||
|
|
||||||
|
// Using `assert()` works the same:
|
||||||
|
assert(0);
|
||||||
|
// throws:
|
||||||
|
// "AssertionError: The expression evaluated to a falsy value:
|
||||||
|
//
|
||||||
|
// assert(0)
|
||||||
```
|
```
|
||||||
|
|
||||||
## assert.strictEqual(actual, expected[, message])
|
## assert.strictEqual(actual, expected[, message])
|
||||||
|
150
lib/assert.js
150
lib/assert.js
@ -20,10 +20,33 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { isDeepEqual, isDeepStrictEqual } =
|
const { Buffer } = require('buffer');
|
||||||
require('internal/util/comparisons');
|
const {
|
||||||
|
isDeepEqual,
|
||||||
|
isDeepStrictEqual
|
||||||
|
} = require('internal/util/comparisons');
|
||||||
const errors = require('internal/errors');
|
const errors = require('internal/errors');
|
||||||
|
const { openSync, closeSync, readSync } = require('fs');
|
||||||
|
const { parseExpressionAt } = require('internal/deps/acorn/dist/acorn');
|
||||||
const { inspect } = require('util');
|
const { inspect } = require('util');
|
||||||
|
const { EOL } = require('os');
|
||||||
|
|
||||||
|
const codeCache = new Map();
|
||||||
|
// Escape control characters but not \n and \t to keep the line breaks and
|
||||||
|
// indentation intact.
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
const escapeSequencesRegExp = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
|
||||||
|
const meta = [
|
||||||
|
'\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004',
|
||||||
|
'\\u0005', '\\u0006', '\\u0007', '\\b', '',
|
||||||
|
'', '\\u000b', '\\f', '', '\\u000e',
|
||||||
|
'\\u000f', '\\u0010', '\\u0011', '\\u0012', '\\u0013',
|
||||||
|
'\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018',
|
||||||
|
'\\u0019', '\\u001a', '\\u001b', '\\u001c', '\\u001d',
|
||||||
|
'\\u001e', '\\u001f'
|
||||||
|
];
|
||||||
|
|
||||||
|
const escapeFn = (str) => meta[str.charCodeAt(0)];
|
||||||
|
|
||||||
// The assert module provides functions that throw
|
// The assert module provides functions that throw
|
||||||
// AssertionError's when particular conditions are not met. The
|
// AssertionError's when particular conditions are not met. The
|
||||||
@ -74,20 +97,123 @@ assert.fail = fail;
|
|||||||
// expected: expected });
|
// expected: expected });
|
||||||
assert.AssertionError = errors.AssertionError;
|
assert.AssertionError = errors.AssertionError;
|
||||||
|
|
||||||
|
function getBuffer(fd, assertLine) {
|
||||||
|
var lines = 0;
|
||||||
|
// Prevent blocking the event loop by limiting the maximum amount of
|
||||||
|
// data that may be read.
|
||||||
|
var maxReads = 64; // bytesPerRead * maxReads = 512 kb
|
||||||
|
var bytesRead = 0;
|
||||||
|
var startBuffer = 0; // Start reading from that char on
|
||||||
|
const bytesPerRead = 8192;
|
||||||
|
const buffers = [];
|
||||||
|
do {
|
||||||
|
const buffer = Buffer.allocUnsafe(bytesPerRead);
|
||||||
|
bytesRead = readSync(fd, buffer, 0, bytesPerRead);
|
||||||
|
for (var i = 0; i < bytesRead; i++) {
|
||||||
|
if (buffer[i] === 10) {
|
||||||
|
lines++;
|
||||||
|
if (lines === assertLine) {
|
||||||
|
startBuffer = i + 1;
|
||||||
|
// Read up to 15 more lines to make sure all code gets matched
|
||||||
|
} else if (lines === assertLine + 16) {
|
||||||
|
buffers.push(buffer.slice(startBuffer, i));
|
||||||
|
return buffers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lines >= assertLine) {
|
||||||
|
buffers.push(buffer.slice(startBuffer, bytesRead));
|
||||||
|
// Reset the startBuffer in case we need more than one chunk
|
||||||
|
startBuffer = 0;
|
||||||
|
}
|
||||||
|
} while (--maxReads !== 0 && bytesRead !== 0);
|
||||||
|
return buffers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function innerOk(args, fn) {
|
||||||
|
var [value, message] = args;
|
||||||
|
|
||||||
// Pure assertion tests whether a value is truthy, as determined
|
|
||||||
// by !!value.
|
|
||||||
function ok(value, message) {
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
if (message == null) {
|
||||||
|
// Use the call as error message if possible.
|
||||||
|
// This does not work with e.g. the repl.
|
||||||
|
const err = new Error();
|
||||||
|
// Make sure the limit is set to 1. Otherwise it could fail (<= 0) or it
|
||||||
|
// does to much work.
|
||||||
|
const tmpLimit = Error.stackTraceLimit;
|
||||||
|
Error.stackTraceLimit = 1;
|
||||||
|
Error.captureStackTrace(err, fn);
|
||||||
|
Error.stackTraceLimit = tmpLimit;
|
||||||
|
|
||||||
|
const tmpPrepare = Error.prepareStackTrace;
|
||||||
|
Error.prepareStackTrace = (_, stack) => stack;
|
||||||
|
const call = err.stack[0];
|
||||||
|
Error.prepareStackTrace = tmpPrepare;
|
||||||
|
|
||||||
|
const filename = call.getFileName();
|
||||||
|
const line = call.getLineNumber() - 1;
|
||||||
|
const column = call.getColumnNumber() - 1;
|
||||||
|
const identifier = `${filename}${line}${column}`;
|
||||||
|
|
||||||
|
if (codeCache.has(identifier)) {
|
||||||
|
message = codeCache.get(identifier);
|
||||||
|
} else {
|
||||||
|
var fd;
|
||||||
|
try {
|
||||||
|
fd = openSync(filename, 'r', 0o666);
|
||||||
|
const buffers = getBuffer(fd, line);
|
||||||
|
const code = Buffer.concat(buffers).toString('utf8');
|
||||||
|
const nodes = parseExpressionAt(code, column);
|
||||||
|
// Node type should be "CallExpression" and some times
|
||||||
|
// "SequenceExpression".
|
||||||
|
const node = nodes.type === 'CallExpression' ?
|
||||||
|
nodes :
|
||||||
|
nodes.expressions[0];
|
||||||
|
// TODO: fix the "generatedMessage property"
|
||||||
|
// Since this is actually a generated message, it has to be
|
||||||
|
// determined differently from now on.
|
||||||
|
|
||||||
|
const name = node.callee.name;
|
||||||
|
// Calling `ok` with .apply or .call is uncommon but we use a simple
|
||||||
|
// safeguard nevertheless.
|
||||||
|
if (name !== 'apply' && name !== 'call') {
|
||||||
|
// Only use `assert` and `assert.ok` to reference the "real API" and
|
||||||
|
// not user defined function names.
|
||||||
|
const ok = name === 'ok' ? '.ok' : '';
|
||||||
|
const args = node.arguments;
|
||||||
|
message = code
|
||||||
|
.slice(args[0].start, args[args.length - 1].end)
|
||||||
|
.replace(escapeSequencesRegExp, escapeFn);
|
||||||
|
message = 'The expression evaluated to a falsy value:' +
|
||||||
|
`${EOL}${EOL} assert${ok}(${message})${EOL}`;
|
||||||
|
}
|
||||||
|
// Make sure to always set the cache! No matter if the message is
|
||||||
|
// undefined or not
|
||||||
|
codeCache.set(identifier, message);
|
||||||
|
} catch (e) {
|
||||||
|
// Invalidate cache to prevent trying to read this part again.
|
||||||
|
codeCache.set(identifier, undefined);
|
||||||
|
} finally {
|
||||||
|
if (fd !== undefined)
|
||||||
|
closeSync(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
innerFail({
|
innerFail({
|
||||||
actual: value,
|
actual: value,
|
||||||
expected: true,
|
expected: true,
|
||||||
message,
|
message,
|
||||||
operator: '==',
|
operator: '==',
|
||||||
stackStartFn: ok
|
stackStartFn: fn
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pure assertion tests whether a value is truthy, as determined
|
||||||
|
// by !!value.
|
||||||
|
function ok(...args) {
|
||||||
|
innerOk(args, ok);
|
||||||
|
}
|
||||||
assert.ok = ok;
|
assert.ok = ok;
|
||||||
|
|
||||||
// The equality assertion tests shallow, coercive equality with ==.
|
// The equality assertion tests shallow, coercive equality with ==.
|
||||||
@ -318,16 +444,8 @@ assert.doesNotThrow = function doesNotThrow(block, error, message) {
|
|||||||
assert.ifError = function ifError(err) { if (err) throw err; };
|
assert.ifError = function ifError(err) { if (err) throw err; };
|
||||||
|
|
||||||
// Expose a strict only variant of assert
|
// Expose a strict only variant of assert
|
||||||
function strict(value, message) {
|
function strict(...args) {
|
||||||
if (!value) {
|
innerOk(args, strict);
|
||||||
innerFail({
|
|
||||||
actual: value,
|
|
||||||
expected: true,
|
|
||||||
message,
|
|
||||||
operator: '==',
|
|
||||||
stackStartFn: strict
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
assert.strict = Object.assign(strict, assert, {
|
assert.strict = Object.assign(strict, assert, {
|
||||||
equal: assert.strictEqual,
|
equal: assert.strictEqual,
|
||||||
|
@ -138,7 +138,7 @@ class AssertionError extends Error {
|
|||||||
throw new exports.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'Object');
|
throw new exports.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'Object');
|
||||||
}
|
}
|
||||||
var { actual, expected, message, operator, stackStartFn } = options;
|
var { actual, expected, message, operator, stackStartFn } = options;
|
||||||
if (message) {
|
if (message != null) {
|
||||||
super(message);
|
super(message);
|
||||||
} else {
|
} else {
|
||||||
if (actual && actual.stack && actual instanceof Error)
|
if (actual && actual.stack && actual instanceof Error)
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
const common = require('../common');
|
const common = require('../common');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
const { EOL } = require('os');
|
||||||
const a = assert;
|
const a = assert;
|
||||||
|
|
||||||
function makeBlock(f) {
|
function makeBlock(f) {
|
||||||
@ -753,14 +754,24 @@ common.expectsError(
|
|||||||
assert.equal(Object.keys(assert).length, Object.keys(a).length);
|
assert.equal(Object.keys(assert).length, Object.keys(a).length);
|
||||||
/* eslint-enable no-restricted-properties */
|
/* eslint-enable no-restricted-properties */
|
||||||
assert(7);
|
assert(7);
|
||||||
|
|
||||||
|
// Test setting the limit to zero and that assert.strict works properly.
|
||||||
|
const tmpLimit = Error.stackTraceLimit;
|
||||||
|
Error.stackTraceLimit = 0;
|
||||||
common.expectsError(
|
common.expectsError(
|
||||||
() => assert(),
|
() => {
|
||||||
|
assert.ok(
|
||||||
|
typeof 123 === 'string'
|
||||||
|
);
|
||||||
|
},
|
||||||
{
|
{
|
||||||
code: 'ERR_ASSERTION',
|
code: 'ERR_ASSERTION',
|
||||||
type: assert.AssertionError,
|
type: assert.AssertionError,
|
||||||
message: 'undefined == true'
|
message: `The expression evaluated to a falsy value:${EOL}${EOL} ` +
|
||||||
|
`assert.ok(typeof 123 === 'string')${EOL}`
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
Error.stackTraceLimit = tmpLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
common.expectsError(
|
common.expectsError(
|
||||||
@ -768,7 +779,108 @@ common.expectsError(
|
|||||||
{
|
{
|
||||||
code: 'ERR_ASSERTION',
|
code: 'ERR_ASSERTION',
|
||||||
type: assert.AssertionError,
|
type: assert.AssertionError,
|
||||||
message: 'null == true'
|
message: `The expression evaluated to a falsy value:${EOL}${EOL} ` +
|
||||||
|
`assert.ok(null)${EOL}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
common.expectsError(
|
||||||
|
() => assert(typeof 123 === 'string'),
|
||||||
|
{
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
type: assert.AssertionError,
|
||||||
|
message: `The expression evaluated to a falsy value:${EOL}${EOL} ` +
|
||||||
|
`assert(typeof 123 === 'string')${EOL}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
// Test caching
|
||||||
|
const fs = process.binding('fs');
|
||||||
|
const tmp = fs.close;
|
||||||
|
fs.close = common.mustCall(tmp, 1);
|
||||||
|
function throwErr() {
|
||||||
|
// eslint-disable-next-line prefer-assert-methods
|
||||||
|
assert(
|
||||||
|
(Buffer.from('test') instanceof Error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
common.expectsError(
|
||||||
|
() => throwErr(),
|
||||||
|
{
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
type: assert.AssertionError,
|
||||||
|
message: `The expression evaluated to a falsy value:${EOL}${EOL} ` +
|
||||||
|
`assert(Buffer.from('test') instanceof Error)${EOL}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
common.expectsError(
|
||||||
|
() => throwErr(),
|
||||||
|
{
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
type: assert.AssertionError,
|
||||||
|
message: `The expression evaluated to a falsy value:${EOL}${EOL} ` +
|
||||||
|
`assert(Buffer.from('test') instanceof Error)${EOL}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
fs.close = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
common.expectsError(
|
||||||
|
() => {
|
||||||
|
a(
|
||||||
|
(() => 'string')()
|
||||||
|
// eslint-disable-next-line
|
||||||
|
===
|
||||||
|
123 instanceof
|
||||||
|
Buffer
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
type: assert.AssertionError,
|
||||||
|
message: `The expression evaluated to a falsy value:${EOL}${EOL} ` +
|
||||||
|
`assert((() => 'string')()${EOL}` +
|
||||||
|
` // eslint-disable-next-line${EOL}` +
|
||||||
|
` ===${EOL}` +
|
||||||
|
` 123 instanceof${EOL}` +
|
||||||
|
` Buffer)${EOL}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
common.expectsError(
|
||||||
|
() => assert(null, undefined),
|
||||||
|
{
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
type: assert.AssertionError,
|
||||||
|
message: `The expression evaluated to a falsy value:${EOL}${EOL} ` +
|
||||||
|
`assert(null, undefined)${EOL}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
common.expectsError(
|
||||||
|
() => assert.ok.apply(null, [0]),
|
||||||
|
{
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
type: assert.AssertionError,
|
||||||
|
message: '0 == true'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
common.expectsError(
|
||||||
|
() => assert.ok.call(null, 0),
|
||||||
|
{
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
type: assert.AssertionError,
|
||||||
|
message: '0 == true'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
common.expectsError(
|
||||||
|
() => assert.ok.call(null, 0, 'test'),
|
||||||
|
{
|
||||||
|
code: 'ERR_ASSERTION',
|
||||||
|
type: assert.AssertionError,
|
||||||
|
message: 'test'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user