[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix(pretty-format): limit output for large object (#9949)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

authored by

Hiroshi Ogawa
Claude Opus 4.6 (1M context)
and committed by
GitHub
(Mar 23, 2026, 11:24 AM +0100) 0d5f9d6e 7b605432

+231 -17
+3
pnpm-lock.yaml
··· 1464 1464 '@vitest/mocker': 1465 1465 specifier: workspace:* 1466 1466 version: link:../../packages/mocker 1467 + '@vitest/pretty-format': 1468 + specifier: workspace:* 1469 + version: link:../../packages/pretty-format 1467 1470 '@vitest/runner': 1468 1471 specifier: workspace:* 1469 1472 version: link:../../packages/runner
+1
test/core/package.json
··· 22 22 "@test/vite-external": "link:./projects/vite-external", 23 23 "@vitest/expect": "workspace:*", 24 24 "@vitest/mocker": "workspace:*", 25 + "@vitest/pretty-format": "workspace:*", 25 26 "@vitest/runner": "workspace:*", 26 27 "@vitest/test-dep-cjs": "file:./deps/dep-cjs", 27 28 "@vitest/test-dep-nested-cjs": "file:./deps/dep-nested-cjs",
+37 -17
packages/pretty-format/src/index.ts
··· 380 380 refs: Refs, 381 381 hasCalledToJSON?: boolean, 382 382 ): string { 383 + let result: string 384 + 383 385 const plugin = findPlugin(config.plugins, val) 384 386 if (plugin !== null) { 385 - return printPlugin(plugin, val, config, indentation, depth, refs) 387 + result = printPlugin(plugin, val, config, indentation, depth, refs) 388 + } 389 + else { 390 + const basicResult = printBasicValue( 391 + val, 392 + config.printFunctionName, 393 + config.escapeRegex, 394 + config.escapeString, 395 + ) 396 + if (basicResult !== null) { 397 + result = basicResult 398 + } 399 + else { 400 + result = printComplexValue( 401 + val, 402 + config, 403 + indentation, 404 + depth, 405 + refs, 406 + hasCalledToJSON, 407 + ) 408 + } 386 409 } 387 410 388 - const basicResult = printBasicValue( 389 - val, 390 - config.printFunctionName, 391 - config.escapeRegex, 392 - config.escapeString, 393 - ) 394 - if (basicResult !== null) { 395 - return basicResult 411 + // Check string length budget: 412 + // accumulate output length and if exceeded, 413 + // force no further recursion by patching maxDepth. 414 + // Inspired by Node's util.inspect bail out approach. 415 + config.outputLength += result.length 416 + if (config.outputLength > config.maxOutputLength) { 417 + config.maxDepth = 0 396 418 } 397 419 398 - return printComplexValue( 399 - val, 400 - config, 401 - indentation, 402 - depth, 403 - refs, 404 - hasCalledToJSON, 405 - ) 420 + return result 406 421 } 407 422 408 423 const DEFAULT_THEME: Theme = { ··· 425 440 highlight: false, 426 441 indent: 2, 427 442 maxDepth: Number.POSITIVE_INFINITY, 443 + // Practical default hard-limit to avoid too long string being generated 444 + // (Node's limit is buffer.constants.MAX_STRING_LENGTH ~ 512MB) 445 + maxOutputLength: 1_000_000, 428 446 maxWidth: Number.POSITIVE_INFINITY, 429 447 min: false, 430 448 plugins: [], ··· 509 527 printShadowRoot: options?.printShadowRoot ?? true, 510 528 spacingInner: options?.min ? ' ' : '\n', 511 529 spacingOuter: options?.min ? '' : '\n', 530 + maxOutputLength: options?.maxOutputLength ?? DEFAULT_OPTIONS.maxOutputLength, 531 + outputLength: 0, 512 532 } 513 533 } 514 534
+3
packages/pretty-format/src/types.ts
··· 45 45 indent?: number 46 46 maxDepth?: number 47 47 maxWidth?: number 48 + maxOutputLength?: number 48 49 min?: boolean 49 50 printBasicPrototype?: boolean 50 51 printFunctionName?: boolean ··· 71 72 printShadowRoot: boolean 72 73 spacingInner: string 73 74 spacingOuter: string 75 + maxOutputLength: number 76 + outputLength: number 74 77 } 75 78 76 79 export type Printer = (
+187
test/core/test/pretty-format.test.ts
··· 1 + import { format } from '@vitest/pretty-format' 2 + import { describe, expect, test } from 'vitest' 3 + 4 + describe('maxOutputLength', () => { 5 + function createObjectGraph(n: number) { 6 + // owner 7 + // |-> cats 8 + // |-> cat0 -> owner 9 + // |-> cat1 -> owner 10 + // |-> cat2 11 + // |-> ... 12 + // |-> dogs 13 + // |-> dog0 14 + // |-> dog1 15 + // |-> dog2 16 + // |-> ... 17 + interface Owner { 18 + dogs: Pet[] 19 + cats: Pet[] 20 + } 21 + interface Pet { 22 + name: string 23 + owner: Owner 24 + } 25 + const owner: Owner = { dogs: [], cats: [] } 26 + for (let i = 0; i < n; i++) { 27 + owner.dogs.push({ name: `dog${i}`, owner }) 28 + } 29 + for (let i = 0; i < n; i++) { 30 + owner.cats.push({ name: `cat${i}`, owner }) 31 + } 32 + return owner 33 + } 34 + 35 + test('quadratic growth example depending on format root', () => { 36 + const owner = createObjectGraph(3) 37 + 38 + // when starting from owner, each pet is expanded once, so no amplification, just linear growth. 39 + expect(format(owner)).toMatchInlineSnapshot(` 40 + "Object { 41 + "cats": Array [ 42 + Object { 43 + "name": "cat0", 44 + "owner": [Circular], 45 + }, 46 + Object { 47 + "name": "cat1", 48 + "owner": [Circular], 49 + }, 50 + Object { 51 + "name": "cat2", 52 + "owner": [Circular], 53 + }, 54 + ], 55 + "dogs": Array [ 56 + Object { 57 + "name": "dog0", 58 + "owner": [Circular], 59 + }, 60 + Object { 61 + "name": "dog1", 62 + "owner": [Circular], 63 + }, 64 + Object { 65 + "name": "dog2", 66 + "owner": [Circular], 67 + }, 68 + ], 69 + }" 70 + `) 71 + 72 + // when starting from owner.cats, each cat re-expands the full dogs list via owner. 73 + // this exhibits quadratic growth, which is what the budget is designed to prevent. 74 + expect(format(owner.cats)).toMatchInlineSnapshot(` 75 + "Array [ 76 + Object { 77 + "name": "cat0", 78 + "owner": Object { 79 + "cats": [Circular], 80 + "dogs": Array [ 81 + Object { 82 + "name": "dog0", 83 + "owner": [Circular], 84 + }, 85 + Object { 86 + "name": "dog1", 87 + "owner": [Circular], 88 + }, 89 + Object { 90 + "name": "dog2", 91 + "owner": [Circular], 92 + }, 93 + ], 94 + }, 95 + }, 96 + Object { 97 + "name": "cat1", 98 + "owner": Object { 99 + "cats": [Circular], 100 + "dogs": Array [ 101 + Object { 102 + "name": "dog0", 103 + "owner": [Circular], 104 + }, 105 + Object { 106 + "name": "dog1", 107 + "owner": [Circular], 108 + }, 109 + Object { 110 + "name": "dog2", 111 + "owner": [Circular], 112 + }, 113 + ], 114 + }, 115 + }, 116 + Object { 117 + "name": "cat2", 118 + "owner": Object { 119 + "cats": [Circular], 120 + "dogs": Array [ 121 + Object { 122 + "name": "dog0", 123 + "owner": [Circular], 124 + }, 125 + Object { 126 + "name": "dog1", 127 + "owner": [Circular], 128 + }, 129 + Object { 130 + "name": "dog2", 131 + "owner": [Circular], 132 + }, 133 + ], 134 + }, 135 + }, 136 + ]" 137 + `) 138 + }) 139 + 140 + test('budget prevents blowup on large graphs', () => { 141 + // quickly hit the kill switch due to quadratic growth 142 + expect([10, 20, 30, 1000, 2000, 3000].map(n => format(createObjectGraph(n).cats).length)) 143 + .toMatchInlineSnapshot(` 144 + [ 145 + 9729, 146 + 36659, 147 + 80789, 148 + 273009, 149 + 374009, 150 + 299009, 151 + ] 152 + `) 153 + 154 + // depending on object/array shape, output can exceed the limit 1mb 155 + // but the output size is proportional to the amount of objects and the size of array. 156 + expect(format(createObjectGraph(10000).cats).length).toMatchInlineSnapshot(`936779`) 157 + expect(format(createObjectGraph(20000).cats).length).toMatchInlineSnapshot(`1236779`) 158 + }) 159 + 160 + test('early elements expanded, later elements folded after budget trips', () => { 161 + // First few objects are fully expanded, but once budget is exceeded, 162 + // maxDepth = 0 means no more expansion. 163 + const arr = Array.from({ length: 10 }, (_, i) => ({ i })) 164 + expect(format(arr, { maxOutputLength: 100 })).toMatchInlineSnapshot(` 165 + "Array [ 166 + Object { 167 + "i": 0, 168 + }, 169 + Object { 170 + "i": 1, 171 + }, 172 + Object { 173 + "i": 2, 174 + }, 175 + Object { 176 + "i": 3, 177 + }, 178 + [Object], 179 + [Object], 180 + [Object], 181 + [Object], 182 + [Object], 183 + [Object], 184 + ]" 185 + `) 186 + }) 187 + })