Skip to content

Commit 2d9e876

Browse files
committed
assert: improve error messages
From now on all error messages produced by `assert` in strict mode will produce a error diff. PR-URL: #17615 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent cb88f35 commit 2d9e876

File tree

4 files changed

+332
-10
lines changed

4 files changed

+332
-10
lines changed

doc/api/assert.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ For more information about the used equality comparisons see
1717
<!-- YAML
1818
added: REPLACEME
1919
changes:
20+
- version: REPLACEME
21+
pr-url: https://github.com/nodejs/node/pull/REPLACEME
22+
description: Added error diffs to the strict mode
2023
- version: REPLACEME
2124
pr-url: https://github.com/nodejs/node/pull/17002
2225
description: Added strict mode to the assert module.
@@ -26,12 +29,42 @@ When using the `strict mode`, any `assert` function will use the equality used i
2629
the strict function mode. So [`assert.deepEqual()`][] will, for example, work the
2730
same as [`assert.deepStrictEqual()`][].
2831

32+
On top of that, error messages which involve objects produce an error diff
33+
instead of displaying both objects. That is not the case for the legacy mode.
34+
2935
It can be accessed using:
3036

3137
```js
3238
const assert = require('assert').strict;
3339
```
3440

41+
Example error diff (the `expected`, `actual`, and `Lines skipped` will be on a
42+
single row):
43+
44+
```js
45+
const assert = require('assert').strict;
46+
47+
assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]);
48+
```
49+
50+
```diff
51+
AssertionError [ERR_ASSERTION]: Input A expected to deepStrictEqual input B:
52+
+ expected
53+
- actual
54+
... Lines skipped
55+
56+
[
57+
[
58+
...
59+
2,
60+
- 3
61+
+ '3'
62+
],
63+
...
64+
5
65+
]
66+
```
67+
3568
## Legacy mode
3669

3770
> Stability: 0 - Deprecated: Use strict mode instead.

lib/assert.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ const meta = [
4848

4949
const escapeFn = (str) => meta[str.charCodeAt(0)];
5050

51+
const ERR_DIFF_DEACTIVATED = 0;
52+
const ERR_DIFF_NOT_EQUAL = 1;
53+
const ERR_DIFF_EQUAL = 2;
54+
5155
// The assert module provides functions that throw
5256
// AssertionError's when particular conditions are not met. The
5357
// assert module must conform to the following interface.
@@ -283,7 +287,8 @@ assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) {
283287
expected,
284288
message,
285289
operator: 'deepStrictEqual',
286-
stackStartFn: deepStrictEqual
290+
stackStartFn: deepStrictEqual,
291+
errorDiff: this === strict ? ERR_DIFF_EQUAL : ERR_DIFF_DEACTIVATED
287292
});
288293
}
289294
};
@@ -296,7 +301,8 @@ function notDeepStrictEqual(actual, expected, message) {
296301
expected,
297302
message,
298303
operator: 'notDeepStrictEqual',
299-
stackStartFn: notDeepStrictEqual
304+
stackStartFn: notDeepStrictEqual,
305+
errorDiff: this === strict ? ERR_DIFF_NOT_EQUAL : ERR_DIFF_DEACTIVATED
300306
});
301307
}
302308
}
@@ -308,7 +314,8 @@ assert.strictEqual = function strictEqual(actual, expected, message) {
308314
expected,
309315
message,
310316
operator: 'strictEqual',
311-
stackStartFn: strictEqual
317+
stackStartFn: strictEqual,
318+
errorDiff: this === strict ? ERR_DIFF_EQUAL : ERR_DIFF_DEACTIVATED
312319
});
313320
}
314321
};
@@ -320,7 +327,8 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
320327
expected,
321328
message,
322329
operator: 'notStrictEqual',
323-
stackStartFn: notStrictEqual
330+
stackStartFn: notStrictEqual,
331+
errorDiff: this === strict ? ERR_DIFF_NOT_EQUAL : ERR_DIFF_DEACTIVATED
324332
});
325333
}
326334
};

lib/internal/errors.js

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,22 +132,164 @@ class SystemError extends makeNodeError(Error) {
132132
}
133133
}
134134

