Skip to content

Commit e89c369

Browse files
authoredJan 9, 2025··
fix: capturePrintError logger duplicate event handlers (#7197)
1 parent 57b671d commit e89c369

File tree

5 files changed

+86
-77
lines changed

5 files changed

+86
-77
lines changed
 

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

+55-21
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
/* eslint-disable prefer-template */
12
import type { ErrorWithDiff, ParsedStack } from '@vitest/utils'
23
import type { Vitest } from './core'
3-
import type { ErrorOptions } from './logger'
4+
import type { ErrorOptions, Logger } from './logger'
45
import type { TestProject } from './project'
5-
/* eslint-disable prefer-template */
6+
import { Console } from 'node:console'
67
import { existsSync, readFileSync } from 'node:fs'
78
import { Writable } from 'node:stream'
89
import { stripVTControlCharacters } from 'node:util'
@@ -12,22 +13,24 @@ import c from 'tinyrainbow'
1213
import { TypeCheckError } from '../typecheck/typechecker'
1314
import {
1415
lineSplitRE,
16+
parseErrorStacktrace,
1517
positionToOffset,
1618
} from '../utils/source-map'
17-
import { Logger } from './logger'
1819
import { F_POINTER } from './reporters/renderers/figures'
1920
import { divider, truncateString } from './reporters/renderers/utils'
2021

22+
type ErrorLogger = Pick<Logger, 'error' | 'highlight'>
23+
2124
interface PrintErrorOptions {
25+
logger: ErrorLogger
2226
type?: string
23-
logger: Logger
2427
showCodeFrame?: boolean
2528
printProperties?: boolean
2629
screenshotPaths?: string[]
2730
parseErrorStacktrace: (error: ErrorWithDiff) => ParsedStack[]
2831
}
2932

30-
export interface PrintErrorResult {
33+
interface PrintErrorResult {
3134
nearest?: ParsedStack
3235
}
3336

@@ -44,15 +47,52 @@ export function capturePrintError(
4447
callback()
4548
},
4649
})
47-
const logger = new Logger(ctx, writable, writable)
48-
const result = logger.printError(error, {
50+
const console = new Console(writable)
51+
const logger: ErrorLogger = {
52+
error: console.error.bind(console),
53+
highlight: ctx.logger.highlight.bind(ctx.logger),
54+
}
55+
const result = printError(error, ctx, logger, {
4956
showCodeFrame: false,
5057
...options,
5158
})
5259
return { nearest: result?.nearest, output }
5360
}
5461

5562
export function printError(
63+
error: unknown,
64+
ctx: Vitest,
65+
logger: ErrorLogger,
66+
options: ErrorOptions,
67+
) {
68+
const project = options.project
69+
?? ctx.coreWorkspaceProject
70+
?? ctx.projects[0]
71+
return printErrorInner(error, project, {
72+
logger,
73+
type: options.type,
74+
showCodeFrame: options.showCodeFrame,
75+
screenshotPaths: options.screenshotPaths,
76+
printProperties: options.verbose,
77+
parseErrorStacktrace(error) {
78+
// browser stack trace needs to be processed differently,
79+
// so there is a separate method for that
80+
if (options.task?.file.pool === 'browser' && project.browser) {
81+
return project.browser.parseErrorStacktrace(error, {
82+
ignoreStackEntries: options.fullStack ? [] : undefined,
83+
})
84+
}
85+
86+
// node.js stack trace already has correct source map locations
87+
return parseErrorStacktrace(error, {
88+
frameFilter: project.config.onStackTrace,
89+
ignoreStackEntries: options.fullStack ? [] : undefined,
90+
})
91+
},
92+
})
93+
}
94+
95+
function printErrorInner(
5696
error: unknown,
5797
project: TestProject | undefined,
5898
options: PrintErrorOptions,
@@ -129,7 +169,7 @@ export function printError(
129169

130170
// E.g. AssertionError from assert does not set showDiff but has both actual and expected properties
131171
if (e.diff) {
132-
displayDiff(e.diff, logger.console)
172+
logger.error(`\n${e.diff}\n`)
133173
}
134174

135175
// if the error provide the frame
@@ -193,7 +233,7 @@ export function printError(
193233

194234
if (typeof e.cause === 'object' && e.cause && 'name' in e.cause) {
195235
(e.cause as any).name = `Caused by: ${(e.cause as any).name}`
196-
printError(e.cause, project, {
236+
printErrorInner(e.cause, project, {
197237
showCodeFrame: false,
198238
logger: options.logger,
199239
parseErrorStacktrace: options.parseErrorStacktrace,
@@ -251,7 +291,7 @@ const esmErrors = [
251291
'Unexpected token \'export\'',
252292
]
253293

254-
function handleImportOutsideModuleError(stack: string, logger: Logger) {
294+
function handleImportOutsideModuleError(stack: string, logger: ErrorLogger) {
255295
if (!esmErrors.some(e => stack.includes(e))) {
256296
return
257297
}
@@ -274,7 +314,7 @@ function handleImportOutsideModuleError(stack: string, logger: Logger) {
274314
}
275315

276316
function printModuleWarningForPackage(
277-
logger: Logger,
317+
logger: ErrorLogger,
278318
path: string,
279319
name: string,
280320
) {
@@ -305,7 +345,7 @@ function printModuleWarningForPackage(
305345
)
306346
}
307347

308-
function printModuleWarningForSourceCode(logger: Logger, path: string) {
348+
function printModuleWarningForSourceCode(logger: ErrorLogger, path: string) {
309349
logger.error(
310350
c.yellow(
311351
`Module ${path} seems to be an ES Module but shipped in a CommonJS package. `
@@ -314,13 +354,7 @@ function printModuleWarningForSourceCode(logger: Logger, path: string) {
314354
)
315355
}
316356

317-
export function displayDiff(diff: string | undefined, console: Console) {
318-
if (diff) {
319-
console.error(`\n${diff}\n`)
320-
}
321-
}
322-
323-
function printErrorMessage(error: ErrorWithDiff, logger: Logger) {
357+
function printErrorMessage(error: ErrorWithDiff, logger: ErrorLogger) {
324358
const errorName = error.name || error.nameStr || 'Unknown Error'
325359
if (!error.message) {
326360
logger.error(error)
@@ -335,8 +369,8 @@ function printErrorMessage(error: ErrorWithDiff, logger: Logger) {
335369
}
336370
}
337371

338-
export function printStack(
339-
logger: Logger,
372+
function printStack(
373+
logger: ErrorLogger,
340374
project: TestProject,
341375
stack: ParsedStack[],
342376
highlight: ParsedStack | undefined,

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

+2-29
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ import type { ErrorWithDiff } from '@vitest/utils'
33
import type { Writable } from 'node:stream'
44
import type { TypeCheckError } from '../typecheck/typechecker'
55
import type { Vitest } from './core'
6-
import type { PrintErrorResult } from './error'
76
import type { TestProject } from './project'
87
import { Console } from 'node:console'
98
import { toArray } from '@vitest/utils'
10-
import { parseErrorStacktrace } from '@vitest/utils/source-map'
119
import c from 'tinyrainbow'
1210
import { highlightCode } from '../utils/colors'
1311
import { printError } from './error'
@@ -106,33 +104,8 @@ export class Logger {
106104
this.console.log(`${CURSOR_TO_START}${ERASE_DOWN}${log}`)
107105
}
108106

109-
printError(err: unknown, options: ErrorOptions = {}): PrintErrorResult | undefined {
110-
const { fullStack = false, type } = options
111-
const project = options.project
112-
?? this.ctx.coreWorkspaceProject
113-
?? this.ctx.projects[0]
114-
return printError(err, project, {
115-
type,
116-
showCodeFrame: options.showCodeFrame ?? true,
117-
logger: this,
118-
printProperties: options.verbose,
119-
screenshotPaths: options.screenshotPaths,
120-
parseErrorStacktrace: (error) => {
121-
// browser stack trace needs to be processed differently,
122-
// so there is a separate method for that
123-
if (options.task?.file.pool === 'browser' && project.browser) {
124-
return project.browser.parseErrorStacktrace(error, {
125-
ignoreStackEntries: fullStack ? [] : undefined,
126-
})
127-
}
128-
129-
// node.js stack trace already has correct source map locations
130-
return parseErrorStacktrace(error, {
131-
frameFilter: project.config.onStackTrace,
132-
ignoreStackEntries: fullStack ? [] : undefined,
133-
})
134-
},
135-
})
107+
printError(err: unknown, options: ErrorOptions = {}) {
108+
printError(err, this.ctx, this, options)
136109
}
137110

138111
clearHighlightCache(filename?: string) {

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

+13-26
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ import type { DiffOptions } from '@vitest/utils/diff'
22
import { stripVTControlCharacters } from 'node:util'
33
import { processError } from '@vitest/runner'
44
import { diff, diffStringsUnified, printDiffOrStringify } from '@vitest/utils/diff'
5-
import { expect, test, vi } from 'vitest'
6-
import { displayDiff } from '../../../packages/vitest/src/node/error'
5+
import { expect, test } from 'vitest'
6+
7+
function wrapDiff(diff?: string) {
8+
return diff && stripVTControlCharacters(`\n${diff}\n`)
9+
}
710

811
test('displays string diff', () => {
912
const stringA = 'Hello AWorld'
1013
const stringB = 'Hello BWorld'
11-
const console = { log: vi.fn(), error: vi.fn() }
12-
displayDiff(printDiffOrStringify(stringA, stringB), console as any)
13-
expect(stripVTControlCharacters(console.error.mock.calls[0][0])).toMatchInlineSnapshot(`
14+
expect(wrapDiff(printDiffOrStringify(stringA, stringB))).toMatchInlineSnapshot(`
1415
"
1516
Expected: "Hello BWorld"
1617
Received: "Hello AWorld"
@@ -21,9 +22,7 @@ test('displays string diff', () => {
2122
test('displays object diff', () => {
2223
const objectA = { a: 1, b: 2 }
2324
const objectB = { a: 1, b: 3 }
24-
const console = { log: vi.fn(), error: vi.fn() }
25-
displayDiff(diff(objectA, objectB), console as any)
26-
expect(stripVTControlCharacters(console.error.mock.calls[0][0])).toMatchInlineSnapshot(`
25+
expect(wrapDiff(diff(objectA, objectB))).toMatchInlineSnapshot(`
2726
"
2827
- Expected
2928
+ Received
@@ -40,9 +39,7 @@ test('displays object diff', () => {
4039
test('display truncated object diff', () => {
4140
const objectA = { a: 1, b: 2, c: 3, d: 4, e: 5 }
4241
const objectB = { a: 1, b: 3, c: 4, d: 5, e: 6 }
43-
const console = { log: vi.fn(), error: vi.fn() }
44-
displayDiff(diff(objectA, objectB, { truncateThreshold: 4 }), console as any)
45-
expect(stripVTControlCharacters(console.error.mock.calls[0][0])).toMatchInlineSnapshot(`
42+
expect(wrapDiff(diff(objectA, objectB, { truncateThreshold: 4 }))).toMatchInlineSnapshot(`
4643
"
4744
- Expected
4845
+ Received
@@ -61,9 +58,7 @@ test('display truncated object diff', () => {
6158
test('display one line string diff', () => {
6259
const string1 = 'string1'
6360
const string2 = 'string2'
64-
const console = { log: vi.fn(), error: vi.fn() }
65-
displayDiff(diff(string1, string2), console as any)
66-
expect(stripVTControlCharacters(console.error.mock.calls[0][0])).toMatchInlineSnapshot(`
61+
expect(wrapDiff(diff(string1, string2))).toMatchInlineSnapshot(`
6762
"
6863
- Expected
6964
+ Received
@@ -77,9 +72,7 @@ test('display one line string diff', () => {
7772
test('display one line string diff should not be affected by truncateThreshold', () => {
7873
const string1 = 'string1'
7974
const string2 = 'string2'
80-
const console = { log: vi.fn(), error: vi.fn() }
81-
displayDiff(diff(string1, string2, { truncateThreshold: 3 }), console as any)
82-
expect(stripVTControlCharacters(console.error.mock.calls[0][0])).toMatchInlineSnapshot(`
75+
expect(wrapDiff(diff(string1, string2, { truncateThreshold: 3 }))).toMatchInlineSnapshot(`
8376
"
8477
- Expected
8578
+ Received
@@ -93,9 +86,7 @@ test('display one line string diff should not be affected by truncateThreshold',
9386
test('display multiline string diff', () => {
9487
const string1 = 'string1\nstring2\nstring3'
9588
const string2 = 'string2\nstring2\nstring1'
96-
const console = { log: vi.fn(), error: vi.fn() }
97-
displayDiff(diff(string1, string2), console as any)
98-
expect(stripVTControlCharacters(console.error.mock.calls[0][0])).toMatchInlineSnapshot(`
89+
expect(wrapDiff(diff(string1, string2))).toMatchInlineSnapshot(`
9990
"
10091
- Expected
10192
+ Received
@@ -112,9 +103,7 @@ test('display multiline string diff', () => {
112103
test('display truncated multiline string diff', () => {
113104
const string1 = 'string1\nstring2\nstring3'
114105
const string2 = 'string2\nstring2\nstring1'
115-
const console = { log: vi.fn(), error: vi.fn() }
116-
displayDiff(diff(string1, string2, { truncateThreshold: 2 }), console as any)
117-
expect(stripVTControlCharacters(console.error.mock.calls[0][0])).toMatchInlineSnapshot(`
106+
expect(wrapDiff(diff(string1, string2, { truncateThreshold: 2 }))).toMatchInlineSnapshot(`
118107
"
119108
- Expected
120109
+ Received
@@ -130,9 +119,7 @@ test('display truncated multiline string diff', () => {
130119
test('display truncated multiple items array diff', () => {
131120
const array1 = Array.from({ length: 45000 }).fill('foo')
132121
const array2 = Array.from({ length: 45000 }).fill('bar')
133-
const console = { log: vi.fn(), error: vi.fn() }
134-
displayDiff(diff(array1, array2, { truncateThreshold: 3 }), console as any)
135-
expect(stripVTControlCharacters(console.error.mock.calls[0][0])).toMatchInlineSnapshot(`
122+
expect(wrapDiff(diff(array1, array2, { truncateThreshold: 3 }))).toMatchInlineSnapshot(`
136123
"
137124
- Expected
138125
+ Received
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from "vitest"
2+
3+
test.for([...Array(20)].map((_, j) => j))('%i', (i) => {
4+
throw new Error(`error-${i}`)
5+
})

‎test/reporters/tests/junit.test.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { File, Suite, Task, TaskResult } from 'vitest'
22
import { resolve } from 'pathe'
33
import { expect, test } from 'vitest'
44
import { getDuration } from '../../../packages/vitest/src/node/reporters/junit'
5-
import { runVitest } from '../../test-utils'
5+
import { runVitest, runVitestCli } from '../../test-utils'
66

77
const root = resolve(__dirname, '../fixtures')
88

@@ -169,3 +169,13 @@ test.each([true, false])('addFileAttribute %s', async (t) => {
169169
})
170170
expect(stabilizeReport(stdout)).matchSnapshot()
171171
})
172+
173+
test('many errors without warning', async () => {
174+
const { stderr } = await runVitestCli(
175+
'run',
176+
'--reporter=junit',
177+
'--root',
178+
resolve(import.meta.dirname, '../fixtures/many-errors'),
179+
)
180+
expect(stderr).not.toContain('MaxListenersExceededWarning')
181+
})

0 commit comments

Comments
 (0)
Please sign in to comment.