Skip to content

Commit 5aa3a2d

Browse files
BridgeARMylesBorins
authored andcommitted
assert: improve error messages
From now on all error messages produced by `assert` in strict mode will produce a error diff. Backport-PR-URL: #19230 PR-URL: #17615 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent c18ac52 commit 5aa3a2d

File tree

4 files changed

+332
-10
lines changed

4 files changed

+332
-10
lines changed

doc/api/assert.md

+33
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

+12-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ const { isDeepEqual, isDeepStrictEqual } =
2525
const errors = require('internal/errors');
2626
const { inspect } = require('util');
2727

28+
const ERR_DIFF_DEACTIVATED = 0;
29+
const ERR_DIFF_NOT_EQUAL = 1;
30+
const ERR_DIFF_EQUAL = 2;
31+
2832
// The assert module provides functions that throw
2933
// AssertionError's when particular conditions are not met. The
3034
// assert module must conform to the following interface.
@@ -154,7 +158,8 @@ assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) {
154158
expected,
155159
message,
156160
operator: 'deepStrictEqual',
157-
stackStartFn: deepStrictEqual
161+
stackStartFn: deepStrictEqual,
162+
errorDiff: this === strict ? ERR_DIFF_EQUAL : ERR_DIFF_DEACTIVATED
158163
});
159164
}
160165
};
@@ -167,7 +172,8 @@ function notDeepStrictEqual(actual, expected, message) {
167172
expected,
168173
message,
169174
operator: 'notDeepStrictEqual',
170-
stackStartFn: notDeepStrictEqual
175+
stackStartFn: notDeepStrictEqual,
176+
errorDiff: this === strict ? ERR_DIFF_NOT_EQUAL : ERR_DIFF_DEACTIVATED
171177
});
172178
}
173179
}
@@ -180,7 +186,8 @@ assert.strictEqual = function strictEqual(actual, expected, message) {
180186
expected,
181187
message,
182188
operator: '===',
183-
stackStartFn: strictEqual
189+
stackStartFn: strictEqual,
190+
errorDiff: this === strict ? ERR_DIFF_EQUAL : ERR_DIFF_DEACTIVATED
184191
});
185192
}
186193
};
@@ -194,7 +201,8 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
194201
expected,
195202
message,
196203
operator: '!==',
197-
stackStartFn: notStrictEqual
204+
stackStartFn: notStrictEqual,
205+
errorDiff: this === strict ? ERR_DIFF_NOT_EQUAL : ERR_DIFF_DEACTIVATED
198206
});
199207
}
200208
};

lib/internal/errors.js

+146-4
Original file line numberDiff line numberDiff line change
@@ -78,22 +78,164 @@ function makeNodeError(Base) {
7878
};
7979
}
8080

