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: add error diffs #17615

Closed
wants to merge 5 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
36 changes: 36 additions & 0 deletions doc/api/assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ For more information about the used equality comparisons see
<!-- YAML
added: REPLACEME
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/REPLACEME
description: Added error diffs to the strict mode
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/17002
description: Added strict mode to the assert module.
Expand All @@ -26,12 +29,45 @@ When using the `strict mode`, any `assert` function will use the equality used i
the strict function mode. So [`assert.deepEqual()`][] will, for example, work the
same as [`assert.deepStrictEqual()`][].

On top of that, error messages which involve objects produce an error diff
instead of displaying both objects. That is not the case for the legacy mode.

It can be accessed using:

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

Example error diff (the `expected`, `actual`, and `Lines skipped` will be on a
single row):

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

assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]);
```

```diff
AssertionError [ERR_ASSERTION]: Input A expected to deepStrictEqual input B:
+ expected
- actual
... Lines skipped

[
[
...
2,
- 3
+ '3'
],
...
5
]
```

To deactivate the colors, use the `NODE_DISABLE_COLORS` environment variable.
Please note that this will also deactivate the colors in the REPL.

## Legacy mode

> Stability: 0 - Deprecated: Use strict mode instead.
Expand Down
26 changes: 26 additions & 0 deletions doc/api/tty.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,32 @@ added: v0.7.7
A `number` specifying the number of rows the TTY currently has. This property
is updated whenever the `'resize'` event is emitted.

### writeStream.getColorDepth([env])
<!-- YAML
added: REPLACEME
-->

* `env` {object} A object containing the environment variables to check.
Defaults to `process.env`.
* Returns: {number}

Returns:
* 1 for 2,
* 4 for 16,
* 8 for 256,
* 24 for 16,777,216
colors supported.

Use this to determine what colors the terminal supports. Due to the nature of
colors in terminals it is possible to either have false positives or false
negatives. It depends on process information and the environment variables that
may lie about what terminal is used.
To enforce a specific behavior without relying on `process.env` it is possible
to pass in an object with different settings.

Use the `NODE_DISABLE_COLORS` environment variable to enforce this function to
always return 1.

## tty.isatty(fd)
<!-- YAML
added: v0.5.8
Expand Down
16 changes: 12 additions & 4 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ const meta = [

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

const ERR_DIFF_DEACTIVATED = 0;
const ERR_DIFF_NOT_EQUAL = 1;
const ERR_DIFF_EQUAL = 2;

// The assert module provides functions that throw
// AssertionError's when particular conditions are not met. The
// assert module must conform to the following interface.
Expand Down Expand Up @@ -283,7 +287,8 @@ assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) {
expected,
message,
operator: 'deepStrictEqual',
stackStartFn: deepStrictEqual
stackStartFn: deepStrictEqual,
errorDiff: this === strict ? ERR_DIFF_EQUAL : ERR_DIFF_DEACTIVATED
});
}
};
Expand All @@ -296,7 +301,8 @@ function notDeepStrictEqual(actual, expected, message) {
expected,
message,
operator: 'notDeepStrictEqual',
stackStartFn: notDeepStrictEqual
stackStartFn: notDeepStrictEqual,
errorDiff: this === strict ? ERR_DIFF_NOT_EQUAL : ERR_DIFF_DEACTIVATED
});
}
}
Expand All @@ -308,7 +314,8 @@ assert.strictEqual = function strictEqual(actual, expected, message) {
expected,
message,
operator: 'strictEqual',
stackStartFn: strictEqual
stackStartFn: strictEqual,
errorDiff: this === strict ? ERR_DIFF_EQUAL : ERR_DIFF_DEACTIVATED
});
}
};
Expand All @@ -320,7 +327,8 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {
expected,
message,
operator: 'notStrictEqual',
stackStartFn: notStrictEqual
stackStartFn: notStrictEqual,
errorDiff: this === strict ? ERR_DIFF_NOT_EQUAL : ERR_DIFF_DEACTIVATED
});
}
};
Expand Down
161 changes: 157 additions & 4 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const kCode = Symbol('code');
const kInfo = Symbol('info');
const messages = new Map();

var green = '';
var red = '';
var white = '';

const { errmap } = process.binding('uv');
const { kMaxLength } = process.binding('buffer');
const { defineProperty } = Object;
Expand Down Expand Up @@ -132,22 +136,171 @@ class SystemError extends makeNodeError(Error) {
}
}

function createErrDiff(actual, expected, operator) {
var other = '';
var res = '';
var lastPos = 0;
var end = '';
var skipped = false;
const actualLines = util
.inspect(actual, { compact: false }).split('\n');
const expectedLines = util
.inspect(expected, { compact: false }).split('\n');
const msg = `Input A expected to ${operator} input B:\n` +
`${green}+ expected${white} ${red}- actual${white}`;
const skippedMsg = ' ... Lines skipped';

// Remove all ending lines that match (this optimizes the output for
// readability by reducing the number of total changed lines).
var a = actualLines[actualLines.length - 1];
var b = expectedLines[expectedLines.length - 1];
var i = 0;
while (a === b) {
if (i++ < 2) {
end = `\n ${a}${end}`;
} else {
other = a;
}
actualLines.pop();
expectedLines.pop();
a = actualLines[actualLines.length - 1];
b = expectedLines[expectedLines.length - 1];
}
if (i > 3) {
end = `\n...${end}`;
skipped = true;
}
if (other !== '') {
end = `\n ${other}${end}`;
other = '';
}

const maxLines = Math.max(actualLines.length, expectedLines.length);
var printedLines = 0;
for (i = 0; i < maxLines; i++) {
// Only extra expected lines exist
const cur = i - lastPos;
if (actualLines.length < i + 1) {
if (cur > 1 && i > 2) {
if (cur > 4) {
res += '\n...';
skipped = true;
} else if (cur > 3) {
res += `\n ${expectedLines[i - 2]}`;
printedLines++;
}
res += `\n ${expectedLines[i - 1]}`;
printedLines++;
}
lastPos = i;
other += `\n${green}+${white} ${expectedLines[i]}`;
printedLines++;
// Only extra actual lines exist
} else if (expectedLines.length < i + 1) {
if (cur > 1 && i > 2) {
if (cur > 4) {
res += '\n...';
skipped = true;
} else if (cur > 3) {
res += `\n ${actualLines[i - 2]}`;
printedLines++;
}
res += `\n ${actualLines[i - 1]}`;
printedLines++;
}
lastPos = i;
res += `\n${red}-${white} ${actualLines[i]}`;
printedLines++;
// Lines diverge
} else if (actualLines[i] !== expectedLines[i]) {
if (cur > 1 && i > 2) {
if (cur > 4) {
res += '\n...';
skipped = true;
} else if (cur > 3) {
res += `\n ${actualLines[i - 2]}`;
printedLines++;
}
res += `\n ${actualLines[i - 1]}`;
printedLines++;
}
lastPos = i;
res += `\n${red}-${white} ${actualLines[i]}`;
other += `\n${green}+${white} ${expectedLines[i]}`;
printedLines += 2;
// Lines are identical
} else {
res += other;
other = '';
if (cur === 1 || i === 0) {
res += `\n ${actualLines[i]}`;
printedLines++;
}
}
// Inspected object to big (Show ~20 rows max)
if (printedLines > 20 && i < maxLines - 2) {
return `${msg}${skippedMsg}\n${res}\n...${other}\n...`;
}
}
return `${msg}${skipped ? skippedMsg : ''}\n${res}${other}${end}`;
}

class AssertionError extends Error {
constructor(options) {
if (typeof options !== 'object' || options === null) {
throw new exports.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'Object');
}
var { actual, expected, message, operator, stackStartFn } = options;
var {
actual,
expected,
message,
operator,
stackStartFn,
errorDiff = 0
} = options;

if (message != null) {
super(message);
} else {
if (util === null) {
util = require('util');
if (process.stdout.isTTY && process.stdout.getColorDepth() !== 1) {
green = '\u001b[32m';
white = '\u001b[39m';
red = '\u001b[31m';
}
}

if (actual && actual.stack && actual instanceof Error)
actual = `${actual.name}: ${actual.message}`;
if (expected && expected.stack && expected instanceof Error)
expected = `${expected.name}: ${expected.message}`;
if (util === null) util = require('util');
super(`${util.inspect(actual).slice(0, 128)} ` +
`${operator} ${util.inspect(expected).slice(0, 128)}`);

if (errorDiff === 0) {
let res = util.inspect(actual);
let other = util.inspect(expected);
if (res.length > 128)
res = `${res.slice(0, 125)}...`;
if (other.length > 128)
other = `${other.slice(0, 125)}...`;
super(`${res} ${operator} ${other}`);
} else if (errorDiff === 1) {
// In case the objects are equal but the operator requires unequal, show
// the first object and say A equals B
const res = util
.inspect(actual, { compact: false }).split('\n');

if (res.length > 20) {
res[19] = '...';
while (res.length > 20) {
res.pop();
}
}
// Only print a single object.
super(`Identical input passed to ${operator}:\n${res.join('\n')}`);
} else {
super(createErrDiff(actual, expected, operator));
}
}

this.generatedMessage = !message;
Expand Down
5 changes: 2 additions & 3 deletions lib/internal/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const {
isHexTable
} = require('internal/querystring');

const { getConstructorOf } = require('internal/util');
const { getConstructorOf, removeColors } = require('internal/util');
const errors = require('internal/errors');
const querystring = require('querystring');

Expand Down Expand Up @@ -181,9 +181,8 @@ class URLSearchParams {
for (var i = 0; i < list.length; i += 2)
output.push(`${innerInspect(list[i])} => ${innerInspect(list[i + 1])}`);

var colorRe = /\u001b\[\d\d?m/g;
var length = output.reduce(
(prev, cur) => prev + cur.replace(colorRe, '').length + separator.length,
(prev, cur) => prev + removeColors(cur).length + separator.length,
-separator.length
);
if (length > ctx.breakLength) {
Expand Down
7 changes: 7 additions & 0 deletions lib/internal/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ const noCrypto = !process.versions.openssl;

const experimentalWarnings = new Set();

const colorRegExp = /\u001b\[\d\d?m/g;

function removeColors(str) {
return str.replace(colorRegExp, '');
}

function isError(e) {
return objectToString(e) === '[object Error]' || e instanceof Error;
}
Expand Down Expand Up @@ -347,6 +353,7 @@ module.exports = {
objectToString,
promisify,
spliceOne,
removeColors,

// Symbol used to customize promisify conversion
customPromisifyArgs: kCustomPromisifyArgsSymbol,
Expand Down
Loading