• 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 { 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