1'use strict'; 2 3const { 4 ArrayPrototypeJoin, 5 ArrayPrototypePop, 6 Error, 7 ErrorCaptureStackTrace, 8 MathMax, 9 ObjectCreate, 10 ObjectDefineProperty, 11 ObjectGetPrototypeOf, 12 ObjectKeys, 13 String, 14 StringPrototypeEndsWith, 15 StringPrototypeRepeat, 16 StringPrototypeSlice, 17 StringPrototypeSplit, 18} = primordials; 19 20const { inspect } = require('internal/util/inspect'); 21const { 22 removeColors, 23} = require('internal/util'); 24const colors = require('internal/util/colors'); 25const { 26 validateObject, 27} = require('internal/validators'); 28const { isErrorStackTraceLimitWritable } = require('internal/errors'); 29 30 31const kReadableOperator = { 32 deepStrictEqual: 'Expected values to be strictly deep-equal:', 33 strictEqual: 'Expected values to be strictly equal:', 34 strictEqualObject: 'Expected "actual" to be reference-equal to "expected":', 35 deepEqual: 'Expected values to be loosely deep-equal:', 36 notDeepStrictEqual: 'Expected "actual" not to be strictly deep-equal to:', 37 notStrictEqual: 'Expected "actual" to be strictly unequal to:', 38 notStrictEqualObject: 39 'Expected "actual" not to be reference-equal to "expected":', 40 notDeepEqual: 'Expected "actual" not to be loosely deep-equal to:', 41 notIdentical: 'Values have same structure but are not reference-equal:', 42 notDeepEqualUnequal: 'Expected values not to be loosely deep-equal:', 43}; 44 45// Comparing short primitives should just show === / !== instead of using the 46// diff. 47const kMaxShortLength = 12; 48 49function copyError(source) { 50 const keys = ObjectKeys(source); 51 const target = ObjectCreate(ObjectGetPrototypeOf(source)); 52 for (const key of keys) { 53 target[key] = source[key]; 54 } 55 ObjectDefineProperty(target, 'message', { __proto__: null, value: source.message }); 56 return target; 57} 58 59function inspectValue(val) { 60 // The util.inspect default values could be changed. This makes sure the 61 // error messages contain the necessary information nevertheless. 62 return inspect( 63 val, 64 { 65 compact: false, 66 customInspect: false, 67 depth: 1000, 68 maxArrayLength: Infinity, 69 // Assert compares only enumerable properties (with a few exceptions). 70 showHidden: false, 71 // Assert does not detect proxies currently. 72 showProxy: false, 73 sorted: true, 74 // Inspect getters as we also check them when comparing entries. 75 getters: true, 76 }, 77 ); 78} 79 80function createErrDiff(actual, expected, operator) { 81 let other = ''; 82 let res = ''; 83 let end = ''; 84 let skipped = false; 85 const actualInspected = inspectValue(actual); 86 const actualLines = StringPrototypeSplit(actualInspected, '\n'); 87 const expectedLines = StringPrototypeSplit(inspectValue(expected), '\n'); 88 89 let i = 0; 90 let indicator = ''; 91 92 // In case both values are objects or functions explicitly mark them as not 93 // reference equal for the `strictEqual` operator. 94 if (operator === 'strictEqual' && 95 ((typeof actual === 'object' && actual !== null && 96 typeof expected === 'object' && expected !== null) || 97 (typeof actual === 'function' && typeof expected === 'function'))) { 98 operator = 'strictEqualObject'; 99 } 100 101 // If "actual" and "expected" fit on a single line and they are not strictly 102 // equal, check further special handling. 103 if (actualLines.length === 1 && expectedLines.length === 1 && 104 actualLines[0] !== expectedLines[0]) { 105 // Check for the visible length using the `removeColors()` function, if 106 // appropriate. 107 const c = inspect.defaultOptions.colors; 108 const actualRaw = c ? removeColors(actualLines[0]) : actualLines[0]; 109 const expectedRaw = c ? removeColors(expectedLines[0]) : expectedLines[0]; 110 const inputLength = actualRaw.length + expectedRaw.length; 111 // If the character length of "actual" and "expected" together is less than 112 // kMaxShortLength and if neither is an object and at least one of them is 113 // not `zero`, use the strict equal comparison to visualize the output. 114 if (inputLength <= kMaxShortLength) { 115 if ((typeof actual !== 'object' || actual === null) && 116 (typeof expected !== 'object' || expected === null) && 117 (actual !== 0 || expected !== 0)) { // -0 === +0 118 return `${kReadableOperator[operator]}\n\n` + 119 `${actualLines[0]} !== ${expectedLines[0]}\n`; 120 } 121 } else if (operator !== 'strictEqualObject') { 122 // If the stderr is a tty and the input length is lower than the current 123 // columns per line, add a mismatch indicator below the output. If it is 124 // not a tty, use a default value of 80 characters. 125 const maxLength = process.stderr.isTTY ? process.stderr.columns : 80; 126 if (inputLength < maxLength) { 127 while (actualRaw[i] === expectedRaw[i]) { 128 i++; 129 } 130 // Ignore the first characters. 131 if (i > 2) { 132 // Add position indicator for the first mismatch in case it is a 133 // single line and the input length is less than the column length. 134 indicator = `\n ${StringPrototypeRepeat(' ', i)}^`; 135 i = 0; 136 } 137 } 138 } 139 } 140 141 // Remove all ending lines that match (this optimizes the output for 142 // readability by reducing the number of total changed lines). 143 let a = actualLines[actualLines.length - 1]; 144 let b = expectedLines[expectedLines.length - 1]; 145 while (a === b) { 146 if (i++ < 3) { 147 end = `\n ${a}${end}`; 148 } else { 149 other = a; 150 } 151 ArrayPrototypePop(actualLines); 152 ArrayPrototypePop(expectedLines); 153 if (actualLines.length === 0 || expectedLines.length === 0) 154 break; 155 a = actualLines[actualLines.length - 1]; 156 b = expectedLines[expectedLines.length - 1]; 157 } 158 159 const maxLines = MathMax(actualLines.length, expectedLines.length); 160 // Strict equal with identical objects that are not identical by reference. 161 // E.g., assert.deepStrictEqual({ a: Symbol() }, { a: Symbol() }) 162 if (maxLines === 0) { 163 // We have to get the result again. The lines were all removed before. 164 const actualLines = StringPrototypeSplit(actualInspected, '\n'); 165 166 // Only remove lines in case it makes sense to collapse those. 167 // TODO: Accept env to always show the full error. 168 if (actualLines.length > 50) { 169 actualLines[46] = `${colors.blue}...${colors.white}`; 170 while (actualLines.length > 47) { 171 ArrayPrototypePop(actualLines); 172 } 173 } 174 175 return `${kReadableOperator.notIdentical}\n\n` + 176 `${ArrayPrototypeJoin(actualLines, '\n')}\n`; 177 } 178 179 // There were at least five identical lines at the end. Mark a couple of 180 // skipped. 181 if (i >= 5) { 182 end = `\n${colors.blue}...${colors.white}${end}`; 183 skipped = true; 184 } 185 if (other !== '') { 186 end = `\n ${other}${end}`; 187 other = ''; 188 } 189 190 let printedLines = 0; 191 let identical = 0; 192 const msg = kReadableOperator[operator] + 193 `\n${colors.green}+ actual${colors.white} ${colors.red}- expected${colors.white}`; 194 const skippedMsg = ` ${colors.blue}...${colors.white} Lines skipped`; 195 196 let lines = actualLines; 197 let plusMinus = `${colors.green}+${colors.white}`; 198 let maxLength = expectedLines.length; 199 if (actualLines.length < maxLines) { 200 lines = expectedLines; 201 plusMinus = `${colors.red}-${colors.white}`; 202 maxLength = actualLines.length; 203 } 204 205 for (i = 0; i < maxLines; i++) { 206 if (maxLength < i + 1) { 207 // If more than two former lines are identical, print them. Collapse them 208 // in case more than five lines were identical. 209 if (identical > 2) { 210 if (identical > 3) { 211 if (identical > 4) { 212 if (identical === 5) { 213 res += `\n ${lines[i - 3]}`; 214 printedLines++; 215 } else { 216 res += `\n${colors.blue}...${colors.white}`; 217 skipped = true; 218 } 219 } 220 res += `\n ${lines[i - 2]}`; 221 printedLines++; 222 } 223 res += `\n ${lines[i - 1]}`; 224 printedLines++; 225 } 226 // No identical lines before. 227 identical = 0; 228 // Add the expected line to the cache. 229 if (lines === actualLines) { 230 res += `\n${plusMinus} ${lines[i]}`; 231 } else { 232 other += `\n${plusMinus} ${lines[i]}`; 233 } 234 printedLines++; 235 // Only extra actual lines exist 236 // Lines diverge 237 } else { 238 const expectedLine = expectedLines[i]; 239 let actualLine = actualLines[i]; 240 // If the lines diverge, specifically check for lines that only diverge by 241 // a trailing comma. In that case it is actually identical and we should 242 // mark it as such. 243 let divergingLines = 244 actualLine !== expectedLine && 245 (!StringPrototypeEndsWith(actualLine, ',') || 246 StringPrototypeSlice(actualLine, 0, -1) !== expectedLine); 247 // If the expected line has a trailing comma but is otherwise identical, 248 // add a comma at the end of the actual line. Otherwise the output could 249 // look weird as in: 250 // 251 // [ 252 // 1 // No comma at the end! 253 // + 2 254 // ] 255 // 256 if (divergingLines && 257 StringPrototypeEndsWith(expectedLine, ',') && 258 StringPrototypeSlice(expectedLine, 0, -1) === actualLine) { 259 divergingLines = false; 260 actualLine += ','; 261 } 262 if (divergingLines) { 263 // If more than two former lines are identical, print them. Collapse 264 // them in case more than five lines were identical. 265 if (identical > 2) { 266 if (identical > 3) { 267 if (identical > 4) { 268 if (identical === 5) { 269 res += `\n ${actualLines[i - 3]}`; 270 printedLines++; 271 } else { 272 res += `\n${colors.blue}...${colors.white}`; 273 skipped = true; 274 } 275 } 276 res += `\n ${actualLines[i - 2]}`; 277 printedLines++; 278 } 279 res += `\n ${actualLines[i - 1]}`; 280 printedLines++; 281 } 282 // No identical lines before. 283 identical = 0; 284 // Add the actual line to the result and cache the expected diverging 285 // line so consecutive diverging lines show up as +++--- and not +-+-+-. 286 res += `\n${colors.green}+${colors.white} ${actualLine}`; 287 other += `\n${colors.red}-${colors.white} ${expectedLine}`; 288 printedLines += 2; 289 // Lines are identical 290 } else { 291 // Add all cached information to the result before adding other things 292 // and reset the cache. 293 res += other; 294 other = ''; 295 identical++; 296 // The very first identical line since the last diverging line is be 297 // added to the result. 298 if (identical <= 2) { 299 res += `\n ${actualLine}`; 300 printedLines++; 301 } 302 } 303 } 304 // Inspected object to big (Show ~50 rows max) 305 if (printedLines > 50 && i < maxLines - 2) { 306 return `${msg}${skippedMsg}\n${res}\n${colors.blue}...${colors.white}${other}\n` + 307 `${colors.blue}...${colors.white}`; 308 } 309 } 310 311 return `${msg}${skipped ? skippedMsg : ''}\n${res}${other}${end}${indicator}`; 312} 313 314function addEllipsis(string) { 315 const lines = StringPrototypeSplit(string, '\n', 11); 316 if (lines.length > 10) { 317 lines.length = 10; 318 return `${ArrayPrototypeJoin(lines, '\n')}\n...`; 319 } else if (string.length > 512) { 320 return `${StringPrototypeSlice(string, 512)}...`; 321 } 322 return string; 323} 324 325class AssertionError extends Error { 326 constructor(options) { 327 validateObject(options, 'options'); 328 const { 329 message, 330 operator, 331 stackStartFn, 332 details, 333 // Compatibility with older versions. 334 stackStartFunction, 335 } = options; 336 let { 337 actual, 338 expected, 339 } = options; 340 341 const limit = Error.stackTraceLimit; 342 if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = 0; 343 344 if (message != null) { 345 super(String(message)); 346 } else { 347 // Reset colors on each call to make sure we handle dynamically set environment 348 // variables correct. 349 colors.refresh(); 350 // Prevent the error stack from being visible by duplicating the error 351 // in a very close way to the original in case both sides are actually 352 // instances of Error. 353 if (typeof actual === 'object' && actual !== null && 354 typeof expected === 'object' && expected !== null && 355 'stack' in actual && actual instanceof Error && 356 'stack' in expected && expected instanceof Error) { 357 actual = copyError(actual); 358 expected = copyError(expected); 359 } 360 361 if (operator === 'deepStrictEqual' || operator === 'strictEqual') { 362 super(createErrDiff(actual, expected, operator)); 363 } else if (operator === 'notDeepStrictEqual' || 364 operator === 'notStrictEqual') { 365 // In case the objects are equal but the operator requires unequal, show 366 // the first object and say A equals B 367 let base = kReadableOperator[operator]; 368 const res = StringPrototypeSplit(inspectValue(actual), '\n'); 369 370 // In case "actual" is an object or a function, it should not be 371 // reference equal. 372 if (operator === 'notStrictEqual' && 373 ((typeof actual === 'object' && actual !== null) || 374 typeof actual === 'function')) { 375 base = kReadableOperator.notStrictEqualObject; 376 } 377 378 // Only remove lines in case it makes sense to collapse those. 379 // TODO: Accept env to always show the full error. 380 if (res.length > 50) { 381 res[46] = `${colors.blue}...${colors.white}`; 382 while (res.length > 47) { 383 ArrayPrototypePop(res); 384 } 385 } 386 387 // Only print a single input. 388 if (res.length === 1) { 389 super(`${base}${res[0].length > 5 ? '\n\n' : ' '}${res[0]}`); 390 } else { 391 super(`${base}\n\n${ArrayPrototypeJoin(res, '\n')}\n`); 392 } 393 } else { 394 let res = inspectValue(actual); 395 let other = inspectValue(expected); 396 const knownOperator = kReadableOperator[operator]; 397 if (operator === 'notDeepEqual' && res === other) { 398 res = `${knownOperator}\n\n${res}`; 399 if (res.length > 1024) { 400 res = `${StringPrototypeSlice(res, 0, 1021)}...`; 401 } 402 super(res); 403 } else { 404 if (res.length > 512) { 405 res = `${StringPrototypeSlice(res, 0, 509)}...`; 406 } 407 if (other.length > 512) { 408 other = `${StringPrototypeSlice(other, 0, 509)}...`; 409 } 410 if (operator === 'deepEqual') { 411 res = `${knownOperator}\n\n${res}\n\nshould loosely deep-equal\n\n`; 412 } else { 413 const newOp = kReadableOperator[`${operator}Unequal`]; 414 if (newOp) { 415 res = `${newOp}\n\n${res}\n\nshould not loosely deep-equal\n\n`; 416 } else { 417 other = ` ${operator} ${other}`; 418 } 419 } 420 super(`${res}${other}`); 421 } 422 } 423 } 424 425 if (isErrorStackTraceLimitWritable()) Error.stackTraceLimit = limit; 426 427 this.generatedMessage = !message; 428 ObjectDefineProperty(this, 'name', { 429 __proto__: null, 430 value: 'AssertionError [ERR_ASSERTION]', 431 enumerable: false, 432 writable: true, 433 configurable: true, 434 }); 435 this.code = 'ERR_ASSERTION'; 436 if (details) { 437 this.actual = undefined; 438 this.expected = undefined; 439 this.operator = undefined; 440 for (let i = 0; i < details.length; i++) { 441 this['message ' + i] = details[i].message; 442 this['actual ' + i] = details[i].actual; 443 this['expected ' + i] = details[i].expected; 444 this['operator ' + i] = details[i].operator; 445 this['stack trace ' + i] = details[i].stack; 446 } 447 } else { 448 this.actual = actual; 449 this.expected = expected; 450 this.operator = operator; 451 } 452 ErrorCaptureStackTrace(this, stackStartFn || stackStartFunction); 453 // Create error message including the error code in the name. 454 this.stack; // eslint-disable-line no-unused-expressions 455 // Reset the name. 456 this.name = 'AssertionError'; 457 } 458 459 toString() { 460 return `${this.name} [${this.code}]: ${this.message}`; 461 } 462 463 [inspect.custom](recurseTimes, ctx) { 464 // Long strings should not be fully inspected. 465 const tmpActual = this.actual; 466 const tmpExpected = this.expected; 467 468 if (typeof this.actual === 'string') { 469 this.actual = addEllipsis(this.actual); 470 } 471 if (typeof this.expected === 'string') { 472 this.expected = addEllipsis(this.expected); 473 } 474 475 // This limits the `actual` and `expected` property default inspection to 476 // the minimum depth. Otherwise those values would be too verbose compared 477 // to the actual error message which contains a combined view of these two 478 // input values. 479 const result = inspect(this, { 480 ...ctx, 481 customInspect: false, 482 depth: 0, 483 }); 484 485 // Reset the properties after inspection. 486 this.actual = tmpActual; 487 this.expected = tmpExpected; 488 489 return result; 490 } 491} 492 493module.exports = AssertionError; 494