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