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