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