Skip to content

Commit f76ef50

Browse files
committed
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: nodejs#17581 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Ron Korving <ron@ronkorving.nl>
1 parent 27925c4 commit f76ef50

File tree

4 files changed

+285
-24
lines changed

4 files changed

+285
-24
lines changed

doc/api/assert.md

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -658,19 +658,50 @@ parameter is `undefined`, a default error message is assigned. If the `message`
658658
parameter is an instance of an [`Error`][] then it will be thrown instead of the
659659
`AssertionError`.
660660

661+
Be aware that in the `repl` the error message will be different to the one
662+
thrown in a file! See below for further details.
663+
661664
```js
662665
const assert = require('assert').strict;
663666

664667
assert.ok(true);
665668
// OK
666669
assert.ok(1);
667670
// OK
668-
assert.ok(false);
669-
// throws "AssertionError: false == true"
670-
assert.ok(0);
671-
// throws "AssertionError: 0 == true"
671+
672672
assert.ok(false, 'it\'s false');
673673
// throws "AssertionError: it's false"
674+
675+
// In the repl:
676+
assert.ok(typeof 123 === 'string');
677+
// throws:
678+
// "AssertionError: false == true
679+
680+
// In a file (e.g. test.js):
681+
assert.ok(typeof 123 === 'string');
682+
// throws:
683+
// "AssertionError: The expression evaluated to a falsy value:
684+
//
685+
// assert.ok(typeof 123 === 'string')
686+
687+
assert.ok(false);
688+
// throws:
689+
// "AssertionError: The expression evaluated to a falsy value:
690+
//
691+
// assert.ok(false)
692+
693+
assert.ok(0);
694+
// throws:
695+
// "AssertionError: The expression evaluated to a falsy value:
696+
//
697+
// assert.ok(0)
698+
699+
// Using `assert()` works the same:
700+
assert(0);
701+
// throws:
702+
// "AssertionError: The expression evaluated to a falsy value:
703+
//
704+
// assert(0)
674705
```
675706

676707
## assert.strictEqual(actual, expected[, message])

lib/assert.js

Lines changed: 134 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,33 @@
2020

2121
'use strict';
2222

23-
const { isDeepEqual, isDeepStrictEqual } =
24-
require('internal/util/comparisons');
23+
const { Buffer } = require('buffer');
24+
const {
25+
isDeepEqual,
26+
isDeepStrictEqual
27+
} = require('internal/util/comparisons');
2528
const errors = require('internal/errors');
29+
const { openSync, closeSync, readSync } = require('fs');
30+
const { parseExpressionAt } = require('internal/deps/acorn/dist/acorn');
2631
const { inspect } = require('util');
32+
const { EOL } = require('os');
33+
34+
const codeCache = new Map();
35+
// Escape control characters but not \n and \t to keep the line breaks and
36+
// indentation intact.
37+
// eslint-disable-next-line no-control-regex
38+
const escapeSequencesRegExp = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
39+
const meta = [
40+
'\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004',
41+
'\\u0005', '\\u0006', '\\u0007', '\\b', '',
42+
'', '\\u000b', '\\f', '', '\\u000e',
43+
'\\u000f', '\\u0010', '\\u0011', '\\u0012', '\\u0013',
44+
'\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018',
45+
'\\u0019', '\\u001a', '\\u001b', '\\u001c', '\\u001d',
46+
'\\u001e', '\\u001f'
47+
];
48+
49+
const escapeFn = (str) => meta[str.charCodeAt(0)];
2750

2851
// The assert module provides functions that throw
2952
// AssertionError's when particular conditions are not met. The
@@ -74,20 +97,123 @@ assert.fail = fail;
7497
// expected: expected });
7598
assert.AssertionError = errors.AssertionError;
7699

