Skip to content

Commit 7ae1417

Browse files
sheremet-vadammy001
andauthoredDec 20, 2022
feat: add more options to configure diff output (#2522)
* feat: add more options to configure diff output * chore: add more tests for diff, cleanup * docs: add `-` examples * chore: cleanup * chore: fix test and wrong "could not display diff" * test: diff ignores undefined * Apply suggestions from code review Co-authored-by: Anjorin Damilare <damilareanjorin1@gmail.com> * docs: add `--logHeapUsage` to docs Co-authored-by: Anjorin Damilare <damilareanjorin1@gmail.com>
1 parent ecad79a commit 7ae1417

File tree

10 files changed

+201
-29
lines changed

10 files changed

+201
-29
lines changed
 

‎docs/config/index.md

+42-4
Original file line numberDiff line numberDiff line change
@@ -347,17 +347,55 @@ Custom reporters for output. Reporters can be [a Reporter instance](https://gith
347347
### outputTruncateLength
348348

349349
- **Type:** `number`
350-
- **Default:** `80`
350+
- **Default:** `stdout.columns || 80`
351+
- **CLI:** `--outputTruncateLength <length>`, `--output-truncate-length <length>`
351352

352-
Truncate output diff lines up to `80` number of characters. You may wish to tune this,
353-
depending on your terminal window width.
353+
Truncate the size of diff line up to `stdout.columns` or `80` number of characters. You may wish to tune this, depending on your terminal window width. Vitest includes `+-` characters and spaces for this. For example, you might see this diff, if you set this to `6`:
354+
355+
```diff
356+
// actual line: "Text that seems correct"
357+
- Text...
358+
+ Test...
359+
```
354360

355361
### outputDiffLines
356362

357363
- **Type:** `number`
358364
- **Default:** `15`
365+
- **CLI:** `--outputDiffLines <lines>`, `--output-diff-lines <lines>`
366+
367+
Limit the number of single output diff lines up to `15`. Vitest counts all `+-` lines when determining when to stop. For example, you might see diff like this, if you set this property to `3`:
368+
369+
```diff
370+
- test: 1,
371+
+ test: 2,
372+
- obj: '1',
373+
...
374+
- test2: 1,
375+
+ test2: 1,
376+
- obj2: '2',
377+
...
378+
```
379+
380+
### outputDiffMaxLines
381+
382+
- **Type:** `number`
383+
- **Default:** `50`
384+
- **CLI:** `--outputDiffMaxLines <lines>`, `--output-diff-max-lines <lines>`
385+
- **Version:** Since Vitest 0.26.0
386+
387+
The maximum number of lines to display in diff window. Beware that if you have a large object with many small diffs, you might not see all of them at once.
388+
389+
### outputDiffMaxSize
390+
391+
- **Type:** `number`
392+
- **Default:** `10000`
393+
- **CLI:** `--outputDiffMaxSize <length>`, `--output-diff-max-size <length>`
394+
- **Version:** Since Vitest 0.26.0
395+
396+
The maximum length of the stringified object before the diff happens. Vitest tries to stringify an object before doing a diff, but if the object is too large, it will reduce the depth of the object to fit within this limit. Because of this, if the object is too big or nested, you might not see the diff.
359397

360-
Limit number of output diff lines up to `15`.
398+
Increasing this limit can increase the duration of diffing.
361399

362400
### outputFile
363401

‎docs/guide/cli.md

+3
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ vitest related /src/index.ts /src/hello-world.js
5858
| `--silent` | Silent console output from tests |
5959
| `--isolate` | Isolate environment for each test file (default: `true`) |
6060
| `--reporter <name>` | Select reporter: `default`, `verbose`, `dot`, `junit`, `json`, or a path to a custom reporter |
61+
| `--outputDiffMaxSize <length>` | Object diff output max size (default: 10000) |
62+
| `--outputDiffMaxLines <lines>` | Max lines in diff output window (default: 50) |
6163
| `--outputTruncateLength <length>` | Truncate output diff lines up to `<length>` number of characters. |
6264
| `--outputDiffLines <lines>` | Limit number of output diff lines up to `<lines>`. |
6365
| `--outputFile <filename/-s>` | Write test results to a file when the `--reporter=json` or `--reporter=junit` option is also specified <br /> Via [cac's dot notation] you can specify individual outputs for multiple reporters |
@@ -70,6 +72,7 @@ vitest related /src/index.ts /src/hello-world.js
7072
| `--browser` | Run tests in browser |
7173
| `--environment <env>` | Runner environment (default: `node`) |
7274
| `--passWithNoTests` | Pass when no tests found |
75+
| `--logHeapUsage` | Show the size of heap for each test |
7376
| `--allowOnly` | Allow tests and suites that are marked as `only` (default: false in CI, true otherwise) |
7477
| `--dangerouslyIgnoreUnhandledErrors` | Ignore any unhandled errors that occur |
7578
| `--changed [since]` | Run tests that are affected by the changed files (default: false). See [docs](#changed) |

‎packages/vitest/src/integrations/chai/jest-matcher-utils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ const SPACE_SYMBOL = '\u{00B7}' // middle dot
102102
const replaceTrailingSpaces = (text: string): string =>
103103
text.replace(/\s+$/gm, spaces => SPACE_SYMBOL.repeat(spaces.length))
104104

105-
export function stringify(object: unknown, maxDepth = 10, options?: PrettyFormatOptions): string {
106-
const MAX_LENGTH = 10000
105+
export function stringify(object: unknown, maxDepth = 10, { maxLength, ...options }: PrettyFormatOptions & { maxLength?: number } = {}): string {
106+
const MAX_LENGTH = maxLength ?? 10000
107107
let result
108108

109109
try {

‎packages/vitest/src/node/cli.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ cli
2424
.option('--silent', 'silent console output from tests')
2525
.option('--isolate', 'isolate environment for each test file (default: true)')
2626
.option('--reporter <name>', 'reporter')
27-
.option('--outputTruncateLength <length>', 'diff output length (default: 80)')
28-
.option('--outputDiffLines <lines>', 'number of diff output lines (default: 15)')
27+
.option('--outputDiffMaxSize <length>', 'object diff output max size (default: 10000)')
28+
.option('--outputDiffMaxLines <length>', 'max lines in diff output window (default: 50)')
29+
.option('--outputTruncateLength <length>', 'diff output line length (default: 80)')
30+
.option('--outputDiffLines <lines>', 'number of lines in single diff (default: 15)')
2931
.option('--outputFile <filename/-s>', 'write test results to a file when the --reporter=json or --reporter=junit option is also specified, use cac\'s dot notation for individual outputs of multiple reporters')
3032
.option('--coverage', 'enable coverage report')
3133
.option('--run', 'do not watch')
@@ -35,6 +37,7 @@ cli
3537
.option('--browser', 'run tests in browser')
3638
.option('--environment <env>', 'runner environment (default: node)')
3739
.option('--passWithNoTests', 'pass when no tests found')
40+
.option('--logHeapUsage', 'show the size of heap for each test')
3841
.option('--allowOnly', 'Allow tests and suites that are marked as only (default: !process.env.CI)')
3942
.option('--dangerouslyIgnoreUnhandledErrors', 'Ignore any unhandled errors that occur')
4043
.option('--shard <shard>', 'Test suite shard to execute in a format of <index>/<count>')

‎packages/vitest/src/node/error.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,14 @@ function handleImportOutsideModuleError(stack: string, ctx: Vitest) {
160160
}\n`)))
161161
}
162162

163-
function displayDiff(actual: string, expected: string, console: Console, options?: Omit<DiffOptions, 'showLegend'>) {
164-
console.error(c.gray(unifiedDiff(actual, expected, options)) + '\n')
163+
export function displayDiff(actual: string, expected: string, console: Console, options: Omit<DiffOptions, 'showLegend'> = {}) {
164+
const diff = unifiedDiff(actual, expected, options)
165+
const dim = options.noColor ? (s: string) => s : c.dim
166+
const black = options.noColor ? (s: string) => s : c.black
167+
if (diff)
168+
console.error(diff + '\n')
169+
else if (actual && expected && actual !== '"undefined"' && expected !== '"undefined"')
170+
console.error(dim('Could not display diff. It\'s possible objects are too large to compare.\nTry increasing ') + black('--outputDiffMaxSize') + dim(' option.\n'))
165171
}
166172

167173
function printErrorMessage(error: ErrorWithDiff, logger: Logger) {

‎packages/vitest/src/runtime/error.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import util from 'util'
22
import { util as ChaiUtil } from 'chai'
33
import { stringify } from '../integrations/chai/jest-matcher-utils'
4-
import { deepClone, getType } from '../utils'
4+
import { deepClone, getType, getWorkerState } from '../utils'
55

66
const IS_RECORD_SYMBOL = '@@__IMMUTABLE_RECORD__@@'
77
const IS_COLLECTION_SYMBOL = '@@__IMMUTABLE_ITERABLE__@@'
@@ -102,10 +102,13 @@ export function processError(err: any) {
102102
err.actual = replacedActual
103103
err.expected = replacedExpected
104104

105+
const workerState = getWorkerState()
106+
const maxDiffSize = workerState.config.outputDiffMaxSize
107+
105108
if (typeof err.expected !== 'string')
106-
err.expected = stringify(err.expected)
109+
err.expected = stringify(err.expected, 10, { maxLength: maxDiffSize })
107110
if (typeof err.actual !== 'string')
108-
err.actual = stringify(err.actual)
111+
err.actual = stringify(err.actual, 10, { maxLength: maxDiffSize })
109112

110113
// some Error implementations don't allow rewriting message
111114
try {

‎packages/vitest/src/typecheck/parse.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export async function getTsconfigPath(root: string, config: TypecheckConfig) {
7272
try {
7373
const tmpTsConfig: Record<string, any> = { ...tsconfig.config }
7474

75-
tmpTsConfig.compilerOptions ??= {}
75+
tmpTsConfig.compilerOptions = tmpTsConfig.compilerOptions || {}
7676
tmpTsConfig.compilerOptions.emitDeclarationOnly = false
7777
tmpTsConfig.compilerOptions.incremental = true
7878
tmpTsConfig.compilerOptions.tsBuildInfoFile = path.join(

‎packages/vitest/src/types/config.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -156,20 +156,37 @@ export interface InlineConfig {
156156

157157
/**
158158
* Custom reporter for output. Can contain one or more built-in report names, reporter instances,
159-
* and/or paths to custom reporters
159+
* and/or paths to custom reporters.
160160
*/
161161
reporters?: Arrayable<BuiltinReporters | 'html' | Reporter | Omit<string, BuiltinReporters>>
162162

163163
/**
164-
* diff output length
164+
* Truncates lines in the output to the given length.
165+
* @default stdout.columns || 80
165166
*/
166167
outputTruncateLength?: number
167168

168169
/**
169-
* number of diff output lines
170+
* Maximum number of line to show in a single diff.
171+
* @default 15
170172
*/
171173
outputDiffLines?: number
172174

175+
/**
176+
* The maximum number of characters allowed in a single object before doing a diff.
177+
* Vitest tries to stringify an object before doing a diff, but if the object is too large,
178+
* it will reduce the depth of the object to fit within this limit.
179+
* Because of this if object is too big or nested, you might not see the diff.
180+
* @default 10000
181+
*/
182+
outputDiffMaxSize?: number
183+
184+
/**
185+
* Maximum number of lines in a diff overall.
186+
* @default 50
187+
*/
188+
outputDiffMaxLines?: number
189+
173190
/**
174191
* Write test results to a file when the --reporter=json` or `--reporter=junit` option is also specified.
175192
* Also definable individually per reporter by using an object instead.

‎packages/vitest/src/utils/diff.ts

+33-12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export function formatLine(line: string, outputTruncateLength?: number) {
77
}
88

99
export interface DiffOptions {
10+
noColor?: boolean
11+
outputDiffMaxLines?: number
1012
outputTruncateLength?: number
1113
outputDiffLines?: number
1214
showLegend?: boolean
@@ -25,17 +27,23 @@ export function unifiedDiff(actual: string, expected: string, options: DiffOptio
2527
if (actual === expected)
2628
return ''
2729

28-
const { outputTruncateLength, outputDiffLines, showLegend = true } = options
30+
const { outputTruncateLength, outputDiffLines, outputDiffMaxLines, noColor, showLegend = true } = options
2931

3032
const indent = ' '
3133
const diffLimit = outputDiffLines || 15
34+
const diffMaxLines = outputDiffMaxLines || 50
3235

3336
const counts = {
3437
'+': 0,
3538
'-': 0,
3639
}
3740
let previousState: '-' | '+' | null = null
3841
let previousCount = 0
42+
43+
const str = (str: string) => str
44+
const dim = noColor ? str : c.dim
45+
const green = noColor ? str : c.green
46+
const red = noColor ? str : c.red
3947
function preprocess(line: string) {
4048
if (!line || line.match(/\\ No newline/))
4149
return
@@ -49,42 +57,55 @@ export function unifiedDiff(actual: string, expected: string, options: DiffOptio
4957
previousCount++
5058
counts[char]++
5159
if (previousCount === diffLimit)
52-
return c.dim(`${char} ...`)
60+
return dim(`${char} ...`)
5361
else if (previousCount > diffLimit)
5462
return
5563
}
5664
return line
5765
}
5866

5967
const msg = diff.createPatch('string', expected, actual)
60-
const lines = msg.split('\n').slice(5).map(preprocess).filter(Boolean) as string[]
68+
let lines = msg.split('\n').slice(5).map(preprocess).filter(Boolean) as string[]
69+
let moreLines = 0
6170
const isCompact = counts['+'] === 1 && counts['-'] === 1 && lines.length === 2
6271

72+
if (lines.length > diffMaxLines) {
73+
const firstDiff = lines.findIndex(line => line[0] === '-' || line[0] === '+')
74+
const displayLines = lines.slice(firstDiff - 2, diffMaxLines)
75+
const lastDisplayedIndex = firstDiff - 2 + diffMaxLines
76+
if (lastDisplayedIndex < lines.length)
77+
moreLines = lines.length - lastDisplayedIndex
78+
lines = displayLines
79+
}
80+
6381
let formatted = lines.map((line: string) => {
6482
line = line.replace(/\\"/g, '"')
6583
if (line[0] === '-') {
6684
line = formatLine(line.slice(1), outputTruncateLength)
6785
if (isCompact)
68-
return c.green(line)
69-
return c.green(`- ${formatLine(line, outputTruncateLength)}`)
86+
return green(line)
87+
return green(`- ${formatLine(line, outputTruncateLength)}`)
7088
}
7189
if (line[0] === '+') {
7290
line = formatLine(line.slice(1), outputTruncateLength)
7391
if (isCompact)
74-
return c.red(line)
75-
return c.red(`+ ${formatLine(line, outputTruncateLength)}`)
92+
return red(line)
93+
return red(`+ ${formatLine(line, outputTruncateLength)}`)
7694
}
7795
if (line.match(/@@/))
7896
return '--'
7997
return ` ${line}`
8098
})
8199

100+
if (moreLines)
101+
formatted.push(dim(`... ${moreLines} more lines`))
102+
82103
if (showLegend) {
83104
// Compact mode
84105
if (isCompact) {
85106
formatted = [
86-
`${c.green('- Expected')} ${formatted[0]}`,
87-
`${c.red('+ Received')} ${formatted[1]}`,
107+
`${green('- Expected')} ${formatted[0]}`,
108+
`${red('+ Received')} ${formatted[1]}`,
88109
]
89110
}
90111
else {
@@ -96,12 +117,12 @@ export function unifiedDiff(actual: string, expected: string, options: DiffOptio
96117
formatted[last] = formatted[last].slice(0, formatted[last].length - 1)
97118

98119
formatted.unshift(
99-
c.green(`- Expected - ${counts['-']}`),
100-
c.red(`+ Received + ${counts['+']}`),
120+
green(`- Expected - ${counts['-']}`),
121+
red(`+ Received + ${counts['+']}`),
101122
'',
102123
)
103124
}
104125
}
105126

106-
return formatted.map(i => indent + i).join('\n')
127+
return formatted.map(i => i ? (indent + i) : i).join('\n')
107128
}

‎test/core/test/diff.test.ts

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { expect, test, vi } from 'vitest'
2+
import { displayDiff } from 'vitest/src/node/error'
3+
import { stringify } from 'vitest/src/integrations/chai/jest-matcher-utils'
4+
5+
test('displays an error for large objects', () => {
6+
const objectA = new Array(1000).fill(0).map((_, i) => ({ i, long: 'a'.repeat(i) }))
7+
const objectB = new Array(1000).fill(0).map((_, i) => ({ i, long: 'b'.repeat(i) }))
8+
const console = { log: vi.fn(), error: vi.fn() }
9+
displayDiff(stringify(objectA), stringify(objectB), console as any, { noColor: true })
10+
expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(`
11+
"Could not display diff. It's possible objects are too large to compare.
12+
Try increasing --outputDiffMaxSize option.
13+
"
14+
`)
15+
})
16+
17+
test('displays an error for large objects', () => {
18+
const console = { log: vi.fn(), error: vi.fn() }
19+
displayDiff(stringify('undefined'), stringify('undefined'), console as any, { noColor: true })
20+
expect(console.error).not.toHaveBeenCalled()
21+
})
22+
23+
test('displays diff', () => {
24+
const objectA = { a: 1, b: 2 }
25+
const objectB = { a: 1, b: 3 }
26+
const console = { log: vi.fn(), error: vi.fn() }
27+
displayDiff(stringify(objectA), stringify(objectB), console as any, { noColor: true })
28+
expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(`
29+
" - Expected - 1
30+
+ Received + 1
31+
32+
Object {
33+
\\"a\\": 1,
34+
- \\"b\\": 3,
35+
+ \\"b\\": 2,
36+
}
37+
"
38+
`)
39+
})
40+
41+
test('displays long diff', () => {
42+
const objectA = { a: 1, b: 2, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10, k: 11, l: 12, m: 13, n: 14, o: 15, p: 16, q: 17, r: 18, s: 19, t: 20, u: 21, v: 22, w: 23, x: 24, y: 25, z: 26 }
43+
const objectB = { a: 1, b: 3, k: 11, l: 12, m: 13, n: 14, p: 16, o: 17, r: 18, s: 23, t: 88, u: 21, v: 44, w: 23, x: 24, y: 25, z: 26 }
44+
const console = { log: vi.fn(), error: vi.fn() }
45+
displayDiff(stringify(objectA), stringify(objectB), console as any, { noColor: true, outputDiffMaxLines: 5 })
46+
expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(`
47+
" - Expected - 5
48+
+ Received + 13
49+
50+
Object {
51+
\\"a\\": 1,
52+
- \\"b\\": 3,
53+
+ \\"b\\": 2,
54+
+ \\"d\\": 4,
55+
... 26 more lines
56+
"
57+
`)
58+
})
59+
60+
test('displays truncated diff', () => {
61+
const stringA = `Lorem ipsum dolor sit amet, consectetur adipiscing elit.
62+
Suspendisse viverra sapien ac venenatis lacinia.
63+
Morbi consectetur arcu nec lorem lacinia tempus.`
64+
const objectB = `Quisque hendrerit metus id dapibus pulvinar.
65+
Quisque pellentesque enim a elit faucibus cursus.
66+
Sed in tellus aliquet mauris interdum semper a in lacus.`
67+
const console = { log: vi.fn(), error: vi.fn() }
68+
displayDiff((stringA), (objectB), console as any, { noColor: true, outputTruncateLength: 14 })
69+
expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(`
70+
" - Expected - 3
71+
+ Received + 3
72+
73+
- Quisque h…
74+
- Quisque p…
75+
- Sed in te…
76+
+ Lorem ips…
77+
+ Suspendis…
78+
+ Morbi con…
79+
"
80+
`)
81+
})

0 commit comments

Comments
 (0)
Please sign in to comment.