• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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