81+
function createErrDiff(actual, expected, operator) {
82+
var other = '';
83+
var res = '';
84+
var lastPos = 0;
85+
var end = '';
86+
var skipped = false;
87+
const util = lazyUtil();
88+
const actualLines = util
89+
.inspect(actual, { compact: false }).split('\n');
90+
const expectedLines = util
91+
.inspect(expected, { compact: false }).split('\n');
92+
const msg = `Input A expected to ${operator} input B:\n` +
93+
'\u001b[32m+ expected\u001b[39m \u001b[31m- actual\u001b[39m';
94+
const skippedMsg = ' ... Lines skipped';
95+
96+
// Remove all ending lines that match (this optimizes the output for
97+
// readability by reducing the number of total changed lines).
98+
var a = actualLines[actualLines.length - 1];
99+
var b = expectedLines[expectedLines.length - 1];
100+
var i = 0;
101+
while (a === b) {
102+
if (i++ < 2) {
103+
end = `\n ${a}${end}`;
104+
} else {
105+
other = a;
106+
}
107+
actualLines.pop();
108+
expectedLines.pop();
109+
a = actualLines[actualLines.length - 1];
110+
b = expectedLines[expectedLines.length - 1];
111+
}
112+
if (i > 3) {
113+
end = `\n...${end}`;
114+
skipped = true;
115+
}
116+
if (other !== '') {
117+
end = `\n ${other}${end}`;
118+
other = '';
119+
}
120+
121+
const maxLines = Math.max(actualLines.length, expectedLines.length);
122+
var printedLines = 0;
123+
for (i = 0; i < maxLines; i++) {
124+
// Only extra expected lines exist
125+
const cur = i - lastPos;
126+
if (actualLines.length < i + 1) {
127+
if (cur > 1 && i > 2) {
128+
if (cur > 4) {
129+
res += '\n...';
130+
skipped = true;
131+
} else if (cur > 3) {
132+
res += `\n ${expectedLines[i - 2]}`;
133+
printedLines++;
134+
}
135+
res += `\n ${expectedLines[i - 1]}`;
136+
printedLines++;
137+
}
138+
lastPos = i;
139+
other += `\n\u001b[32m+\u001b[39m ${expectedLines[i]}`;
140+
printedLines++;
141+
// Only extra actual lines exist
142+
} else if (expectedLines.length < i + 1) {
143+
if (cur > 1 && i > 2) {
144+
if (cur > 4) {
145+
res += '\n...';
146+
skipped = true;
147+
} else if (cur > 3) {
148+
res += `\n ${actualLines[i - 2]}`;
149+
printedLines++;
150+
}
151+
res += `\n ${actualLines[i - 1]}`;
152+
printedLines++;
153+
}
154+
lastPos = i;
155+
res += `\n\u001b[31m-\u001b[39m ${actualLines[i]}`;
156+
printedLines++;
157+
// Lines diverge
158+
} else if (actualLines[i] !== expectedLines[i]) {
159+
if (cur > 1 && i > 2) {
160+
if (cur > 4) {
161+
res += '\n...';
162+
skipped = true;
163+
} else if (cur > 3) {
164+
res += `\n ${actualLines[i - 2]}`;
165+
printedLines++;
166+
}
167+
res += `\n ${actualLines[i - 1]}`;
168+
printedLines++;
169+
}
170+
lastPos = i;
171+
res += `\n\u001b[31m-\u001b[39m ${actualLines[i]}`;
172+
other += `\n\u001b[32m+\u001b[39m ${expectedLines[i]}`;
173+
printedLines += 2;
174+
// Lines are identical
175+
} else {
176+
res += other;
177+
other = '';
178+
if (cur === 1 || i === 0) {
179+
res += `\n ${actualLines[i]}`;
180+
printedLines++;
181+
}
182+
}
183+
// Inspected object to big (Show ~20 rows max)
184+
if (printedLines > 20 && i < maxLines - 2) {
185+
return `${msg}${skippedMsg}\n${res}\n...${other}\n...`;
186+
}
187+
}
188+
return `${msg}${skipped ? skippedMsg : ''}\n${res}${other}${end}`;
189+
}
190+
81191
class AssertionError extends Error {
82192
constructor(options) {
83193
if (typeof options !== 'object' || options === null) {
84194
throw new exports.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'Object');
85195
}
86-
var { actual, expected, message, operator, stackStartFn } = options;
87-
if (message) {
196+
var {
197+
actual,
198+
expected,
199+
message,
200+
operator,
201+
stackStartFn,
202+
errorDiff = 0
203+
} = options;
204+
205+
if (message != null) {
88206
super(message);
89207
} else {
90208
const util = lazyUtil();
91209
if (actual && actual.stack && actual instanceof Error)
92210
actual = `${actual.name}: ${actual.message}`;
93211
if (expected && expected.stack && expected instanceof Error)
94212
expected = `${expected.name}: ${expected.message}`;
95-
super(`${util.inspect(actual).slice(0, 128)} ` +
96-
`${operator} ${util.inspect(expected).slice(0, 128)}`);
213+
214+
if (errorDiff === 0) {
215+
let res = util.inspect(actual);
216+
let other = util.inspect(expected);
217+
if (res.length > 128)
218+
res = `${res.slice(0, 125)}...`;
219+
if (other.length > 128)
220+
other = `${other.slice(0, 125)}...`;
221+
super(`${res} ${operator} ${other}`);
222+
} else if (errorDiff === 1) {
223+
// In case the objects are equal but the operator requires unequal, show
224+
// the first object and say A equals B
225+
const res = util
226+
.inspect(actual, { compact: false }).split('\n');
227+
228+
if (res.length > 20) {
229+
res[19] = '...';
230+
while (res.length > 20) {
231+
res.pop();
232+
}
233+
}
234+
// Only print a single object.
235+
super(`Identical input passed to ${operator}:\n${res.join('\n')}`);
236+
} else {
237+
super(createErrDiff(actual, expected, operator));
238+
}
97239
}
98240

99241
this.generatedMessage = !message;

0 commit comments

Comments
 (0)