diff --git a/lib/assert.js b/lib/assert.js index ac9fd30793aaf7..27e71c2751ed74 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -25,15 +25,12 @@ const { isDeepEqual, isDeepStrictEqual } = require('internal/util/comparisons'); -const { - AssertionError, - errorCache, - codes: { - ERR_AMBIGUOUS_ARGUMENT, - ERR_INVALID_ARG_TYPE, - ERR_INVALID_RETURN_VALUE - } -} = require('internal/errors'); +const { codes: { + ERR_AMBIGUOUS_ARGUMENT, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_RETURN_VALUE +} } = require('internal/errors'); +const { AssertionError, errorCache } = require('internal/assert'); const { openSync, closeSync, readSync } = require('fs'); const { inspect, types: { isPromise, isRegExp } } = require('util'); const { EOL } = require('os'); diff --git a/lib/internal/assert.js b/lib/internal/assert.js new file mode 100644 index 00000000000000..990065a9378f14 --- /dev/null +++ b/lib/internal/assert.js @@ -0,0 +1,275 @@ +'use strict'; + +const { inspect } = require('util'); +const { codes: { + ERR_INVALID_ARG_TYPE +} } = require('internal/errors'); + +let blue = ''; +let green = ''; +let red = ''; +let white = ''; + +const READABLE_OPERATOR = { + deepStrictEqual: 'Input A expected to strictly deep-equal input B', + notDeepStrictEqual: 'Input A expected to strictly not deep-equal input B', + strictEqual: 'Input A expected to strictly equal input B', + notStrictEqual: 'Input A expected to strictly not equal input B' +}; + +function copyError(source) { + const keys = Object.keys(source); + const target = Object.create(Object.getPrototypeOf(source)); + for (const key of keys) { + target[key] = source[key]; + } + Object.defineProperty(target, 'message', { value: source.message }); + return target; +} + +function inspectValue(val) { + // The util.inspect default values could be changed. This makes sure the + // error messages contain the necessary information nevertheless. + return inspect( + val, + { + compact: false, + customInspect: false, + depth: 1000, + maxArrayLength: Infinity, + // Assert compares only enumerable properties (with a few exceptions). + showHidden: false, + // Having a long line as error is better than wrapping the line for + // comparison. + breakLength: Infinity, + // Assert does not detect proxies currently. + showProxy: false + } + ).split('\n'); +} + +function createErrDiff(actual, expected, operator) { + var other = ''; + var res = ''; + var lastPos = 0; + var end = ''; + var skipped = false; + const actualLines = inspectValue(actual); + const expectedLines = inspectValue(expected); + const msg = READABLE_OPERATOR[operator] + + `:\n${green}+ expected${white} ${red}- actual${white}`; + const skippedMsg = ` ${blue}...${white} 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${blue}...${white}${end}`; + skipped = true; + } + if (other !== '') { + end = `\n ${other}${end}`; + other = ''; + } + + const maxLines = Math.max(actualLines.length, expectedLines.length); + var printedLines = 0; + var identical = 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${blue}...${white}`; + 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${blue}...${white}`; + 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${blue}...${white}`; + 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++; + } + identical++; + } + // Inspected object to big (Show ~20 rows max) + if (printedLines > 20 && i < maxLines - 2) { + return `${msg}${skippedMsg}\n${res}\n${blue}...${white}${other}\n` + + `${blue}...${white}`; + } + } + + // Strict equal with identical objects that are not identical by reference. + if (identical === maxLines) { + // E.g., assert.deepStrictEqual(Symbol(), Symbol()) + const base = operator === 'strictEqual' ? + 'Input objects identical but not reference equal:' : + 'Input objects not identical:'; + + // We have to get the result again. The lines were all removed before. + const actualLines = inspectValue(actual); + + // Only remove lines in case it makes sense to collapse those. + // TODO: Accept env to always show the full error. + if (actualLines.length > 30) { + actualLines[26] = `${blue}...${white}`; + while (actualLines.length > 27) { + actualLines.pop(); + } + } + + return `${base}\n\n${actualLines.join('\n')}\n`; + } + return `${msg}${skipped ? skippedMsg : ''}\n${res}${other}${end}`; +} + +class AssertionError extends Error { + constructor(options) { + if (typeof options !== 'object' || options === null) { + throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); + } + var { + actual, + expected, + message, + operator, + stackStartFn + } = options; + + if (message != null) { + super(message); + } else { + if (process.stdout.isTTY) { + // Reset on each call to make sure we handle dynamically set environment + // variables correct. + if (process.stdout.getColorDepth() !== 1) { + blue = '\u001b[34m'; + green = '\u001b[32m'; + white = '\u001b[39m'; + red = '\u001b[31m'; + } else { + blue = ''; + green = ''; + white = ''; + red = ''; + } + } + // Prevent the error stack from being visible by duplicating the error + // in a very close way to the original in case both sides are actually + // instances of Error. + if (typeof actual === 'object' && actual !== null && + typeof expected === 'object' && expected !== null && + 'stack' in actual && actual instanceof Error && + 'stack' in expected && expected instanceof Error) { + actual = copyError(actual); + expected = copyError(expected); + } + + if (operator === 'deepStrictEqual' || operator === 'strictEqual') { + super(createErrDiff(actual, expected, operator)); + } else if (operator === 'notDeepStrictEqual' || + operator === 'notStrictEqual') { + // In case the objects are equal but the operator requires unequal, show + // the first object and say A equals B + const res = inspectValue(actual); + const base = `Identical input passed to ${operator}:`; + + // Only remove lines in case it makes sense to collapse those. + // TODO: Accept env to always show the full error. + if (res.length > 30) { + res[26] = `${blue}...${white}`; + while (res.length > 27) { + res.pop(); + } + } + + // Only print a single input. + if (res.length === 1) { + super(`${base} ${res[0]}`); + } else { + super(`${base}\n\n${res.join('\n')}\n`); + } + } else { + let res = inspect(actual); + let other = inspect(expected); + if (res.length > 128) + res = `${res.slice(0, 125)}...`; + if (other.length > 128) + other = `${other.slice(0, 125)}...`; + super(`${res} ${operator} ${other}`); + } + } + + this.generatedMessage = !message; + this.name = 'AssertionError [ERR_ASSERTION]'; + this.code = 'ERR_ASSERTION'; + this.actual = actual; + this.expected = expected; + this.operator = operator; + Error.captureStackTrace(this, stackStartFn); + } +} + +module.exports = { + AssertionError, + errorCache: new Map() +}; diff --git a/lib/internal/errors.js b/lib/internal/errors.js index fc306e256c1f1a..0a46e2e7a9946c 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -15,18 +15,6 @@ const kInfo = Symbol('info'); const messages = new Map(); const codes = {}; -let blue = ''; -let green = ''; -let red = ''; -let white = ''; - -const READABLE_OPERATOR = { - deepStrictEqual: 'Input A expected to strictly deep-equal input B', - notDeepStrictEqual: 'Input A expected to strictly not deep-equal input B', - strictEqual: 'Input A expected to strictly equal input B', - notStrictEqual: 'Input A expected to strictly not equal input B' -}; - const { errmap, UV_EAI_MEMORY, @@ -37,10 +25,10 @@ const { kMaxLength } = process.binding('buffer'); const { defineProperty } = Object; // Lazily loaded -var util; -var buffer; +let util; +let assert; -var internalUtil = null; +let internalUtil = null; function lazyInternalUtil() { if (!internalUtil) { internalUtil = require('internal/util'); @@ -48,35 +36,11 @@ function lazyInternalUtil() { return internalUtil; } -function copyError(source) { - const keys = Object.keys(source); - const target = Object.create(Object.getPrototypeOf(source)); - for (const key of keys) { - target[key] = source[key]; - } - Object.defineProperty(target, 'message', { value: source.message }); - return target; -} - -function inspectValue(val) { - // The util.inspect default values could be changed. This makes sure the - // error messages contain the necessary information nevertheless. - return util.inspect( - val, - { - compact: false, - customInspect: false, - depth: 1000, - maxArrayLength: Infinity, - // Assert compares only enumerable properties (with a few exceptions). - showHidden: false, - // Having a long line as error is better than wrapping the line for - // comparison. - breakLength: Infinity, - // Assert does not detect proxies currently. - showProxy: false - } - ).split('\n'); +let buffer; +function lazyBuffer() { + if (buffer === undefined) + buffer = require('buffer').Buffer; + return buffer; } // A specialized Error that includes an additional info property with @@ -241,254 +205,14 @@ function E(sym, val, def, ...otherClasses) { codes[sym] = def; } -function lazyBuffer() { - if (buffer === undefined) - buffer = require('buffer').Buffer; - return buffer; -} - -function createErrDiff(actual, expected, operator) { - var other = ''; - var res = ''; - var lastPos = 0; - var end = ''; - var skipped = false; - if (util === undefined) util = require('util'); - const actualLines = inspectValue(actual); - const expectedLines = inspectValue(expected); - const msg = READABLE_OPERATOR[operator] + - `:\n${green}+ expected${white} ${red}- actual${white}`; - const skippedMsg = ` ${blue}...${white} 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${blue}...${white}${end}`; - skipped = true; - } - if (other !== '') { - end = `\n ${other}${end}`; - other = ''; - } - - const maxLines = Math.max(actualLines.length, expectedLines.length); - var printedLines = 0; - var identical = 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${blue}...${white}`; - 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${blue}...${white}`; - 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${blue}...${white}`; - 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++; - } - identical++; - } - // Inspected object to big (Show ~20 rows max) - if (printedLines > 20 && i < maxLines - 2) { - return `${msg}${skippedMsg}\n${res}\n${blue}...${white}${other}\n` + - `${blue}...${white}`; - } - } - - // Strict equal with identical objects that are not identical by reference. - if (identical === maxLines) { - // E.g., assert.deepStrictEqual(Symbol(), Symbol()) - const base = operator === 'strictEqual' ? - 'Input objects identical but not reference equal:' : - 'Input objects not identical:'; - - // We have to get the result again. The lines were all removed before. - const actualLines = inspectValue(actual); - - // Only remove lines in case it makes sense to collapse those. - // TODO: Accept env to always show the full error. - if (actualLines.length > 30) { - actualLines[26] = `${blue}...${white}`; - while (actualLines.length > 27) { - actualLines.pop(); - } - } - - return `${base}\n\n${actualLines.join('\n')}\n`; - } - return `${msg}${skipped ? skippedMsg : ''}\n${res}${other}${end}`; -} - -class AssertionError extends Error { - constructor(options) { - if (typeof options !== 'object' || options === null) { - throw new codes.ERR_INVALID_ARG_TYPE('options', 'Object', options); - } - var { - actual, - expected, - message, - operator, - stackStartFn - } = options; - - if (message != null) { - super(message); - } else { - if (process.stdout.isTTY) { - // Reset on each call to make sure we handle dynamically set environment - // variables correct. - if (process.stdout.getColorDepth() !== 1) { - blue = '\u001b[34m'; - green = '\u001b[32m'; - white = '\u001b[39m'; - red = '\u001b[31m'; - } else { - blue = ''; - green = ''; - white = ''; - red = ''; - } - } - if (util === undefined) util = require('util'); - // Prevent the error stack from being visible by duplicating the error - // in a very close way to the original in case both sides are actually - // instances of Error. - if (typeof actual === 'object' && actual !== null && - typeof expected === 'object' && expected !== null && - 'stack' in actual && actual instanceof Error && - 'stack' in expected && expected instanceof Error) { - actual = copyError(actual); - expected = copyError(expected); - } - - if (operator === 'deepStrictEqual' || operator === 'strictEqual') { - super(createErrDiff(actual, expected, operator)); - } else if (operator === 'notDeepStrictEqual' || - operator === 'notStrictEqual') { - // In case the objects are equal but the operator requires unequal, show - // the first object and say A equals B - const res = inspectValue(actual); - const base = `Identical input passed to ${operator}:`; - - // Only remove lines in case it makes sense to collapse those. - // TODO: Accept env to always show the full error. - if (res.length > 30) { - res[26] = `${blue}...${white}`; - while (res.length > 27) { - res.pop(); - } - } - - // Only print a single input. - if (res.length === 1) { - super(`${base} ${res[0]}`); - } else { - super(`${base}\n\n${res.join('\n')}\n`); - } - } else { - 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}`); - } - } - - this.generatedMessage = !message; - this.name = 'AssertionError [ERR_ASSERTION]'; - this.code = 'ERR_ASSERTION'; - this.actual = actual; - this.expected = expected; - this.operator = operator; - Error.captureStackTrace(this, stackStartFn); - } -} - -// This is defined here instead of using the assert module to avoid a -// circular dependency. The effect is largely the same. -function internalAssert(condition, message) { - if (!condition) { - throw new AssertionError({ - message, - actual: false, - expected: true, - operator: '==' - }); - } -} - function getMessage(key, args) { const msg = messages.get(key); + if (util === undefined) util = require('util'); + if (assert === undefined) assert = require('assert'); if (typeof msg === 'function') { - internalAssert( + assert( msg.length <= args.length, // Default options do not count. `Code: ${key}; The provided arguments length (${args.length}) does not ` + `match the required ones (${msg.length}).` @@ -497,7 +221,7 @@ function getMessage(key, args) { } const expectedLength = (msg.match(/%[dfijoOs]/g) || []).length; - internalAssert( + assert( expectedLength === args.length, `Code: ${key}; The provided arguments length (${args.length}) does not ` + `match the required ones (${expectedLength}).` @@ -693,11 +417,9 @@ module.exports = { uvException, isStackOverflowError, getMessage, - AssertionError, SystemError, codes, - E, // This is exported only to facilitate testing. - errorCache: new Map() // This is in here only to facilitate testing. + E // This is exported only to facilitate testing. }; // To declare an error message, use the E(sym, val, def) function above. The sym @@ -1045,7 +767,7 @@ E('ERR_VM_MODULE_STATUS', 'Module status %s', Error); E('ERR_ZLIB_INITIALIZATION_FAILED', 'Initialization failed', Error); function invalidArgType(name, expected, actual) { - internalAssert(typeof name === 'string', 'name must be a string'); + assert(typeof name === 'string', "'name' must be a string"); // determiner: 'must be' or 'must not be' let determiner; @@ -1071,7 +793,7 @@ function invalidArgType(name, expected, actual) { } function missingArgs(...args) { - internalAssert(args.length > 0, 'At least one arg needs to be specified'); + assert(args.length > 0, 'At least one arg needs to be specified'); let msg = 'The '; const len = args.length; args = args.map((a) => `"${a}"`); @@ -1091,11 +813,11 @@ function missingArgs(...args) { } function oneOf(expected, thing) { - internalAssert(typeof thing === 'string', '`thing` has to be of type string'); + assert(typeof thing === 'string', '`thing` has to be of type string'); if (Array.isArray(expected)) { const len = expected.length; - internalAssert(len > 0, - 'At least one expected value needs to be specified'); + assert(len > 0, + 'At least one expected value needs to be specified'); expected = expected.map((i) => String(i)); if (len > 2) { return `one of ${thing} ${expected.slice(0, len - 1).join(', ')}, or ` + diff --git a/node.gyp b/node.gyp index 851fdb8c04cc81..621669e514f139 100644 --- a/node.gyp +++ b/node.gyp @@ -79,6 +79,7 @@ 'lib/v8.js', 'lib/vm.js', 'lib/zlib.js', + 'lib/internal/assert.js', 'lib/internal/async_hooks.js', 'lib/internal/buffer.js', 'lib/internal/cli_table.js', diff --git a/test/parallel/test-assert.js b/test/parallel/test-assert.js index cbcda17ab7188d..cab6fb33e1887a 100644 --- a/test/parallel/test-assert.js +++ b/test/parallel/test-assert.js @@ -29,7 +29,7 @@ const common = require('../common'); const assert = require('assert'); const { EOL } = require('os'); const EventEmitter = require('events'); -const { errorCache } = require('internal/errors'); +const { errorCache } = require('internal/assert'); const { writeFileSync, unlinkSync } = require('fs'); const { inspect } = require('util'); const a = assert;