100+
function getBuffer(fd, assertLine) {
101+
var lines = 0;
102+
// Prevent blocking the event loop by limiting the maximum amount of
103+
// data that may be read.
104+
var maxReads = 64; // bytesPerRead * maxReads = 512 kb
105+
var bytesRead = 0;
106+
var startBuffer = 0; // Start reading from that char on
107+
const bytesPerRead = 8192;
108+
const buffers = [];
109+
do {
110+
const buffer = Buffer.allocUnsafe(bytesPerRead);
111+
bytesRead = readSync(fd, buffer, 0, bytesPerRead);
112+
for (var i = 0; i < bytesRead; i++) {
113+
if (buffer[i] === 10) {
114+
lines++;
115+
if (lines === assertLine) {
116+
startBuffer = i + 1;
117+
// Read up to 15 more lines to make sure all code gets matched
118+
} else if (lines === assertLine + 16) {
119+
buffers.push(buffer.slice(startBuffer, i));
120+
return buffers;
121+
}
122+
}
123+
}
124+
if (lines >= assertLine) {
125+
buffers.push(buffer.slice(startBuffer, bytesRead));
126+
// Reset the startBuffer in case we need more than one chunk
127+
startBuffer = 0;
128+
}
129+
} while (--maxReads !== 0 && bytesRead !== 0);
130+
return buffers;
131+
}
132+
133+
function innerOk(args, fn) {
134+
var [value, message] = args;
77135

78-
// Pure assertion tests whether a value is truthy, as determined
79-
// by !!value.
80-
function ok(value, message) {
81136
if (!value) {
137+
if (message == null) {
138+
// Use the call as error message if possible.
139+
// This does not work with e.g. the repl.
140+
const err = new Error();
141+
// Make sure the limit is set to 1. Otherwise it could fail (<= 0) or it
142+
// does to much work.
143+
const tmpLimit = Error.stackTraceLimit;
144+
Error.stackTraceLimit = 1;
145+
Error.captureStackTrace(err, fn);
146+
Error.stackTraceLimit = tmpLimit;
147+
148+
const tmpPrepare = Error.prepareStackTrace;
149+
Error.prepareStackTrace = (_, stack) => stack;
150+
const call = err.stack[0];
151+
Error.prepareStackTrace = tmpPrepare;
152+
153+
const filename = call.getFileName();
154+
const line = call.getLineNumber() - 1;
155+
const column = call.getColumnNumber() - 1;
156+
const identifier = `${filename}${line}${column}`;
157+
158+
if (codeCache.has(identifier)) {
159+
message = codeCache.get(identifier);
160+
} else {
161+
var fd;
162+
try {
163+
fd = openSync(filename, 'r', 0o666);
164+
const buffers = getBuffer(fd, line);
165+
const code = Buffer.concat(buffers).toString('utf8');
166+
const nodes = parseExpressionAt(code, column);
167+
// Node type should be "CallExpression" and some times
168+
// "SequenceExpression".
169+
const node = nodes.type === 'CallExpression' ?
170+
nodes :
171+
nodes.expressions[0];
172+
// TODO: fix the "generatedMessage property"
173+
// Since this is actually a generated message, it has to be
174+
// determined differently from now on.
175+
176+
const name = node.callee.name;
177+
// Calling `ok` with .apply or .call is uncommon but we use a simple
178+
// safeguard nevertheless.
179+
if (name !== 'apply' && name !== 'call') {
180+
// Only use `assert` and `assert.ok` to reference the "real API" and
181+
// not user defined function names.
182+
const ok = name === 'ok' ? '.ok' : '';
183+
const args = node.arguments;
184+
message = code
185+
.slice(args[0].start, args[args.length - 1].end)
186+
.replace(escapeSequencesRegExp, escapeFn);
187+
message = 'The expression evaluated to a falsy value:' +
188+
`${EOL}${EOL} assert${ok}(${message})${EOL}`;
189+
}
190+
// Make sure to always set the cache! No matter if the message is
191+
// undefined or not
192+
codeCache.set(identifier, message);
193+
} catch (e) {
194+
// Invalidate cache to prevent trying to read this part again.
195+
codeCache.set(identifier, undefined);
196+
} finally {
197+
if (fd !== undefined)
198+
closeSync(fd);
199+
}
200+
}
201+
}
82202
innerFail({
83203
actual: value,
84204
expected: true,
85205
message,
86206
operator: '==',
87-
stackStartFn: ok
207+
stackStartFn: fn
88208
});
89209
}
90210
}
211+
212+
// Pure assertion tests whether a value is truthy, as determined
213+
// by !!value.
214+
function ok(...args) {
215+
innerOk(args, ok);
216+
}
91217
assert.ok = ok;
92218

93219
// The equality assertion tests shallow, coercive equality with ==.
@@ -318,16 +444,8 @@ assert.doesNotThrow = function doesNotThrow(block, error, message) {
318444
assert.ifError = function ifError(err) { if (err) throw err; };
319445

320446
// Expose a strict only variant of assert
321-
function strict(value, message) {
322-
if (!value) {
323-
innerFail({
324-
actual: value,
325-
expected: true,
326-
message,
327-
operator: '==',
328-
stackStartFn: strict
329-
});
330-
}
447+
function strict(...args) {
448+
innerOk(args, strict);
331449
}
332450
assert.strict = Object.assign(strict, assert, {
333451
equal: assert.strictEqual,

lib/internal/errors.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ class AssertionError extends Error {
138138
throw new exports.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'Object');
139139
}
140140
var { actual, expected, message, operator, stackStartFn } = options;
141-
if (message) {
141+
if (message != null) {
142142
super(message);
143143
} else {
144144
if (actual && actual.stack && actual instanceof Error)

test/parallel/test-assert.js

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
const common = require('../common');
2727
const assert = require('assert');
28+
const { EOL } = require('os');
2829
const a = assert;
2930

3031
function makeBlock(f) {
@@ -753,22 +754,133 @@ common.expectsError(
753754
assert.equal(Object.keys(assert).length, Object.keys(a).length);
754755
/* eslint-enable no-restricted-properties */
755756
assert(7);
757+
758+
// Test setting the limit to zero and that assert.strict works properly.
759+
const tmpLimit = Error.stackTraceLimit;
760+
Error.stackTraceLimit = 0;
756761
common.expectsError(
757-
() => assert(),
762+
() => {
763+
assert.ok(
764+
typeof 123 === 'string'
765+
);
766+
},
758767
{
759768
code: 'ERR_ASSERTION',
760769
type: assert.AssertionError,
761-
message: 'undefined == true'
770+
message: `The expression evaluated to a falsy value:${EOL}${EOL} ` +
771+
`assert.ok(typeof 123 === 'string')${EOL}`
762772
}
763773
);
774+
Error.stackTraceLimit = tmpLimit;
764775
}
765776

766777
common.expectsError(
767778
() => assert.ok(null),
768779
{
769780
code: 'ERR_ASSERTION',
770781
type: assert.AssertionError,
771-
message: 'null == true'
782+
message: `The expression evaluated to a falsy value:${EOL}${EOL} ` +
783+
`assert.ok(null)${EOL}`
784+
}
785+
);
786+
common.expectsError(
787+
() => assert(typeof 123 === 'string'),
788+
{
789+
code: 'ERR_ASSERTION',
790+
type: assert.AssertionError,
791+
message: `The expression evaluated to a falsy value:${EOL}${EOL} ` +
792+
`assert(typeof 123 === 'string')${EOL}`
793+
}
794+
);
795+
796+
{
797+
// Test caching
798+
const fs = process.binding('fs');
799+
const tmp = fs.close;
800+
fs.close = common.mustCall(tmp, 1);
801+
function throwErr() {
802+
// eslint-disable-next-line prefer-assert-methods
803+
assert(
804+
(Buffer.from('test') instanceof Error)
805+
);
806+
}
807+
common.expectsError(
808+
() => throwErr(),
809+
{
810+
code: 'ERR_ASSERTION',
811+
type: assert.AssertionError,
812+
message: `The expression evaluated to a falsy value:${EOL}${EOL} ` +
813+
`assert(Buffer.from('test') instanceof Error)${EOL}`
814+
}
815+
);
816+
common.expectsError(
817+
() => throwErr(),
818+
{
819+
code: 'ERR_ASSERTION',
820+
type: assert.AssertionError,
821+
message: `The expression evaluated to a falsy value:${EOL}${EOL} ` +
822+
`assert(Buffer.from('test') instanceof Error)${EOL}`
823+
}
824+
);
825+
fs.close = tmp;
826+
}
827+
828+
common.expectsError(
829+
() => {
830+
a(
831+
(() => 'string')()
832+
// eslint-disable-next-line
833+
===
834+
123 instanceof
835+
Buffer
836+
);
837+
},
838+
{
839+
code: 'ERR_ASSERTION',
840+
type: assert.AssertionError,
841+
message: `The expression evaluated to a falsy value:${EOL}${EOL} ` +
842+
`assert((() => 'string')()${EOL}` +
843+
` // eslint-disable-next-line${EOL}` +
844+
` ===${EOL}` +
845+
` 123 instanceof${EOL}` +
846+
` Buffer)${EOL}`
847+
}
848+
);
849+
850+
common.expectsError(
851+
() => assert(null, undefined),
852+
{
853+
code: 'ERR_ASSERTION',
854+
type: assert.AssertionError,
855+
message: `The expression evaluated to a falsy value:${EOL}${EOL} ` +
856+
`assert(null, undefined)${EOL}`
857+
}
858+
);
859+
860+
common.expectsError(
861+
() => assert.ok.apply(null, [0]),
862+
{
863+
code: 'ERR_ASSERTION',
864+
type: assert.AssertionError,
865+
message: '0 == true'
866+
}
867+
);
868+
869+
common.expectsError(
870+
() => assert.ok.call(null, 0),
871+
{
872+
code: 'ERR_ASSERTION',
873+
type: assert.AssertionError,
874+
message: '0 == true'
875+
}
876+
);
877+
878+
common.expectsError(
879+
() => assert.ok.call(null, 0, 'test'),
880+
{
881+
code: 'ERR_ASSERTION',
882+
type: assert.AssertionError,
883+
message: 'test'
772884
}
773885
);
774886

0 commit comments

Comments
 (0)