Skip to content

Commit 6797b04

Browse files
authoredApr 5, 2024··
feat: add configuration for diff truncation (#5073) (#5333)
1 parent db98145 commit 6797b04

File tree

8 files changed

+285
-29
lines changed

8 files changed

+285
-29
lines changed
 

‎docs/config/index.md

+22
Original file line numberDiff line numberDiff line change
@@ -2077,6 +2077,28 @@ export default defineConfig({
20772077
```
20782078
:::
20792079

2080+
#### diff.truncateThreshold
2081+
2082+
- **Type**: `number`
2083+
- **Default**: `0`
2084+
2085+
The maximum length of diff result to be displayed. Diffs above this threshold will be truncated.
2086+
Truncation won't take effect with default value 0.
2087+
2088+
#### diff.truncateAnnotation
2089+
2090+
- **Type**: `string`
2091+
- **Default**: `'... Diff result is truncated'`
2092+
2093+
Annotation that is output at the end of diff result if it's truncated.
2094+
2095+
#### diff.truncateAnnotationColor
2096+
2097+
- **Type**: `DiffOptionsColor = (arg: string) => string`
2098+
- **Default**: `noColor = (string: string): string => string`
2099+
2100+
Color of truncate annotation, default is output with no color.
2101+
20802102
### fakeTimers
20812103

20822104
- **Type:** `FakeTimerInstallOpts`

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

+21-15
Original file line numberDiff line numberDiff line change
@@ -81,21 +81,24 @@ function printAnnotation({
8181
return `${aColor(a)}\n${bColor(b)}\n\n`
8282
}
8383

84-
export function printDiffLines(diffs: Array<Diff>, options: DiffOptionsNormalized): string {
84+
export function printDiffLines(diffs: Array<Diff>, truncated: boolean, options: DiffOptionsNormalized): string {
8585
return printAnnotation(options, countChanges(diffs))
86-
+ (options.expand
87-
? joinAlignedDiffsExpand(diffs, options)
88-
: joinAlignedDiffsNoExpand(diffs, options))
86+
+ (options.expand ? joinAlignedDiffsExpand(diffs, options) : joinAlignedDiffsNoExpand(diffs, options))
87+
+ (truncated ? options.truncateAnnotationColor(`\n${options.truncateAnnotation}`) : '')
8988
}
9089

9190
// Compare two arrays of strings line-by-line. Format as comparison lines.
9291
export function diffLinesUnified(aLines: Array<string>, bLines: Array<string>, options?: DiffOptions): string {
92+
const normalizedOptions = normalizeDiffOptions(options)
93+
const [diffs, truncated] = diffLinesRaw(
94+
isEmptyString(aLines) ? [] : aLines,
95+
isEmptyString(bLines) ? [] : bLines,
96+
normalizedOptions,
97+
)
9398
return printDiffLines(
94-
diffLinesRaw(
95-
isEmptyString(aLines) ? [] : aLines,
96-
isEmptyString(bLines) ? [] : bLines,
97-
),
98-
normalizeDiffOptions(options),
99+
diffs,
100+
truncated,
101+
normalizedOptions,
99102
)
100103
}
101104

@@ -120,7 +123,7 @@ export function diffLinesUnified2(aLinesDisplay: Array<string>, bLinesDisplay: A
120123
return diffLinesUnified(aLinesDisplay, bLinesDisplay, options)
121124
}
122125

123-
const diffs = diffLinesRaw(aLinesCompare, bLinesCompare)
126+
const [diffs, truncated] = diffLinesRaw(aLinesCompare, bLinesCompare, options)
124127

125128
// Replace comparison lines with displayable lines.
126129
let aIndex = 0
@@ -144,13 +147,16 @@ export function diffLinesUnified2(aLinesDisplay: Array<string>, bLinesDisplay: A
144147
}
145148
})
146149

147-
return printDiffLines(diffs, normalizeDiffOptions(options))
150+
return printDiffLines(diffs, truncated, normalizeDiffOptions(options))
148151
}
149152

150153
// Compare two arrays of strings line-by-line.
151-
export function diffLinesRaw(aLines: Array<string>, bLines: Array<string>): Array<Diff> {
152-
const aLength = aLines.length
153-
const bLength = bLines.length
154+
export function diffLinesRaw(aLines: Array<string>, bLines: Array<string>, options?: DiffOptions): [Array<Diff>, boolean] {
155+
const truncate = options?.truncateThreshold ?? false
156+
const truncateThreshold = Math.max(Math.floor(options?.truncateThreshold ?? 0), 0)
157+
const aLength = truncate ? Math.min(aLines.length, truncateThreshold) : aLines.length
158+
const bLength = truncate ? Math.min(bLines.length, truncateThreshold) : bLines.length
159+
const truncated = aLength !== aLines.length || bLength !== bLines.length
154160

155161
const isCommon = (aIndex: number, bIndex: number) => aLines[aIndex] === bLines[bIndex]
156162

@@ -185,5 +191,5 @@ export function diffLinesRaw(aLines: Array<string>, bLines: Array<string>): Arra
185191
for (; bIndex !== bLength; bIndex += 1)
186192
diffs.push(new Diff(DIFF_INSERT, bLines[bIndex]))
187193

188-
return diffs
194+
return [diffs, truncated]
189195
}

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

+28-5
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,31 @@
77

88
import * as diff from 'diff-sequences'
99
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, Diff } from './cleanupSemantic'
10+
import type { DiffOptions } from './types'
11+
12+
// platforms compatible
13+
function getNewLineSymbol(string: string) {
14+
return string.includes('\r\n') ? '\r\n' : '\n'
15+
}
16+
17+
function diffStrings(a: string, b: string, options?: DiffOptions): [Array<Diff>, boolean] {
18+
const truncate = options?.truncateThreshold ?? false
19+
const truncateThreshold = Math.max(Math.floor(options?.truncateThreshold ?? 0), 0)
20+
let aLength = a.length
21+
let bLength = b.length
22+
if (truncate) {
23+
const aMultipleLines = a.includes('\n')
24+
const bMultipleLines = b.includes('\n')
25+
const aNewLineSymbol = getNewLineSymbol(a)
26+
const bNewLineSymbol = getNewLineSymbol(b)
27+
// multiple-lines string expects a newline to be appended at the end
28+
const _a = aMultipleLines ? `${a.split(aNewLineSymbol, truncateThreshold).join(aNewLineSymbol)}\n` : a
29+
const _b = bMultipleLines ? `${b.split(bNewLineSymbol, truncateThreshold).join(bNewLineSymbol)}\n` : b
30+
aLength = _a.length
31+
bLength = _b.length
32+
}
33+
const truncated = aLength !== a.length || bLength !== b.length
1034

11-
function diffStrings(a: string, b: string): Array<Diff> {
1235
const isCommon = (aIndex: number, bIndex: number) => a[aIndex] === b[bIndex]
1336

1437
let aIndex = 0
@@ -34,16 +57,16 @@ function diffStrings(a: string, b: string): Array<Diff> {
3457
// @ts-expect-error wrong bundling
3558
const diffSequences = diff.default.default || diff.default
3659

37-
diffSequences(a.length, b.length, isCommon, foundSubsequence)
60+
diffSequences(aLength, bLength, isCommon, foundSubsequence)
3861

3962
// After the last common subsequence, push remaining change items.
40-
if (aIndex !== a.length)
63+
if (aIndex !== aLength)
4164
diffs.push(new Diff(DIFF_DELETE, a.slice(aIndex)))
4265

43-
if (bIndex !== b.length)
66+
if (bIndex !== bLength)
4467
diffs.push(new Diff(DIFF_INSERT, b.slice(bIndex)))
4568

46-
return diffs
69+
return [diffs, truncated]
4770
}
4871

4972
export default diffStrings

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

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { DiffOptions, DiffOptionsNormalized } from './types'
1212
export const noColor = (string: string): string => string
1313

1414
const DIFF_CONTEXT_DEFAULT = 5
15+
const DIFF_TRUNCATE_THRESHOLD_DEFAULT = 0 // not truncate
1516

1617
function getDefaultOptions(): DiffOptionsNormalized {
1718
const c = getColors()
@@ -35,6 +36,9 @@ function getDefaultOptions(): DiffOptionsNormalized {
3536
includeChangeCounts: false,
3637
omitAnnotationLines: false,
3738
patchColor: c.yellow,
39+
truncateThreshold: DIFF_TRUNCATE_THRESHOLD_DEFAULT,
40+
truncateAnnotation: '... Diff result is truncated',
41+
truncateAnnotationColor: noColor,
3842
}
3943
}
4044

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

+6-5
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,17 @@ export function diffStringsUnified(a: string, b: string, options?: DiffOptions):
3232
const isMultiline = a.includes('\n') || b.includes('\n')
3333

3434
// getAlignedDiffs assumes that a newline was appended to the strings.
35-
const diffs = diffStringsRaw(
35+
const [diffs, truncated] = diffStringsRaw(
3636
isMultiline ? `${a}\n` : a,
3737
isMultiline ? `${b}\n` : b,
3838
true, // cleanupSemantic
39+
options,
3940
)
4041

4142
if (hasCommonDiff(diffs, isMultiline)) {
4243
const optionsNormalized = normalizeDiffOptions(options)
4344
const lines = getAlignedDiffs(diffs, optionsNormalized.changeColor)
44-
return printDiffLines(lines, optionsNormalized)
45+
return printDiffLines(lines, truncated, optionsNormalized)
4546
}
4647
}
4748

@@ -51,11 +52,11 @@ export function diffStringsUnified(a: string, b: string, options?: DiffOptions):
5152

5253
// Compare two strings character-by-character.
5354
// Optionally clean up small common substrings, also known as chaff.
54-
export function diffStringsRaw(a: string, b: string, cleanup: boolean): Array<Diff> {
55-
const diffs = diffStrings(a, b)
55+
export function diffStringsRaw(a: string, b: string, cleanup: boolean, options?: DiffOptions): [Array<Diff>, boolean] {
56+
const [diffs, truncated] = diffStrings(a, b, options)
5657

5758
if (cleanup)
5859
cleanupSemantic(diffs) // impure function
5960

60-
return diffs
61+
return [diffs, truncated]
6162
}

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

+6
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export interface DiffOptions {
2727
omitAnnotationLines?: boolean
2828
patchColor?: DiffOptionsColor
2929
compareKeys?: CompareKeys
30+
truncateThreshold?: number
31+
truncateAnnotation?: string
32+
truncateAnnotationColor?: DiffOptionsColor
3033
}
3134

3235
export interface DiffOptionsNormalized {
@@ -48,4 +51,7 @@ export interface DiffOptionsNormalized {
4851
includeChangeCounts: boolean
4952
omitAnnotationLines: boolean
5053
patchColor: DiffOptionsColor
54+
truncateThreshold: number
55+
truncateAnnotation: string
56+
truncateAnnotationColor: DiffOptionsColor
5157
}

‎packages/vitest/src/types/matcher-utils.ts

+3
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,7 @@ export interface DiffOptions {
3131
patchColor?: Formatter
3232
// pretty-format type
3333
compareKeys?: any
34+
truncateThreshold?: number
35+
truncateAnnotation?: string
36+
truncateAnnotationColor?: Formatter
3437
}

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

+195-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { expect, test, vi } from 'vitest'
22
import { getDefaultColors, setupColors } from '@vitest/utils'
3-
import { diff } from '@vitest/utils/diff'
3+
import type { DiffOptions } from '@vitest/utils/diff'
4+
import { diff, diffStringsUnified } from '@vitest/utils/diff'
45
import { processError } from '@vitest/runner'
56
import { displayDiff } from '../../../packages/vitest/src/node/error'
67

@@ -24,6 +25,28 @@ test('displays object diff', () => {
2425
`)
2526
})
2627

