diff --git a/doc/api/assert.md b/doc/api/assert.md index 6935a9db4bda64..6eccfa7ee115c3 100644 --- a/doc/api/assert.md +++ b/doc/api/assert.md @@ -7,6 +7,93 @@ The `assert` module provides a simple set of assertion tests that can be used to test invariants. +A `strict` and a `legacy` mode exist, while it is recommended to only use +[`strict mode`][]. + +For more information about the used equality comparisons see +[MDN's guide on equality comparisons and sameness][mdn-equality-guide]. + +## Strict mode + + +When using the `strict mode`, any `assert` function will use the equality used in +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. + +When accessing `assert` directly instead of using the `strict` property, the +[Abstract Equality Comparison][] will be used for any function without a +"strict" in its name (e.g. [`assert.deepEqual()`][]). + +It can be accessed using: + +```js +const assert = require('assert'); +``` + +It is recommended to use the [`strict mode`][] instead as the +[Abstract Equality Comparison][] can often have surprising results. Especially +in case of [`assert.deepEqual()`][] as the used comparison rules there are very +lax. + +E.g. + +```js +// WARNING: This does not throw an AssertionError! +assert.deepEqual(/a/gi, new Date()); +``` + ## assert(value[, message]) * `block` {Function} -* `error` {RegExp|Function} +* `error` {RegExp|Function|object} * `message` {any} Expects the function `block` to throw an error. -If specified, `error` can be a constructor, [`RegExp`][], or validation -function. +If specified, `error` can be a constructor, [`RegExp`][], a validation +function, or an object where each property will be tested for. If specified, `message` will be the message provided by the `AssertionError` if the block fails to throw. @@ -658,12 +789,15 @@ assert.throws( Validate error message using [`RegExp`][]: +Using a regular expression runs `.toString` on the error object, and will +therefore also include the error name. + ```js assert.throws( () => { throw new Error('Wrong value'); }, - /value/ + /^Error: Wrong value$/ ); ``` @@ -683,19 +817,61 @@ assert.throws( ); ``` +Custom error object / error instance: + +```js +assert.throws( + () => { + const err = new TypeError('Wrong value'); + err.code = 404; + throw err; + }, + { + name: 'TypeError', + message: 'Wrong value' + // Note that only properties on the error object will be tested! + } +); +``` + Note that `error` can not be a string. If a string is provided as the second argument, then `error` is assumed to be omitted and the string will be used for -`message` instead. This can lead to easy-to-miss mistakes: +`message` instead. This can lead to easy-to-miss mistakes. Please read the +example below carefully if using a string as the second argument gets +considered: ```js -// THIS IS A MISTAKE! DO NOT DO THIS! -assert.throws(myFunction, 'missing foo', 'did not throw with expected message'); - -// Do this instead. -assert.throws(myFunction, /missing foo/, 'did not throw with expected message'); +function throwingFirst() { + throw new Error('First'); +} +function throwingSecond() { + throw new Error('Second'); +} +function notThrowing() {} + +// The second argument is a string and the input function threw an Error. +// In that case both cases do not throw as neither is going to try to +// match for the error message thrown by the input function! +assert.throws(throwingFirst, 'Second'); +assert.throws(throwingSecond, 'Second'); + +// The string is only used (as message) in case the function does not throw: +assert.throws(notThrowing, 'Second'); +// AssertionError [ERR_ASSERTION]: Missing expected exception: Second + +// If it was intended to match for the error message do this instead: +assert.throws(throwingSecond, /Second$/); +// Does not throw because the error messages match. +assert.throws(throwingFirst, /Second$/); +// Throws a error: +// Error: First +// at throwingFirst (repl:2:9) ``` +Due to the confusing notation, it is recommended not to use a string as the +second argument. This might lead to difficult-to-spot errors. + ## Caveats For the following cases, consider using ES2015 [`Object.is()`][], @@ -729,15 +905,21 @@ For more information, see [`Set`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set [`Symbol`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol [`TypeError`]: errors.html#errors_class_typeerror +[`WeakMap`]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/WeakMap +[`WeakSet`]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/WeakSet [`assert.deepEqual()`]: #assert_assert_deepequal_actual_expected_message [`assert.deepStrictEqual()`]: #assert_assert_deepstrictequal_actual_expected_message +[`assert.notDeepStrictEqual()`]: #assert_assert_notdeepstrictequal_actual_expected_message +[`assert.notStrictEqual()`]: #assert_assert_notstrictequal_actual_expected_message [`assert.ok()`]: #assert_assert_ok_value_message +[`assert.strictEqual()`]: #assert_assert_strictequal_actual_expected_message [`assert.throws()`]: #assert_assert_throws_block_error_message +[`strict mode`]: #assert_strict_mode [Abstract Equality Comparison]: https://tc39.github.io/ecma262/#sec-abstract-equality-comparison [Object.prototype.toString()]: https://tc39.github.io/ecma262/#sec-object.prototype.tostring [SameValueZero]: https://tc39.github.io/ecma262/#sec-samevaluezero +[SameValue Comparison]: https://tc39.github.io/ecma262/#sec-samevalue [Strict Equality Comparison]: https://tc39.github.io/ecma262/#sec-strict-equality-comparison -[caveats]: #assert_caveats [enumerable "own" properties]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties [mdn-equality-guide]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness [prototype-spec]: https://tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index c2441f69956275..ac1cb7cace45a3 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -774,19 +774,15 @@ Type: Runtime cause a lot of issues. See https://github.com/nodejs/node/issues/14328 for more details. - -### DEP0098: AsyncHooks Embedder AsyncResource.emit{Before,After} APIs + +### DEP0089: require('assert') -Type: Runtime - -The embedded API provided by AsyncHooks exposes emit{Before,After} methods -which are very easy to use incorrectly which can lead to unrecoverable errors. +Type: Documentation-only -Use [`asyncResource.runInAsyncScope()`][] API instead which provides a much -safer, and more convenient, alternative. See -https://github.com/nodejs/node/pull/18513 for more details. +Importing assert directly is not recommended as the exposed functions will use +loose equality checks. Use `require('assert').strict` instead. The API is the +same as the legacy assert but it will always use strict equality checks. -[`--pending-deprecation`]: cli.html#cli_pending_deprecation [`Buffer.allocUnsafeSlow(size)`]: buffer.html#buffer_class_method_buffer_allocunsafeslow_size [`Buffer.from(array)`]: buffer.html#buffer_class_method_buffer_from_array [`Buffer.from(buffer)`]: buffer.html#buffer_class_method_buffer_from_buffer diff --git a/doc/api/tty.md b/doc/api/tty.md index 8b757c0f02751a..ce6dbae8fa6e7d 100644 --- a/doc/api/tty.md +++ b/doc/api/tty.md @@ -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]) + + +* `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) diff --git a/lib/assert.js b/lib/assert.js index 0d061dbff617f7..6c33af3fd4b2ca 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -22,7 +22,12 @@ const { isDeepEqual, isDeepStrictEqual } = require('internal/util/comparisons'); -const errors = require('internal/errors'); +const { AssertionError, TypeError } = require('internal/errors'); +const { inspect } = require('util'); + +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 @@ -30,25 +35,21 @@ const errors = require('internal/errors'); const assert = module.exports = ok; +const NO_EXCEPTION_SENTINEL = {}; + // All of the following functions must throw an AssertionError // when a corresponding condition is not met, with a message that // may be undefined if not provided. All assertion methods provide // both the actual and expected values to the assertion error for // display purposes. -function innerFail(actual, expected, message, operator, stackStartFunction) { - if (message instanceof Error) throw message; +function innerFail(obj) { + if (obj.message instanceof Error) throw obj.message; - throw new errors.AssertionError({ - message, - actual, - expected, - operator, - stackStartFunction - }); + throw new AssertionError(obj); } -function fail(actual, expected, message, operator, stackStartFunction) { +function fail(actual, expected, message, operator, stackStartFn) { const argsLen = arguments.length; if (argsLen === 0) { @@ -60,7 +61,13 @@ function fail(actual, expected, message, operator, stackStartFunction) { operator = '!='; } - innerFail(actual, expected, message, operator, stackStartFunction || fail); + innerFail({ + actual, + expected, + message, + operator, + stackStartFn: stackStartFn || fail + }); } assert.fail = fail; @@ -69,13 +76,21 @@ assert.fail = fail; // new assert.AssertionError({ message: message, // actual: actual, // expected: expected }); -assert.AssertionError = errors.AssertionError; +assert.AssertionError = AssertionError; // Pure assertion tests whether a value is truthy, as determined // by !!value. function ok(value, message) { - if (!value) innerFail(value, true, message, '==', ok); + if (!value) { + innerFail({ + actual: value, + expected: true, + message, + operator: '==', + stackStartFn: ok + }); + } } assert.ok = ok; @@ -83,7 +98,15 @@ assert.ok = ok; /* eslint-disable no-restricted-properties */ assert.equal = function equal(actual, expected, message) { // eslint-disable-next-line eqeqeq - if (actual != expected) innerFail(actual, expected, message, '==', equal); + if (actual != expected) { + innerFail({ + actual, + expected, + message, + operator: '==', + stackStartFn: equal + }); + } }; // The non-equality assertion tests for whether two objects are not @@ -91,43 +114,81 @@ assert.equal = function equal(actual, expected, message) { assert.notEqual = function notEqual(actual, expected, message) { // eslint-disable-next-line eqeqeq if (actual == expected) { - innerFail(actual, expected, message, '!=', notEqual); + innerFail({ + actual, + expected, + message, + operator: '!=', + stackStartFn: notEqual + }); } }; // The equivalence assertion tests a deep equality relation. assert.deepEqual = function deepEqual(actual, expected, message) { if (!isDeepEqual(actual, expected)) { - innerFail(actual, expected, message, 'deepEqual', deepEqual); + innerFail({ + actual, + expected, + message, + operator: 'deepEqual', + stackStartFn: deepEqual + }); } }; // The non-equivalence assertion tests for any deep inequality. assert.notDeepEqual = function notDeepEqual(actual, expected, message) { if (isDeepEqual(actual, expected)) { - innerFail(actual, expected, message, 'notDeepEqual', notDeepEqual); + innerFail({ + actual, + expected, + message, + operator: 'notDeepEqual', + stackStartFn: notDeepEqual + }); } }; /* eslint-enable */ assert.deepStrictEqual = function deepStrictEqual(actual, expected, message) { if (!isDeepStrictEqual(actual, expected)) { - innerFail(actual, expected, message, 'deepStrictEqual', deepStrictEqual); + innerFail({ + actual, + expected, + message, + operator: 'deepStrictEqual', + stackStartFn: deepStrictEqual, + errorDiff: this === strict ? ERR_DIFF_EQUAL : ERR_DIFF_DEACTIVATED + }); } }; assert.notDeepStrictEqual = notDeepStrictEqual; function notDeepStrictEqual(actual, expected, message) { if (isDeepStrictEqual(actual, expected)) { - innerFail(actual, expected, message, 'notDeepStrictEqual', - notDeepStrictEqual); + innerFail({ + actual, + expected, + message, + operator: 'notDeepStrictEqual', + stackStartFn: notDeepStrictEqual, + errorDiff: this === strict ? ERR_DIFF_NOT_EQUAL : ERR_DIFF_DEACTIVATED + }); } } // The strict equality assertion tests strict equality, as determined by ===. assert.strictEqual = function strictEqual(actual, expected, message) { if (actual !== expected) { - innerFail(actual, expected, message, '===', strictEqual); + innerFail({ + actual, + expected, + message, + operator: '===', + stackStartFn: strictEqual, + errorDiff: this === strict ? ERR_DIFF_EQUAL : ERR_DIFF_DEACTIVATED + }); } }; @@ -135,14 +196,51 @@ assert.strictEqual = function strictEqual(actual, expected, message) { // determined by !==. assert.notStrictEqual = function notStrictEqual(actual, expected, message) { if (actual === expected) { - innerFail(actual, expected, message, '!==', notStrictEqual); + innerFail({ + actual, + expected, + message, + operator: '!==', + stackStartFn: notStrictEqual, + errorDiff: this === strict ? ERR_DIFF_NOT_EQUAL : ERR_DIFF_DEACTIVATED + }); } }; -function expectedException(actual, expected) { +function compareExceptionKey(actual, expected, key, msg) { + if (!isDeepStrictEqual(actual[key], expected[key])) { + innerFail({ + actual: actual[key], + expected: expected[key], + message: msg || `${key}: expected ${inspect(expected[key])}, ` + + `not ${inspect(actual[key])}`, + operator: 'throws', + stackStartFn: assert.throws + }); + } +} + +function expectedException(actual, expected, msg) { if (typeof expected !== 'function') { - // Should be a RegExp, if not fail hard - return expected.test(actual); + if (expected instanceof RegExp) + return expected.test(actual); + // assert.doesNotThrow does not accept objects. + if (arguments.length === 2) { + throw new TypeError('ERR_INVALID_ARG_TYPE', 'expected', + ['Function', 'RegExp'], expected); + } + // The name and message could be non enumerable. Therefore test them + // explicitly. + if ('name' in expected) { + compareExceptionKey(actual, expected, 'name', msg); + } + if ('message' in expected) { + compareExceptionKey(actual, expected, 'message', msg); + } + for (const key of Object.keys(expected)) { + compareExceptionKey(actual, expected, key, msg); + } + return true; } // Guard instanceof against arrow functions as they don't have a prototype. if (expected.prototype !== undefined && actual instanceof expected) { @@ -154,59 +252,94 @@ function expectedException(actual, expected) { return expected.call({}, actual) === true; } -function tryBlock(block) { +function getActual(block) { + if (typeof block !== 'function') { + throw new TypeError('ERR_INVALID_ARG_TYPE', 'block', 'Function', + block); + } try { block(); } catch (e) { return e; } + return NO_EXCEPTION_SENTINEL; } -function innerThrows(shouldThrow, block, expected, message) { - var details = ''; +// Expected to throw an error. +assert.throws = function throws(block, error, message) { + const actual = getActual(block); - if (typeof block !== 'function') { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'block', 'Function', - block); - } + if (typeof error === 'string') { + if (arguments.length === 3) + throw new TypeError('ERR_INVALID_ARG_TYPE', + 'error', + ['Function', 'RegExp'], + error); - if (typeof expected === 'string') { - message = expected; - expected = null; + message = error; + error = null; } - const actual = tryBlock(block); - - if (shouldThrow === true) { - if (actual === undefined) { - if (expected && expected.name) { - details += ` (${expected.name})`; - } - details += message ? `: ${message}` : '.'; - fail(actual, expected, `Missing expected exception${details}`, 'throws'); - } - if (expected && expectedException(actual, expected) === false) { - throw actual; - } - } else if (actual !== undefined) { - if (!expected || expectedException(actual, expected)) { - details = message ? `: ${message}` : '.'; - fail(actual, - expected, - `Got unwanted exception${details}\n${actual.message}`, - 'doesNotThrow'); + if (actual === NO_EXCEPTION_SENTINEL) { + let details = ''; + if (error && error.name) { + details += ` (${error.name})`; } + details += message ? `: ${message}` : '.'; + innerFail({ + actual, + expected: error, + operator: 'throws', + message: `Missing expected exception${details}`, + stackStartFn: throws + }); + } + if (error && expectedException(actual, error, message) === false) { throw actual; } -} - -// Expected to throw an error. -assert.throws = function throws(block, error, message) { - innerThrows(true, block, error, message); }; assert.doesNotThrow = function doesNotThrow(block, error, message) { - innerThrows(false, block, error, message); + const actual = getActual(block); + if (actual === NO_EXCEPTION_SENTINEL) + return; + + if (typeof error === 'string') { + message = error; + error = null; + } + + if (!error || expectedException(actual, error)) { + const details = message ? `: ${message}` : '.'; + innerFail({ + actual, + expected: error, + operator: 'doesNotThrow', + message: `Got unwanted exception${details}\n${actual && actual.message}`, + stackStartFn: doesNotThrow + }); + } + throw actual; }; assert.ifError = function ifError(err) { if (err) throw err; }; + +// Expose a strict only variant of assert +function strict(value, message) { + if (!value) { + innerFail({ + actual: value, + expected: true, + message, + operator: '==', + stackStartFn: strict + }); + } +} +assert.strict = Object.assign(strict, assert, { + equal: assert.strictEqual, + deepEqual: assert.deepStrictEqual, + notEqual: assert.notStrictEqual, + notDeepEqual: assert.notDeepStrictEqual +}); +assert.strict.strict = assert.strict; diff --git a/lib/internal/errors.js b/lib/internal/errors.js index f4a77e037bc491..f83458a6a1d70f 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -13,6 +13,10 @@ const kCode = Symbol('code'); const messages = new Map(); +var green = ''; +var red = ''; +var white = ''; + const { UV_EAI_MEMORY, UV_EAI_NODATA, @@ -78,22 +82,175 @@ function makeNodeError(Base) { }; } +function createErrDiff(actual, expected, operator) { + var other = ''; + var res = ''; + var lastPos = 0; + var end = ''; + var skipped = false; + const util = lazyUtil(); + const actualLines = util + .inspect(actual, { compact: false, customInspect: false }).split('\n'); + const expectedLines = util + .inspect(expected, { compact: false, customInspect: 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(); + if (actualLines.length === 0 || expectedLines.length === 0) + break; + 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, stackStartFunction } = options; - if (message) { + var { + actual, + expected, + message, + operator, + stackStartFn, + errorDiff = 0 + } = options; + + if (message != null) { super(message); } else { + if (util_ === null && + process.stdout.isTTY && + process.stdout.getColorDepth() !== 1) { + green = '\u001b[32m'; + white = '\u001b[39m'; + red = '\u001b[31m'; + } const util = lazyUtil(); if (actual && actual.stack && actual instanceof Error) actual = `${actual.name}: ${actual.message}`; if (expected && expected.stack && expected instanceof Error) expected = `${expected.name}: ${expected.message}`; - 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, customInspect: 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; @@ -102,7 +259,7 @@ class AssertionError extends Error { this.actual = actual; this.expected = expected; this.operator = operator; - Error.captureStackTrace(this, stackStartFunction); + Error.captureStackTrace(this, stackStartFn); } } diff --git a/lib/tty.js b/lib/tty.js index 29440d6d96e3d5..12d0836cde5db5 100644 --- a/lib/tty.js +++ b/lib/tty.js @@ -26,12 +26,19 @@ const net = require('net'); const { TTY, isTTY } = process.binding('tty_wrap'); const errors = require('internal/errors'); const readline = require('readline'); +const { release } = require('os'); + +const OSRelease = release().split('.'); + +const COLORS_2 = 1; +const COLORS_16 = 4; +const COLORS_256 = 8; +const COLORS_16m = 24; function isatty(fd) { return Number.isInteger(fd) && fd >= 0 && isTTY(fd); } - function ReadStream(fd, options) { if (!(this instanceof ReadStream)) return new ReadStream(fd, options); @@ -58,7 +65,6 @@ ReadStream.prototype.setRawMode = function(flag) { this.isRaw = flag; }; - function WriteStream(fd) { if (!(this instanceof WriteStream)) return new WriteStream(fd); @@ -78,8 +84,8 @@ function WriteStream(fd) { // Ref: https://github.com/nodejs/node/pull/1771#issuecomment-119351671 this._handle.setBlocking(true); - var winSize = new Array(2); - var err = this._handle.getWindowSize(winSize); + const winSize = new Array(2); + const err = this._handle.getWindowSize(winSize); if (!err) { this.columns = winSize[0]; this.rows = winSize[1]; @@ -87,21 +93,83 @@ function WriteStream(fd) { } inherits(WriteStream, net.Socket); - WriteStream.prototype.isTTY = true; +WriteStream.prototype.getColorDepth = function(env = process.env) { + if (env.NODE_DISABLE_COLORS || env.TERM === 'dumb' && !env.COLORTERM) { + return COLORS_2; + } + + if (process.platform === 'win32') { + // Windows 10 build 10586 is the first Windows release that supports 256 + // colors. Windows 10 build 14931 is the first release that supports + // 16m/TrueColor. + if (+OSRelease[0] >= 10) { + const build = +OSRelease[2]; + if (build >= 14931) + return COLORS_16m; + if (build >= 10586) + return COLORS_256; + } + + return COLORS_16; + } + + if (env.TMUX) { + return COLORS_256; + } + + if (env.CI) { + if ('TRAVIS' in env || 'CIRCLECI' in env || 'APPVEYOR' in env || + 'GITLAB_CI' in env || env.CI_NAME === 'codeship') { + return COLORS_256; + } + return COLORS_2; + } + + if ('TEAMCITY_VERSION' in env) { + return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? + COLORS_16 : COLORS_2; + } + + switch (env.TERM_PROGRAM) { + case 'iTerm.app': + if (!env.TERM_PROGRAM_VERSION || + /^[0-2]\./.test(env.TERM_PROGRAM_VERSION)) { + return COLORS_256; + } + return COLORS_16m; + case 'HyperTerm': + case 'Hyper': + case 'MacTerm': + return COLORS_16m; + case 'Apple_Terminal': + return COLORS_256; + } + + if (env.TERM) { + if (/^xterm-256/.test(env.TERM)) + return COLORS_256; + if (/^screen|^xterm|^vt100|color|ansi|cygwin|linux/i.test(env.TERM)) + return COLORS_16; + } + + if (env.COLORTERM) + return COLORS_16; + + return COLORS_2; +}; WriteStream.prototype._refreshSize = function() { - var oldCols = this.columns; - var oldRows = this.rows; - var winSize = new Array(2); - var err = this._handle.getWindowSize(winSize); + const oldCols = this.columns; + const oldRows = this.rows; + const winSize = new Array(2); + const err = this._handle.getWindowSize(winSize); if (err) { this.emit('error', errors.errnoException(err, 'getWindowSize')); return; } - var newCols = winSize[0]; - var newRows = winSize[1]; + const [newCols, newRows] = winSize; if (oldCols !== newCols || oldRows !== newRows) { this.columns = newCols; this.rows = newRows; @@ -109,8 +177,7 @@ WriteStream.prototype._refreshSize = function() { } }; - -// backwards-compat +// Backwards-compat WriteStream.prototype.cursorTo = function(x, y) { readline.cursorTo(this, x, y); }; @@ -127,5 +194,4 @@ WriteStream.prototype.getWindowSize = function() { return [this.columns, this.rows]; }; - module.exports = { isatty, ReadStream, WriteStream }; diff --git a/lib/util.js b/lib/util.js index cd6321cfe5270e..70fd1a05564389 100644 --- a/lib/util.js +++ b/lib/util.js @@ -68,7 +68,8 @@ const inspectDefaultOptions = Object.seal({ customInspect: true, showProxy: false, maxArrayLength: 100, - breakLength: 60 + breakLength: 60, + compact: true }); const propertyIsEnumerable = Object.prototype.propertyIsEnumerable; @@ -86,6 +87,10 @@ const strEscapeSequencesReplacer = /[\x00-\x1f\x27\x5c]/g; const keyStrRegExp = /^[a-zA-Z_][a-zA-Z_0-9]*$/; const numberRegExp = /^(0|[1-9][0-9]*)$/; +const readableRegExps = {}; + +const MIN_LINE_LENGTH = 16; + // Escaped special characters. Use empty strings to fill up unused entries. const meta = [ '\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004', @@ -257,14 +262,14 @@ function debuglog(set) { } /** - * Echos the value of a value. Tries to print the value out + * Echos the value of any input. Tries to print the value out * in the best way possible given the different types. * - * @param {Object} obj The object to print out. + * @param {any} value The value to print out. * @param {Object} opts Optional options object that alters the output. */ -/* Legacy: obj, showHidden, depth, colors*/ -function inspect(obj, opts) { +/* Legacy: value, showHidden, depth, colors*/ +function inspect(value, opts) { // Default options const ctx = { seen: [], @@ -276,7 +281,8 @@ function inspect(obj, opts) { showProxy: inspectDefaultOptions.showProxy, maxArrayLength: inspectDefaultOptions.maxArrayLength, breakLength: inspectDefaultOptions.breakLength, - indentationLvl: 0 + indentationLvl: 0, + compact: inspectDefaultOptions.compact }; // Legacy... if (arguments.length > 2) { @@ -298,7 +304,7 @@ function inspect(obj, opts) { } if (ctx.colors) ctx.stylize = stylizeWithColor; if (ctx.maxArrayLength === null) ctx.maxArrayLength = Infinity; - return formatValue(ctx, obj, ctx.depth); + return formatValue(ctx, value, ctx.depth); } inspect.custom = customInspectSymbol; @@ -374,7 +380,7 @@ function ensureDebugIsInitialized() { function formatValue(ctx, value, recurseTimes, ln) { // Primitive types cannot have properties if (typeof value !== 'object' && typeof value !== 'function') { - return formatPrimitive(ctx.stylize, value); + return formatPrimitive(ctx.stylize, value, ctx); } if (value === null) { return ctx.stylize('null', 'null'); @@ -481,10 +487,10 @@ function formatValue(ctx, value, recurseTimes, ln) { } catch (e) { /* ignore */ } if (typeof raw === 'string') { - const formatted = formatPrimitive(stylizeNoColor, raw); + const formatted = formatPrimitive(stylizeNoColor, raw, ctx); if (keyLength === raw.length) return ctx.stylize(`[String: ${formatted}]`, 'string'); - base = ` [String: ${formatted}]`; + base = `[String: ${formatted}]`; // For boxed Strings, we have to remove the 0-n indexed entries, // since they just noisy up the output and are redundant // Make boxed primitive Strings look like such @@ -505,12 +511,12 @@ function formatValue(ctx, value, recurseTimes, ln) { const name = `${constructor.name}${value.name ? `: ${value.name}` : ''}`; if (keyLength === 0) return ctx.stylize(`[${name}]`, 'special'); - base = ` [${name}]`; + base = `[${name}]`; } else if (isRegExp(value)) { // Make RegExps say that they are RegExps if (keyLength === 0 || recurseTimes < 0) return ctx.stylize(regExpToString.call(value), 'regexp'); - base = ` ${regExpToString.call(value)}`; + base = `${regExpToString.call(value)}`; } else if (isDate(value)) { if (keyLength === 0) { if (Number.isNaN(value.getTime())) @@ -518,12 +524,12 @@ function formatValue(ctx, value, recurseTimes, ln) { return ctx.stylize(dateToISOString.call(value), 'date'); } // Make dates with properties first say the date - base = ` ${dateToISOString.call(value)}`; + base = `${dateToISOString.call(value)}`; } else if (isError(value)) { // Make error with message first say the error if (keyLength === 0) return formatError(value); - base = ` ${formatError(value)}`; + base = `${formatError(value)}`; } else if (isAnyArrayBuffer(value)) { // Fast path for ArrayBuffer and SharedArrayBuffer. // Can't do the same for DataView because it has a non-primitive @@ -553,13 +559,13 @@ function formatValue(ctx, value, recurseTimes, ln) { const formatted = formatPrimitive(stylizeNoColor, raw); if (keyLength === 0) return ctx.stylize(`[Number: ${formatted}]`, 'number'); - base = ` [Number: ${formatted}]`; + base = `[Number: ${formatted}]`; } else if (typeof raw === 'boolean') { // Make boxed primitive Booleans look like such const formatted = formatPrimitive(stylizeNoColor, raw); if (keyLength === 0) return ctx.stylize(`[Boolean: ${formatted}]`, 'boolean'); - base = ` [Boolean: ${formatted}]`; + base = `[Boolean: ${formatted}]`; } else if (typeof raw === 'symbol') { const formatted = formatPrimitive(stylizeNoColor, raw); return ctx.stylize(`[Symbol: ${formatted}]`, 'symbol'); @@ -603,9 +609,42 @@ function formatNumber(fn, value) { return fn(`${value}`, 'number'); } -function formatPrimitive(fn, value) { - if (typeof value === 'string') +function formatPrimitive(fn, value, ctx) { + if (typeof value === 'string') { + if (ctx.compact === false && + value.length > MIN_LINE_LENGTH && + ctx.indentationLvl + value.length > ctx.breakLength) { + // eslint-disable-next-line max-len + const minLineLength = Math.max(ctx.breakLength - ctx.indentationLvl, MIN_LINE_LENGTH); + // eslint-disable-next-line max-len + const averageLineLength = Math.ceil(value.length / Math.ceil(value.length / minLineLength)); + const divisor = Math.max(averageLineLength, MIN_LINE_LENGTH); + var res = ''; + if (readableRegExps[divisor] === undefined) { + // Build a new RegExp that naturally breaks text into multiple lines. + // + // Rules + // 1. Greedy match all text up the max line length that ends with a + // whitespace or the end of the string. + // 2. If none matches, non-greedy match any text up to a whitespace or + // the end of the string. + // + // eslint-disable-next-line max-len, no-unescaped-regexp-dot + readableRegExps[divisor] = new RegExp(`(.|\\n){1,${divisor}}(\\s|$)|(\\n|.)+?(\\s|$)`, 'gm'); + } + const indent = ' '.repeat(ctx.indentationLvl); + const matches = value.match(readableRegExps[divisor]); + if (matches.length > 1) { + res += `${fn(strEscape(matches[0]), 'string')} +\n`; + for (var i = 1; i < matches.length - 1; i++) { + res += `${indent} ${fn(strEscape(matches[i]), 'string')} +\n`; + } + res += `${indent} ${fn(strEscape(matches[i]), 'string')}`; + return res; + } + } return fn(strEscape(value), 'string'); + } if (typeof value === 'number') return formatNumber(fn, value); if (typeof value === 'boolean') @@ -806,7 +845,7 @@ function formatProperty(ctx, value, recurseTimes, key, array) { const desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key], enumerable: true }; if (desc.value !== undefined) { - const diff = array === 0 ? 3 : 2; + const diff = array !== 0 || ctx.compact === false ? 2 : 3; ctx.indentationLvl += diff; str = formatValue(ctx, desc.value, recurseTimes, array === 0); ctx.indentationLvl -= diff; @@ -839,9 +878,19 @@ function formatProperty(ctx, value, recurseTimes, key, array) { function reduceToSingleString(ctx, output, base, braces, addLn) { const breakLength = ctx.breakLength; + var i = 0; + if (ctx.compact === false) { + const indentation = ' '.repeat(ctx.indentationLvl); + var res = `${base ? `${base} ` : ''}${braces[0]}\n${indentation} `; + for (; i < output.length - 1; i++) { + res += `${output[i]},\n${indentation} `; + } + res += `${output[i]}\n${indentation}${braces[1]}`; + return res; + } if (output.length * 2 <= breakLength) { var length = 0; - for (var i = 0; i < output.length && length <= breakLength; i++) { + for (; i < output.length && length <= breakLength; i++) { if (ctx.colors) { length += removeColors(output[i]).length + 1; } else { @@ -849,7 +898,8 @@ function reduceToSingleString(ctx, output, base, braces, addLn) { } } if (length <= breakLength) - return `${braces[0]}${base} ${join(output, ', ')} ${braces[1]}`; + return `${braces[0]}${base ? ` ${base}` : ''} ${join(output, ', ')} ` + + braces[1]; } // If the opening "brace" is too large, like in the case of "Set {", // we need to force the first item to be on the next line or the @@ -857,7 +907,7 @@ function reduceToSingleString(ctx, output, base, braces, addLn) { const indentation = ' '.repeat(ctx.indentationLvl); const extraLn = addLn === true ? `\n${indentation}` : ''; const ln = base === '' && braces[0].length === 1 ? - ' ' : `${base}\n${indentation} `; + ' ' : `${base ? ` ${base}` : base}\n${indentation} `; const str = join(output, `,\n${indentation} `); return `${extraLn}${braces[0]}${ln}${str} ${braces[1]}`; } diff --git a/test/message/assert_throws_stack.js b/test/message/assert_throws_stack.js new file mode 100644 index 00000000000000..36bc5734cae37f --- /dev/null +++ b/test/message/assert_throws_stack.js @@ -0,0 +1,6 @@ +'use strict'; + +require('../common'); +const assert = require('assert').strict; + +assert.throws(() => { throw new Error('foo'); }, { bar: true }); diff --git a/test/message/assert_throws_stack.out b/test/message/assert_throws_stack.out new file mode 100644 index 00000000000000..d34bdd24798190 --- /dev/null +++ b/test/message/assert_throws_stack.out @@ -0,0 +1,14 @@ +assert.js:* + throw new AssertionError(obj); + ^ + +AssertionError [ERR_ASSERTION]: bar: expected true, not undefined + at Object. (*assert_throws_stack.js:*:*) + at * + at * + at * + at * + at * + at * + at * + at * diff --git a/test/message/error_exit.out b/test/message/error_exit.out index d6fbded760106b..8f03f08a7e0392 100644 --- a/test/message/error_exit.out +++ b/test/message/error_exit.out @@ -1,6 +1,6 @@ Exiting with code=1 assert.js:* - throw new errors.AssertionError({ + throw new AssertionError(obj); ^ AssertionError [ERR_ASSERTION]: 1 === 2 diff --git a/test/parallel/test-assert.js b/test/parallel/test-assert.js index 971c99be918d6a..96d5ff20a5ce26 100644 --- a/test/parallel/test-assert.js +++ b/test/parallel/test-assert.js @@ -25,6 +25,7 @@ const common = require('../common'); const assert = require('assert'); +const { inspect } = require('util'); const a = assert; function makeBlock(f) { @@ -39,16 +40,6 @@ assert.ok(a.AssertionError.prototype instanceof Error, assert.throws(makeBlock(a, false), a.AssertionError, 'ok(false)'); -// Using a object as second arg results in a failure -assert.throws( - () => { assert.throws(() => { throw new Error(); }, { foo: 'bar' }); }, - common.expectsError({ - type: TypeError, - message: 'expected.test is not a function' - }) -); - - assert.doesNotThrow(makeBlock(a, true), a.AssertionError, 'ok(true)'); assert.doesNotThrow(makeBlock(a, 'test', 'ok(\'test\')')); @@ -476,33 +467,21 @@ common.expectsError( } ); -{ - let threw = false; - try { - assert.doesNotThrow(makeBlock(thrower, Error), 'user message'); - } catch (e) { - threw = true; - common.expectsError({ - code: 'ERR_ASSERTION', - message: /Got unwanted exception: user message\n\[object Object\]/ - })(e); +common.expectsError( + () => assert.doesNotThrow(makeBlock(thrower, Error), 'user message'), + { + code: 'ERR_ASSERTION', + message: /Got unwanted exception: user message\n\[object Object\]/ } - assert.ok(threw); -} +); -{ - let threw = false; - try { - assert.doesNotThrow(makeBlock(thrower, Error)); - } catch (e) { - threw = true; - common.expectsError({ - code: 'ERR_ASSERTION', - message: /Got unwanted exception\.\n\[object Object\]/ - })(e); +common.expectsError( + () => assert.doesNotThrow(makeBlock(thrower, Error)), + { + code: 'ERR_ASSERTION', + message: /Got unwanted exception\.\n\[object Object\]/ } - assert.ok(threw); -} +); // make sure that validating using constructor really works { @@ -634,6 +613,7 @@ testAssertionMessage({ a: NaN, b: Infinity, c: -Infinity }, } catch (e) { threw = true; assert.strictEqual(e.message, 'Missing expected exception.'); + assert.ok(!e.stack.includes('throws'), e.stack); } assert.ok(threw); } @@ -678,6 +658,7 @@ try { threw = true; assert.ok(e.message.includes(rangeError.message)); assert.ok(e instanceof assert.AssertionError); + assert.ok(!e.stack.includes('doesNotThrow'), e.stack); } assert.ok(threw); } @@ -689,21 +670,15 @@ try { } const testBlockTypeError = (method, block) => { - let threw = true; - - try { - method(block); - threw = false; - } catch (e) { - common.expectsError({ + common.expectsError( + () => method(block), + { code: 'ERR_INVALID_ARG_TYPE', type: TypeError, message: 'The "block" argument must be of type Function. Received ' + - `type ${typeName(block)}` - })(e); - } - - assert.ok(threw); + `type ${typeName(block)}` + } + ); }; testBlockTypeError(assert.throws, 'string'); @@ -736,7 +711,8 @@ assert.throws(() => { assert.strictEqual('A'.repeat(1000), ''); }, common.expectsError({ code: 'ERR_ASSERTION', - message: new RegExp(`^'${'A'.repeat(127)} === ''$`) })); + message: /^'A{124}\.\.\. === ''$/ +})); { // bad args to AssertionError constructor should throw TypeError @@ -761,3 +737,294 @@ common.expectsError( message: /^'Error: foo' === 'Error: foobar'$/ } ); + +// Test strict assert +{ + const a = require('assert'); + const assert = require('assert').strict; + /* eslint-disable no-restricted-properties */ + assert.throws(() => assert.equal(1, true), assert.AssertionError); + assert.notEqual(0, false); + assert.throws(() => assert.deepEqual(1, true), assert.AssertionError); + assert.notDeepEqual(0, false); + assert.equal(assert.strict, assert.strict.strict); + assert.equal(assert.equal, assert.strictEqual); + assert.equal(assert.deepEqual, assert.deepStrictEqual); + assert.equal(assert.notEqual, assert.notStrictEqual); + assert.equal(assert.notDeepEqual, assert.notDeepStrictEqual); + assert.equal(Object.keys(assert).length, Object.keys(a).length); + assert(7); + common.expectsError( + () => assert(), + { + code: 'ERR_ASSERTION', + type: assert.AssertionError, + message: 'undefined == true' + } + ); + + // Test error diffs + const colors = process.stdout.isTTY && process.stdout.getColorDepth() > 1; + const start = 'Input A expected to deepStrictEqual input B:'; + const actExp = colors ? + '\u001b[32m+ expected\u001b[39m \u001b[31m- actual\u001b[39m' : + '+ expected - actual'; + const plus = colors ? '\u001b[32m+\u001b[39m' : '+'; + const minus = colors ? '\u001b[31m-\u001b[39m' : '-'; + let message = [ + start, + `${actExp} ... Lines skipped`, + '', + ' [', + ' [', + '...', + ' 2,', + `${minus} 3`, + `${plus} '3'`, + ' ]', + '...', + ' 5', + ' ]'].join('\n'); + assert.throws( + () => assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]), + { message }); + + message = [ + start, + `${actExp} ... Lines skipped`, + '', + ' [', + ' 1,', + '...', + ' 0,', + `${plus} 1,`, + ' 1,', + '...', + ' 1', + ' ]' + ].join('\n'); + assert.throws( + () => assert.deepEqual( + [1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1]), + { message }); + + message = [ + start, + `${actExp} ... Lines skipped`, + '', + ' [', + ' 1,', + '...', + ' 0,', + `${minus} 1,`, + ' 1,', + '...', + ' 1', + ' ]' + ].join('\n'); + assert.throws( + () => assert.deepEqual( + [1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1]), + { message }); + + message = [ + start, + actExp, + '', + ' [', + ' 1,', + `${minus} 2,`, + `${plus} 1,`, + ' 1,', + ' 1,', + ' 0,', + `${minus} 1,`, + ' 1', + ' ]' + ].join('\n'); + assert.throws( + () => assert.deepEqual( + [1, 2, 1, 1, 0, 1, 1], + [1, 1, 1, 1, 0, 1]), + { message }); + + message = [ + start, + actExp, + '', + `${minus} [`, + `${minus} 1,`, + `${minus} 2,`, + `${minus} 1`, + `${minus} ]`, + `${plus} undefined`, + ].join('\n'); + assert.throws( + () => assert.deepEqual([1, 2, 1]), + { message }); + + message = [ + start, + actExp, + '', + ' [', + `${minus} 1,`, + ' 2,', + ' 1', + ' ]' + ].join('\n'); + assert.throws( + () => assert.deepEqual([1, 2, 1], [2, 1]), + { message }); + + message = `${start}\n` + + `${actExp} ... Lines skipped\n` + + '\n' + + ' [\n' + + `${minus} 1,\n`.repeat(10) + + '...\n' + + `${plus} 2,\n`.repeat(10) + + '...'; + assert.throws( + () => assert.deepEqual(Array(12).fill(1), Array(12).fill(2)), + { message }); + + const obj1 = {}; + const obj2 = { loop: 'forever' }; + obj2[inspect.custom] = () => '{}'; + // No infinite loop and no custom inspect. + assert.throws(() => assert.deepEqual(obj1, obj2), { + message: `${start}\n` + + `${actExp}\n` + + '\n' + + `${minus} {}\n` + + `${plus} {\n` + + `${plus} loop: 'forever',\n` + + `${plus} [Symbol(util.inspect.custom)]: [Function]\n` + + `${plus} }` + }); + + // notDeepEqual tests + message = 'Identical input passed to notDeepStrictEqual:\n[\n 1\n]'; + assert.throws( + () => assert.notDeepEqual([1], [1]), + { message }); + + message = 'Identical input passed to notDeepStrictEqual:' + + `\n[${'\n 1,'.repeat(18)}\n...`; + const data = Array(21).fill(1); + assert.throws( + () => assert.notDeepEqual(data, data), + { message }); + /* eslint-enable no-restricted-properties */ +} + +common.expectsError( + () => assert.ok(null), + { + code: 'ERR_ASSERTION', + type: assert.AssertionError, + message: 'null == true' + } +); + +common.expectsError( + // eslint-disable-next-line no-restricted-syntax + () => assert.throws(() => {}, 'Error message', 'message'), + { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError, + message: 'The "error" argument must be one of type Function or RegExp. ' + + 'Received type string' + } +); + +{ + const errFn = () => { + const err = new TypeError('Wrong value'); + err.code = 404; + throw err; + }; + const errObj = { + name: 'TypeError', + message: 'Wrong value' + }; + assert.throws(errFn, errObj); + + errObj.code = 404; + assert.throws(errFn, errObj); + + errObj.code = '404'; + common.expectsError( + // eslint-disable-next-line no-restricted-syntax + () => assert.throws(errFn, errObj), + { + code: 'ERR_ASSERTION', + type: assert.AssertionError, + message: 'code: expected \'404\', not 404' + } + ); + + errObj.code = 404; + errObj.foo = 'bar'; + common.expectsError( + // eslint-disable-next-line no-restricted-syntax + () => assert.throws(errFn, errObj), + { + code: 'ERR_ASSERTION', + type: assert.AssertionError, + message: 'foo: expected \'bar\', not undefined' + } + ); + + common.expectsError( + () => assert.throws(() => { throw new Error(); }, { foo: 'bar' }, 'foobar'), + { + type: assert.AssertionError, + code: 'ERR_ASSERTION', + message: 'foobar' + } + ); + + common.expectsError( + () => assert.doesNotThrow(() => { throw new Error(); }, { foo: 'bar' }), + { + type: TypeError, + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "expected" argument must be one of type Function or ' + + 'RegExp. Received type object' + } + ); + + assert.throws(() => { throw new Error('e'); }, new Error('e')); + common.expectsError( + () => assert.throws(() => { throw new TypeError('e'); }, new Error('e')), + { + type: assert.AssertionError, + code: 'ERR_ASSERTION', + message: "name: expected 'Error', not 'TypeError'" + } + ); + common.expectsError( + () => assert.throws(() => { throw new Error('foo'); }, new Error('')), + { + type: assert.AssertionError, + code: 'ERR_ASSERTION', + message: "message: expected '', not 'foo'" + } + ); + + // eslint-disable-next-line no-throw-literal + assert.throws(() => { throw undefined; }, /undefined/); + common.expectsError( + // eslint-disable-next-line no-throw-literal + () => assert.doesNotThrow(() => { throw undefined; }), + { + type: assert.AssertionError, + code: 'ERR_ASSERTION', + message: 'Got unwanted exception.\nundefined' + } + ); +} diff --git a/test/parallel/test-tty-get-color-depth.js b/test/parallel/test-tty-get-color-depth.js new file mode 100644 index 00000000000000..a5a998b6a3ab78 --- /dev/null +++ b/test/parallel/test-tty-get-color-depth.js @@ -0,0 +1,52 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert').strict; +/* eslint-disable no-restricted-properties */ +const { openSync } = require('fs'); +const tty = require('tty'); + +const { WriteStream } = require('tty'); + +// Do our best to grab a tty fd. +function getTTYfd() { + const ttyFd = [0, 1, 2, 4, 5].find(tty.isatty); + if (ttyFd === undefined) { + try { + return openSync('/dev/tty'); + } catch (e) { + // There aren't any tty fd's available to use. + return -1; + } + } + return ttyFd; +} + +const fd = getTTYfd(); + +// Give up if we did not find a tty +if (fd === -1) + common.skip(); + +const writeStream = new WriteStream(fd); + +let depth = writeStream.getColorDepth(); + +assert.equal(typeof depth, 'number'); +assert(depth >= 1 && depth <= 24); + +// If the terminal does not support colors, skip the rest +if (depth === 1) + common.skip(); + +assert.notEqual(writeStream.getColorDepth({ TERM: 'dumb' }), depth); + +// Deactivate colors +const tmp = process.env.NODE_DISABLE_COLORS; +process.env.NODE_DISABLE_COLORS = 1; + +depth = writeStream.getColorDepth(); + +assert.equal(depth, 1); + +process.env.NODE_DISABLE_COLORS = tmp; diff --git a/test/parallel/test-util-inspect.js b/test/parallel/test-util-inspect.js index a99583d454b3f4..83f6b469d68055 100644 --- a/test/parallel/test-util-inspect.js +++ b/test/parallel/test-util-inspect.js @@ -1160,3 +1160,146 @@ if (typeof Symbol !== 'undefined') { assert.doesNotThrow(() => util.inspect(process)); /* eslint-enable accessor-pairs */ + +// Setting custom inspect property to a non-function should do nothing. +{ + const obj = { inspect: 'fhqwhgads' }; + assert.strictEqual(util.inspect(obj), "{ inspect: 'fhqwhgads' }"); +} + +{ + const o = { + a: [1, 2, [[ + 'Lorem ipsum dolor\nsit amet,\tconsectetur adipiscing elit, sed do ' + + 'eiusmod tempor incididunt ut labore et dolore magna aliqua.', + 'test', + 'foo']], 4], + b: new Map([['za', 1], ['zb', 'test']]) + }; + + let out = util.inspect(o, { compact: true, depth: 5, breakLength: 80 }); + let expect = [ + '{ a: ', + ' [ 1,', + ' 2,', + " [ [ 'Lorem ipsum dolor\\nsit amet,\\tconsectetur adipiscing elit, " + + "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',", + " 'test',", + " 'foo' ] ],", + ' 4 ],', + " b: Map { 'za' => 1, 'zb' => 'test' } }", + ].join('\n'); + assert.strictEqual(out, expect); + + out = util.inspect(o, { compact: false, depth: 5, breakLength: 60 }); + expect = [ + '{', + ' a: [', + ' 1,', + ' 2,', + ' [', + ' [', + ' \'Lorem ipsum dolor\\nsit amet,\\tconsectetur \' +', + ' \'adipiscing elit, sed do eiusmod tempor \' +', + ' \'incididunt ut labore et dolore magna \' +', + ' \'aliqua.\',', + ' \'test\',', + ' \'foo\'', + ' ]', + ' ],', + ' 4', + ' ],', + ' b: Map {', + ' \'za\' => 1,', + ' \'zb\' => \'test\'', + ' }', + '}' + ].join('\n'); + assert.strictEqual(out, expect); + + out = util.inspect(o.a[2][0][0], { compact: false, breakLength: 30 }); + expect = [ + '\'Lorem ipsum dolor\\nsit \' +', + ' \'amet,\\tconsectetur \' +', + ' \'adipiscing elit, sed do \' +', + ' \'eiusmod tempor incididunt \' +', + ' \'ut labore et dolore magna \' +', + ' \'aliqua.\'' + ].join('\n'); + assert.strictEqual(out, expect); + + out = util.inspect( + '12345678901234567890123456789012345678901234567890', + { compact: false, breakLength: 3 }); + expect = '\'12345678901234567890123456789012345678901234567890\''; + assert.strictEqual(out, expect); + + out = util.inspect( + '12 45 78 01 34 67 90 23 56 89 123456789012345678901234567890', + { compact: false, breakLength: 3 }); + expect = [ + '\'12 45 78 01 34 \' +', + ' \'67 90 23 56 89 \' +', + ' \'123456789012345678901234567890\'' + ].join('\n'); + assert.strictEqual(out, expect); + + out = util.inspect( + '12 45 78 01 34 67 90 23 56 89 1234567890123 0', + { compact: false, breakLength: 3 }); + expect = [ + '\'12 45 78 01 34 \' +', + ' \'67 90 23 56 89 \' +', + ' \'1234567890123 0\'' + ].join('\n'); + assert.strictEqual(out, expect); + + out = util.inspect( + '12 45 78 01 34 67 90 23 56 89 12345678901234567 0', + { compact: false, breakLength: 3 }); + expect = [ + '\'12 45 78 01 34 \' +', + ' \'67 90 23 56 89 \' +', + ' \'12345678901234567 \' +', + ' \'0\'' + ].join('\n'); + assert.strictEqual(out, expect); + + o.a = () => {}; + o.b = new Number(3); + out = util.inspect(o, { compact: false, breakLength: 3 }); + expect = [ + '{', + ' a: [Function],', + ' b: [Number: 3]', + '}' + ].join('\n'); + assert.strictEqual(out, expect); + + out = util.inspect(o, { compact: false, breakLength: 3, showHidden: true }); + expect = [ + '{', + ' a: [Function] {', + ' [length]: 0,', + " [name]: ''", + ' },', + ' b: [Number: 3]', + '}' + ].join('\n'); + assert.strictEqual(out, expect); + + o[util.inspect.custom] = () => 42; + out = util.inspect(o, { compact: false, breakLength: 3 }); + expect = '42'; + assert.strictEqual(out, expect); + + o[util.inspect.custom] = () => '12 45 78 01 34 67 90 23'; + out = util.inspect(o, { compact: false, breakLength: 3 }); + expect = '12 45 78 01 34 67 90 23'; + assert.strictEqual(out, expect); + + o[util.inspect.custom] = () => ({ a: '12 45 78 01 34 67 90 23' }); + out = util.inspect(o, { compact: false, breakLength: 3 }); + expect = '{\n a: \'12 45 78 01 34 \' +\n \'67 90 23\'\n}'; + assert.strictEqual(out, expect); +}