135+
function createErrDiff(actual, expected, operator) {
136+
var other = '';
137+
var res = '';
138+
var lastPos = 0;
139+
var end = '';
140+
var skipped = false;
141+
const actualLines = util
142+
.inspect(actual, { compact: false }).split('\n');
143+
const expectedLines = util
144+
.inspect(expected, { compact: false }).split('\n');
145+
const msg = `Input A expected to ${operator} input B:\n` +
146+
'\u001b[32m+ expected\u001b[39m \u001b[31m- actual\u001b[39m';
147+
const skippedMsg = ' ... Lines skipped';
148+
149+
// Remove all ending lines that match (this optimizes the output for
150+
// readability by reducing the number of total changed lines).
151+
var a = actualLines[actualLines.length - 1];
152+
var b = expectedLines[expectedLines.length - 1];
153+
var i = 0;
154+
while (a === b) {
155+
if (i++ < 2) {
156+
end = `\n ${a}${end}`;
157+
} else {
158+
other = a;
159+
}
160+
actualLines.pop();
161+
expectedLines.pop();
162+
a = actualLines[actualLines.length - 1];
163+
b = expectedLines[expectedLines.length - 1];
164+
}
165+
if (i > 3) {
166+
end = `\n...${end}`;
167+
skipped = true;
168+
}
169+
if (other !== '') {
170+
end = `\n ${other}${end}`;
171+
other = '';
172+
}
173+
174+
const maxLines = Math.max(actualLines.length, expectedLines.length);
175+
var printedLines = 0;
176+
for (i = 0; i < maxLines; i++) {
177+
// Only extra expected lines exist
178+
const cur = i - lastPos;
179+
if (actualLines.length < i + 1) {
180+
if (cur > 1 && i > 2) {
181+
if (cur > 4) {
182+
res += '\n...';
183+
skipped = true;
184+
} else if (cur > 3) {
185+
res += `\n ${expectedLines[i - 2]}`;
186+
printedLines++;
187+
}
188+
res += `\n ${expectedLines[i - 1]}`;
189+
printedLines++;
190+
}
191+
lastPos = i;
192+
other += `\n\u001b[32m+\u001b[39m ${expectedLines[i]}`;
193+
printedLines++;
194+
// Only extra actual lines exist
195+
} else if (expectedLines.length < i + 1) {
196+
if (cur > 1 && i > 2) {
197+
if (cur > 4) {
198+
res += '\n...';
199+
skipped = true;
200+
} else if (cur > 3) {
201+
res += `\n ${actualLines[i - 2]}`;
202+
printedLines++;
203+
}
204+
res += `\n ${actualLines[i - 1]}`;
205+
printedLines++;
206+
}
207+
lastPos = i;
208+
res += `\n\u001b[31m-\u001b[39m ${actualLines[i]}`;
209+
printedLines++;
210+
// Lines diverge
211+
} else if (actualLines[i] !== expectedLines[i]) {
212+
if (cur > 1 && i > 2) {
213+
if (cur > 4) {
214+
res += '\n...';
215+
skipped = true;
216+
} else if (cur > 3) {
217+
res += `\n ${actualLines[i - 2]}`;
218+
printedLines++;
219+
}
220+
res += `\n ${actualLines[i - 1]}`;
221+
printedLines++;
222+
}
223+
lastPos = i;
224+
res += `\n\u001b[31m-\u001b[39m ${actualLines[i]}`;
225+
other += `\n\u001b[32m+\u001b[39m ${expectedLines[i]}`;
226+
printedLines += 2;
227+
// Lines are identical
228+
} else {
229+
res += other;
230+
other = '';
231+
if (cur === 1 || i === 0) {
232+
res += `\n ${actualLines[i]}`;
233+
printedLines++;
234+
}
235+
}
236+
// Inspected object to big (Show ~20 rows max)
237+
if (printedLines > 20 && i < maxLines - 2) {
238+
return `${msg}${skippedMsg}\n${res}\n...${other}\n...`;
239+
}
240+
}
241+
return `${msg}${skipped ? skippedMsg : ''}\n${res}${other}${end}`;
242+
}
243+
135244
class AssertionError extends Error {
136245
constructor(options) {
137246
if (typeof options !== 'object' || options === null) {
138247
throw new exports.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'Object');
139248
}
140-
var { actual, expected, message, operator, stackStartFn } = options;
249+
var {
250+
actual,
251+
expected,
252+
message,
253+
operator,
254+
stackStartFn,
255+
errorDiff = 0
256+
} = options;
257+
141258
if (message != null) {
142259
super(message);
143260
} else {
261+
if (util === null) util = require('util');
262+
144263
if (actual && actual.stack && actual instanceof Error)
145264
actual = `${actual.name}: ${actual.message}`;
146265
if (expected && expected.stack && expected instanceof Error)
147266
expected = `${expected.name}: ${expected.message}`;
148-
if (util === null) util = require('util');
149-
super(`${util.inspect(actual).slice(0, 128)} ` +
150-
`${operator} ${util.inspect(expected).slice(0, 128)}`);
267+
268+
if (errorDiff === 0) {
269+
let res = util.inspect(actual);
270+
let other = util.inspect(expected);
271+
if (res.length > 128)
272+
res = `${res.slice(0, 125)}...`;
273+
if (other.length > 128)
274+
other = `${other.slice(0, 125)}...`;
275+
super(`${res} ${operator} ${other}`);
276+
} else if (errorDiff === 1) {
277+
// In case the objects are equal but the operator requires unequal, show
278+
// the first object and say A equals B
279+
const res = util
280+
.inspect(actual, { compact: false }).split('\n');
281+
282+
if (res.length > 20) {
283+
res[19] = '...';
284+
while (res.length > 20) {
285+
res.pop();
286+
}
287+
}
288+
// Only print a single object.
289+
super(`Identical input passed to ${operator}:\n${res.join('\n')}`);
290+
} else {
291+
super(createErrDiff(actual, expected, operator));
292+
}
151293
}
152294

153295
this.generatedMessage = !message;

0 commit comments

Comments
 (0)