diff options
author | Ruben Bridgewater <ruben@bridgewater.de> | 2018-05-02 17:21:40 +0300 |
---|---|---|
committer | Ruben Bridgewater <ruben@bridgewater.de> | 2018-05-10 14:39:19 +0300 |
commit | b304096a14f4b266738faeaded8ea31215528ea0 (patch) | |
tree | 018595b6f5f88e6f706d3fc33324fdfcf779ac77 /lib/internal/assert.js | |
parent | 4c6df73ae4e3c7ce0b2346bc38c04c6501239b02 (diff) |
assert: move AssertionError into own file
This moves the `assert` parts from `internal/errors` into an own
file. `internal/errors` got bigger and bigger and it was difficult
to keep a good overview of what was going on. While doing so it
also removes the `internalAssert` function and just lazy loads
`assert`.
PR-URL: https://github.com/nodejs/node/pull/20486
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Diffstat (limited to 'lib/internal/assert.js')
-rw-r--r-- | lib/internal/assert.js | 275 |
1 files changed, 275 insertions, 0 deletions
diff --git a/lib/internal/assert.js b/lib/internal/assert.js new file mode 100644 index 00000000000..990065a9378 --- /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() +}; |