28+
test('display truncated object diff', () => {
29+
const objectA = { a: 1, b: 2, c: 3, d: 4, e: 5 }
30+
const objectB = { a: 1, b: 3, c: 4, d: 5, e: 6 }
31+
const console = { log: vi.fn(), error: vi.fn() }
32+
setupColors(getDefaultColors())
33+
displayDiff(diff(objectA, objectB, { truncateThreshold: 4 }), console as any)
34+
expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(`
35+
"
36+
- Expected
37+
+ Received
38+
39+
Object {
40+
"a": 1,
41+
- "b": 2,
42+
- "c": 3,
43+
+ "b": 3,
44+
+ "c": 4,
45+
... Diff result is truncated
46+
"
47+
`)
48+
})
49+
2750
test('display one line string diff', () => {
2851
const string1 = 'string1'
2952
const string2 = 'string2'
@@ -41,7 +64,24 @@ test('display one line string diff', () => {
4164
`)
4265
})
4366

44-
test('display multiline line string diff', () => {
67+
test('display one line string diff should not be affected by truncateThreshold', () => {
68+
const string1 = 'string1'
69+
const string2 = 'string2'
70+
const console = { log: vi.fn(), error: vi.fn() }
71+
setupColors(getDefaultColors())
72+
displayDiff(diff(string1, string2, { truncateThreshold: 3 }), console as any)
73+
expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(`
74+
"
75+
- Expected
76+
+ Received
77+
78+
- string1
79+
+ string2
80+
"
81+
`)
82+
})
83+
84+
test('display multiline string diff', () => {
4585
const string1 = 'string1\nstring2\nstring3'
4686
const string2 = 'string2\nstring2\nstring1'
4787
const console = { log: vi.fn(), error: vi.fn() }
@@ -61,6 +101,46 @@ test('display multiline line string diff', () => {
61101
`)
62102
})
63103

104+
test('display truncated multiline string diff', () => {
105+
const string1 = 'string1\nstring2\nstring3'
106+
const string2 = 'string2\nstring2\nstring1'
107+
const console = { log: vi.fn(), error: vi.fn() }
108+
setupColors(getDefaultColors())
109+
displayDiff(diff(string1, string2, { truncateThreshold: 2 }), console as any)
110+
expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(`
111+
"
112+
- Expected
113+
+ Received
114+
115+
- string1
116+
+ string2
117+
string2
118+
... Diff result is truncated
119+
"
120+
`)
121+
})
122+
123+
test('display truncated multiple items array diff', () => {
124+
const array1 = Array(45000).fill('foo')
125+
const array2 = Array(45000).fill('bar')
126+
const console = { log: vi.fn(), error: vi.fn() }
127+
setupColors(getDefaultColors())
128+
displayDiff(diff(array1, array2, { truncateThreshold: 3 }), console as any)
129+
expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(`
130+
"
131+
- Expected
132+
+ Received
133+
134+
Array [
135+
- "foo",
136+
- "foo",
137+
+ "bar",
138+
+ "bar",
139+
... Diff result is truncated
140+
"
141+
`)
142+
})
143+
64144
test('asymmetric matcher in object', () => {
65145
setupColors(getDefaultColors())
66146
expect(getErrorDiff({ x: 0, y: 'foo' }, { x: 1, y: expect.anything() })).toMatchInlineSnapshot(`
@@ -75,6 +155,26 @@ test('asymmetric matcher in object', () => {
75155
`)
76156
})
77157

158+
test('asymmetric matcher in object with truncated diff', () => {
159+
setupColors(getDefaultColors())
160+
expect(
161+
getErrorDiff(
162+
{ w: 'foo', x: 0, y: 'bar', z: 'baz' },
163+
{ w: expect.anything(), x: 1, y: expect.anything(), z: 'bar' },
164+
{ truncateThreshold: 3 },
165+
),
166+
).toMatchInlineSnapshot(`
167+
"- Expected
168+
+ Received
169+
170+
Object {
171+
"w": Anything,
172+
- "x": 1,
173+
+ "x": 0,
174+
... Diff result is truncated"
175+
`)
176+
})
177+
78178
test('asymmetric matcher in array', () => {
79179
setupColors(getDefaultColors())
80180
expect(getErrorDiff([0, 'foo'], [1, expect.anything()])).toMatchInlineSnapshot(`
@@ -89,6 +189,25 @@ test('asymmetric matcher in array', () => {
89189
`)
90190
})
91191

192+
test('asymmetric matcher in array with truncated diff', () => {
193+
setupColors(getDefaultColors())
194+
expect(
195+
getErrorDiff(
196+
[0, 'foo', 2],
197+
[1, expect.anything(), 3],
198+
{ truncateThreshold: 2 },
199+
),
200+
).toMatchInlineSnapshot(`
201+
"- Expected
202+
+ Received
203+
204+
Array [
205+
- 1,
206+
+ 0,
207+
... Diff result is truncated"
208+
`)
209+
})
210+
92211
test('asymmetric matcher in nested', () => {
93212
setupColors(getDefaultColors())
94213
expect(
@@ -115,6 +234,78 @@ test('asymmetric matcher in nested', () => {
115234
`)
116235
})
117236

237+
test('asymmetric matcher in nested with truncated diff', () => {
238+
setupColors(getDefaultColors())
239+
expect(
240+
getErrorDiff(
241+
[{ x: 0, y: 'foo', z: 'bar' }, [0, 'bar', 'baz']],
242+
[{ x: 1, y: expect.anything(), z: expect.anything() }, [1, expect.anything(), expect.anything()]],
243+
{ truncateThreshold: 5 },
244+
),
245+
).toMatchInlineSnapshot(`
246+
"- Expected
247+
+ Received
248+
249+
Array [
250+
Object {
251+
- "x": 1,
252+
+ "x": 0,
253+
"y": Anything,
254+
"z": Anything,
255+
... Diff result is truncated"
256+
`)
257+
})
258+
259+
test('diff for multi-line string compared by characters', () => {
260+
const string1 = `
261+
foo,
262+
bar,
263+
`
264+
const string2 = `
265+
FOO,
266+
bar,
267+
`
268+
setupColors(getDefaultColors())
269+
expect(
270+
diffStringsUnified(string1, string2),
271+
).toMatchInlineSnapshot(`
272+
"- Expected
273+
+ Received
274+
275+
276+
- foo,
277+
+ FOO,
278+
bar,
279+
"
280+
`)
281+
})
282+
283+
test('truncated diff for multi-line string compared by characters', () => {
284+
const string1 = `
285+
foo,
286+
bar,
287+
baz,
288+
`
289+
const string2 = `
290+
FOO,
291+
bar,
292+
BAZ,
293+
`
294+
setupColors(getDefaultColors())
295+
expect(
296+
diffStringsUnified(string1, string2, { truncateThreshold: 3 }),
297+
).toMatchInlineSnapshot(`
298+
"- Expected
299+
+ Received
300+
301+
302+
- foo,
303+
+ FOO,
304+
bar,
305+
... Diff result is truncated"
306+
`)
307+
})
308+
118309
test('getter only property', () => {
119310
setupColors(getDefaultColors())
120311
const x = { normalProp: 1 }
@@ -143,12 +334,12 @@ test('getter only property', () => {
143334
`)
144335
})
145336

146-
function getErrorDiff(actual: unknown, expected: unknown) {
337+
function getErrorDiff(actual: unknown, expected: unknown, options?: DiffOptions) {
147338
try {
148339
expect(actual).toEqual(expected)
149340
}
150341
catch (e) {
151-
const error = processError(e)
342+
const error = processError(e, options)
152343
return error.diff
153344
}
154345
expect.unreachable()

0 commit comments

Comments
 (0)
Please sign in to comment.