Skip to content
File

Blob: src/node/internal/internal_assertionerror.ts

typescript549 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4//
5// Adapted from Deno and Node.js:
6// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
7// Adapted from Node.js. Copyright Joyent, Inc. and other Node contributors.
8// Permission is hereby granted, free of charge, to any person obtaining a
9// copy of this software and associated documentation files (the
10// "Software"), to deal in the Software without restriction, including
11// without limitation the rights to use, copy, modify, merge, publish,
12// distribute, sublicense, and/or sell copies of the Software, and to permit
13// persons to whom the Software is furnished to do so, subject to the
14// following conditions:
15// The above copyright notice and this permission notice shall be included
16// in all copies or substantial portions of the Software.
17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
18// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
20// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
21// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
22// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
23// USE OR OTHER DEALINGS IN THE SOFTWARE.
24 
25/* TODO: the following is adopted code, enabling linting one day */
26/* eslint-disable */
27 
28import { ERR_INVALID_ARG_TYPE } from 'node-internal:internal_errors';
29import { inspect } from 'node-internal:internal_inspect';
30 
31let blue = '';
32let green = '';
33let red = '';
34let defaultColor = '';
35 
36const kReadableOperator: { [key: string]: string } = {
37 deepStrictEqual: 'Expected values to be strictly deep-equal:',
38 strictEqual: 'Expected values to be strictly equal:',
39 strictEqualObject: 'Expected "actual" to be reference-equal to "expected":',
40 deepEqual: 'Expected values to be loosely deep-equal:',
41 notDeepStrictEqual: 'Expected "actual" not to be strictly deep-equal to:',
42 notStrictEqual: 'Expected "actual" to be strictly unequal to:',
43 notStrictEqualObject:
44 'Expected "actual" not to be reference-equal to "expected":',
45 notDeepEqual: 'Expected "actual" not to be loosely deep-equal to:',
46 notIdentical: 'Values have same structure but are not reference-equal:',
47 notDeepEqualUnequal: 'Expected values not to be loosely deep-equal:',
48};
49 
50// Comparing short primitives should just show === / !== instead of using the
51// diff.
52const kMaxShortLength = 12;
53 
54export function copyError(source: any): Error {
55 const keys = Object.keys(source);
56 const target = Object.create(Object.getPrototypeOf(source));
57 for (const key of keys) {
58 const desc = Object.getOwnPropertyDescriptor(source, key);
59 
60 if (desc !== undefined) {
61 Object.defineProperty(target, key, desc);
62 }
63 }
64 Object.defineProperty(target, 'message', { value: source.message });
65 return target;
66}
67 
68export function inspectValue(val: unknown): string {
69 return inspect(val, {
70 compact: true,
71 customInspect: false,
72 depth: 1000,
73 maxArrayLength: Infinity,
74 // Assert compares only enumerable properties (with a few exceptions).
75 showHidden: false,
76 // Assert does not detect proxies currently.
77 showProxy: false,
78 sorted: true,
79 // Inspect getters as we also check them when comparing entries.
80 getters: true,
81 });
82}
83 
84export function createErrDiff(
85 actual: unknown,
86 expected: unknown,
87 operator: string
88): string {
89 let other = '';
90 let res = '';
91 let end = '';
92 let skipped = false;
93 const actualInspected = inspectValue(actual);
94 const actualLines = actualInspected.split('\n');
95 const expectedLines = inspectValue(expected).split('\n');
96 
97 let i = 0;
98 let indicator = '';
99 
100 // In case both values are objects or functions explicitly mark them as not
101 // reference equal for the `strictEqual` operator.
102 if (
103 operator === 'strictEqual' &&
104 ((typeof actual === 'object' &&
105 actual !== null &&
106 typeof expected === 'object' &&
107 expected !== null) ||
108 (typeof actual === 'function' && typeof expected === 'function'))
109 ) {
110 operator = 'strictEqualObject';
111 }
112 
113 // If "actual" and "expected" fit on a single line and they are not strictly
114 // equal, check further special handling.
115 if (
116 actualLines.length === 1 &&
117 expectedLines.length === 1 &&
118 actualLines[0] !== expectedLines[0]
119 ) {
120 const actualRaw = actualLines[0];
121 const expectedRaw = expectedLines[0];
122 const inputLength =
123 (actualRaw as string).length + (expectedRaw as string).length;
124 // If the character length of "actual" and "expected" together is less than
125 // kMaxShortLength and if neither is an object and at least one of them is
126 // not `zero`, use the strict equal comparison to visualize the output.
127 if (inputLength <= kMaxShortLength) {
128 if (
129 (typeof actual !== 'object' || actual === null) &&
130 (typeof expected !== 'object' || expected === null) &&
131 (actual !== 0 || expected !== 0)
132 ) {
133 // -0 === +0
134 return (
135 `${kReadableOperator[operator]}\n\n` +
136 `${actualLines[0]} !== ${expectedLines[0]}\n`
137 );
138 }
139 } else if (operator !== 'strictEqualObject') {
140 // If the stderr is a tty and the input length is lower than the current
141 // columns per line, add a mismatch indicator below the output. If it is
142 // not a tty, use a default value of 80 characters.
143 const maxLength = 80;
144 if (inputLength < maxLength) {
145 while ((actualRaw as string)[i] === (expectedRaw as string)[i]) {
146 i++;
147 }
148 // Ignore the first characters.
149 if (i > 2) {
150 // Add position indicator for the first mismatch in case it is a
151 // single line and the input length is less than the column length.
152 indicator = `\n ${' '.repeat(i)}^`;
153 i = 0;
154 }
155 }
156 }
157 }
158 
159 // Remove all ending lines that match (this optimizes the output for
160 // readability by reducing the number of total changed lines).
161 let a = actualLines[actualLines.length - 1];
162 let b = expectedLines[expectedLines.length - 1];
163 while (a === b) {
164 if (i++ < 3) {
165 end = `\n ${a}${end}`;
166 } else {
167 other = a as string;
168 }
169 actualLines.pop();
170 expectedLines.pop();
171 if (actualLines.length === 0 || expectedLines.length === 0) {
172 break;
173 }
174 a = actualLines[actualLines.length - 1];
175 b = expectedLines[expectedLines.length - 1];
176 }
177 
178 const maxLines = Math.max(actualLines.length, expectedLines.length);
179 // Strict equal with identical objects that are not identical by reference.
180 // E.g., assert.deepStrictEqual({ a: Symbol() }, { a: Symbol() })
181 if (maxLines === 0) {
182 // We have to get the result again. The lines were all removed before.
183 const actualLines = actualInspected.split('\n');
184 
185 // Only remove lines in case it makes sense to collapse those.
186 if (actualLines.length > 50) {
187 actualLines[46] = `${blue}...${defaultColor}`;
188 while (actualLines.length > 47) {
189 actualLines.pop();
190 }
191 }
192 
193 return `${kReadableOperator['notIdentical']}\n\n${actualLines.join('\n')}\n`;
194 }
195 
196 // There were at least five identical lines at the end. Mark a couple of
197 // skipped.
198 if (i >= 5) {
199 end = `\n${blue}...${defaultColor}${end}`;
200 skipped = true;
201 }
202 if (other !== '') {
203 end = `\n ${other}${end}`;
204 other = '';
205 }
206 
207 let printedLines = 0;
208 let identical = 0;
209 const msg =
210 kReadableOperator[operator] +
211 `\n${green}+ actual${defaultColor} ${red}- expected${defaultColor}`;
212 const skippedMsg = ` ${blue}...${defaultColor} Lines skipped`;
213 
214 let lines = actualLines;
215 let plusMinus = `${green}+${defaultColor}`;
216 let maxLength = expectedLines.length;
217 if (actualLines.length < maxLines) {
218 lines = expectedLines;
219 plusMinus = `${red}-${defaultColor}`;
220 maxLength = actualLines.length;
221 }
222 
223 for (i = 0; i < maxLines; i++) {
224 if (maxLength < i + 1) {
225 // If more than two former lines are identical, print them. Collapse them
226 // in case more than five lines were identical.
227 if (identical > 2) {
228 if (identical > 3) {
229 if (identical > 4) {
230 if (identical === 5) {
231 res += `\n ${lines[i - 3]}`;
232 printedLines++;
233 } else {
234 res += `\n${blue}...${defaultColor}`;
235 skipped = true;
236 }
237 }
238 res += `\n ${lines[i - 2]}`;
239 printedLines++;
240 }
241 res += `\n ${lines[i - 1]}`;
242 printedLines++;
243 }
244 // No identical lines before.
245 identical = 0;
246 // Add the expected line to the cache.
247 if (lines === actualLines) {
248 res += `\n${plusMinus} ${lines[i]}`;
249 } else {
250 other += `\n${plusMinus} ${lines[i]}`;
251 }
252 printedLines++;
253 // Only extra actual lines exist
254 // Lines diverge
255 } else {
256 const expectedLine = expectedLines[i];
257 let actualLine = actualLines[i];
258 // If the lines diverge, specifically check for lines that only diverge by
259 // a trailing comma. In that case it is actually identical and we should
260 // mark it as such.
261 let divergingLines =
262 actualLine !== expectedLine &&
263 (!(actualLine as string).endsWith(',') ||
264 (actualLine as string).slice(0, -1) !== expectedLine);
265 // If the expected line has a trailing comma but is otherwise identical,
266 // add a comma at the end of the actual line. Otherwise the output could
267 // look weird as in:
268 //
269 // [
270 // 1 // No comma at the end!
271 // + 2
272 // ]
273 //
274 if (
275 divergingLines &&
276 (expectedLine as string).endsWith(',') &&
277 (expectedLine as string).slice(0, -1) === actualLine
278 ) {
279 divergingLines = false;
280 actualLine += ',';
281 }
282 if (divergingLines) {
283 // If more than two former lines are identical, print them. Collapse
284 // them in case more than five lines were identical.
285 if (identical > 2) {
286 if (identical > 3) {
287 if (identical > 4) {
288 if (identical === 5) {
289 res += `\n ${actualLines[i - 3]}`;
290 printedLines++;
291 } else {
292 res += `\n${blue}...${defaultColor}`;
293 skipped = true;
294 }
295 }
296 res += `\n ${actualLines[i - 2]}`;
297 printedLines++;
298 }
299 res += `\n ${actualLines[i - 1]}`;
300 printedLines++;
301 }
302 // No identical lines before.
303 identical = 0;
304 // Add the actual line to the result and cache the expected diverging
305 // line so consecutive diverging lines show up as +++--- and not +-+-+-.
306 res += `\n${green}+${defaultColor} ${actualLine}`;
307 other += `\n${red}-${defaultColor} ${expectedLine}`;
308 printedLines += 2;
309 // Lines are identical
310 } else {
311 // Add all cached information to the result before adding other things
312 // and reset the cache.
313 res += other;
314 other = '';
315 identical++;
316 // The very first identical line since the last diverging line is be
317 // added to the result.
318 if (identical <= 2) {
319 res += `\n ${actualLine}`;
320 printedLines++;
321 }
322 }
323 }
324 // Inspected object to big (Show ~50 rows max)
325 if (printedLines > 50 && i < maxLines - 2) {
326 return (
327 `${msg}${skippedMsg}\n${res}\n${blue}...${defaultColor}${other}\n` +
328 `${blue}...${defaultColor}`
329 );
330 }
331 }
332 
333 return `${msg}${skipped ? skippedMsg : ''}\n${res}${other}${end}${indicator}`;
334}
335 
336export interface AssertionErrorDetailsDescriptor {
337 message: string;
338 actual: unknown;
339 expected: unknown;
340 operator: string;
341 stack: Error;
342}
343 
344export interface AssertionErrorConstructorOptions {
345 message: string | Error | undefined;
346 actual?: unknown;
347 expected?: unknown;
348 operator?: string;
349 details?: AssertionErrorDetailsDescriptor[];
350 // deno-lint-ignore ban-types
351 stackStartFn?: Function;
352 // Compatibility with older versions.
353 // deno-lint-ignore ban-types
354 stackStartFunction?: Function;
355}
356 
357interface ErrorWithStackTraceLimit extends ErrorConstructor {
358 stackTraceLimit: number;
359}
360 
361export class AssertionError extends Error {
362 [key: string]: unknown;
363 
364 // deno-lint-ignore constructor-super
365 constructor(options: AssertionErrorConstructorOptions) {
366 if (typeof options !== 'object' || options === null) {
367 throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
368 }
369 const {
370 message,
371 operator,
372 stackStartFn,
373 details,
374 // Compatibility with older versions.
375 stackStartFunction,
376 } = options;
377 let { actual, expected } = options;
378 
379 // TODO(schwarzkopfb): `stackTraceLimit` should be added to `ErrorConstructor` in
380 // cli/dts/lib.deno.shared_globals.d.ts
381 const limit = (Error as ErrorWithStackTraceLimit).stackTraceLimit;
382 (Error as ErrorWithStackTraceLimit).stackTraceLimit = 0;
383 
384 if (message != null) {
385 super(String(message));
386 } else {
387 // Prevent the error stack from being visible by duplicating the error
388 // in a very close way to the original in case both sides are actually
389 // instances of Error.
390 if (
391 typeof actual === 'object' &&
392 actual !== null &&
393 typeof expected === 'object' &&
394 expected !== null &&
395 'stack' in actual &&
396 actual instanceof Error &&
397 'stack' in expected &&
398 expected instanceof Error
399 ) {
400 actual = copyError(actual);
401 expected = copyError(expected);
402 }
403 
404 if (operator === 'deepStrictEqual' || operator === 'strictEqual') {
405 super(createErrDiff(actual, expected, operator));
406 } else if (
407 operator === 'notDeepStrictEqual' ||
408 operator === 'notStrictEqual'
409 ) {
410 // In case the objects are equal but the operator requires unequal, show
411 // the first object and say A equals B
412 let base = kReadableOperator[operator];
413 const res = inspectValue(actual).split('\n');
414 
415 // In case "actual" is an object or a function, it should not be
416 // reference equal.
417 if (
418 operator === 'notStrictEqual' &&
419 ((typeof actual === 'object' && actual !== null) ||
420 typeof actual === 'function')
421 ) {
422 base = kReadableOperator['notStrictEqualObject'];
423 }
424 
425 // Only remove lines in case it makes sense to collapse those.
426 if (res.length > 50) {
427 res[46] = `${blue}...${defaultColor}`;
428 while (res.length > 47) {
429 res.pop();
430 }
431 }
432 
433 // Only print a single input.
434 if (res.length === 1) {
435 super(`${base}${res[0]!.length > 5 ? '\n\n' : ' '}${res[0]}`);
436 } else {
437 super(`${base}\n\n${res.join('\n')}\n`);
438 }
439 } else {
440 let res = inspectValue(actual);
441 let other = inspectValue(expected);
442 const knownOperator = kReadableOperator[operator ?? ''];
443 if (operator === 'notDeepEqual' && res === other) {
444 res = `${knownOperator}\n\n${res}`;
445 if (res.length > 1024) {
446 res = `${res.slice(0, 1021)}...`;
447 }
448 super(res);
449 } else {
450 if (res.length > 512) {
451 res = `${res.slice(0, 509)}...`;
452 }
453 if (other.length > 512) {
454 other = `${other.slice(0, 509)}...`;
455 }
456 if (operator === 'deepEqual') {
457 res = `${knownOperator}\n\n${res}\n\nshould loosely deep-equal\n\n`;
458 } else {
459 const newOp = kReadableOperator[`${operator}Unequal`];
460 if (newOp) {
461 res = `${newOp}\n\n${res}\n\nshould not loosely deep-equal\n\n`;
462 } else {
463 other = ` ${operator} ${other}`;
464 }
465 }
466 super(`${res}${other}`);
467 }
468 }
469 }
470 
471 (Error as ErrorWithStackTraceLimit).stackTraceLimit = limit;
472 
473 (this as any).generatedMessage = !message;
474 Object.defineProperty(this, 'name', {
475 __proto__: null,
476 value: 'AssertionError [ERR_ASSERTION]',
477 enumerable: false,
478 writable: true,
479 configurable: true,
480 // deno-lint-ignore no-explicit-any
481 } as any);
482 (this as any).code = 'ERR_ASSERTION';
483 
484 if (details) {
485 (this as any).actual = undefined;
486 (this as any).expected = undefined;
487 (this as any).operator = undefined;
488 
489 for (let i = 0; i < details.length; i++) {
490 this['message ' + i] = (details[i] as any).message;
491 this['actual ' + i] = (details[i] as any).actual;
492 this['expected ' + i] = (details[i] as any).expected;
493 this['operator ' + i] = (details[i] as any).operator;
494 this['stack trace ' + i] = (details[i] as any).stack;
495 }
496 } else {
497 (this as any).actual = actual;
498 (this as any).expected = expected;
499 (this as any).operator = operator;
500 }
501 
502 // @ts-ignore this function is not available in lib.dom.d.ts
503 Error.captureStackTrace(this, stackStartFn || stackStartFunction);
504 // Create error message including the error code in the name.
505 this.stack;
506 // Reset the name.
507 this.name = 'AssertionError';
508 }
509 
510 override toString() {
511 return `${this.name} [${(this as any).code}]: ${this.message}`;
512 }
513 
514 [inspect.custom](_recurseTimes: number, ctx: Record<string, unknown>) {
515 // Long strings should not be fully inspected.
516 const tmpActual = (this as any).actual;
517 const tmpExpected = (this as any).expected;
518 
519 for (const name of ['actual', 'expected']) {
520 if (typeof this[name] === 'string') {
521 const value = this[name] as string;
522 const lines = value.split('\n');
523 if (lines.length > 10) {
524 lines.length = 10;
525 this[name] = `${lines.join('\n')}\n...`;
526 } else if (value.length > 512) {
527 this[name] = `${value.slice(512)}...`;
528 }
529 }
530 }
531 
532 // This limits the `actual` and `expected` property default inspection to
533 // the minimum depth. Otherwise those values would be too verbose compared
534 // to the actual error message which contains a combined view of these two
535 // input values.
536 const result = inspect(this, {
537 ...ctx,
538 customInspect: false,
539 depth: 0,
540 });
541 
542 // Reset the properties after inspection.
543 (this as any).actual = tmpActual;
544 (this as any).expected = tmpExpected;
545 
546 return result;
547 }
548}