Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 29 additions & 15 deletions packages/vitest/src/node/reporters/base.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { File, Task, TestAnnotation } from '@vitest/runner'
import type { SerializedError } from '@vitest/utils'
import type { TestError, UserConsoleLog } from '../../types/general'
import type { ParsedStack, SerializedError } from '@vitest/utils'
import type { AsyncLeak, TestError, UserConsoleLog } from '../../types/general'
import type { Vitest } from '../core'
import type { TestSpecification } from '../test-specification'
import type { Reporter, TestRunEndReason } from '../types/reporter'
Expand Down Expand Up @@ -521,17 +521,18 @@ export abstract class BaseReporter implements Reporter {

reportSummary(files: File[], errors: unknown[]): void {
this.printErrorsSummary(files, errors)
this.printLeaksSummary()

const leakCount = this.printLeaksSummary()

if (this.ctx.config.mode === 'benchmark') {
this.reportBenchmarkSummary(files)
}
else {
this.reportTestSummary(files, errors)
this.reportTestSummary(files, errors, leakCount)
}
}

reportTestSummary(files: File[], errors: unknown[]): void {
reportTestSummary(files: File[], errors: unknown[], leakCount: number): void {
this.log()

const affectedFiles = [
Expand Down Expand Up @@ -575,10 +576,8 @@ export abstract class BaseReporter implements Reporter {
)
}

const leaks = this.ctx.state.leakSet.size

if (leaks) {
this.log(padSummaryTitle('Leaks'), c.bold(c.red(`${leaks} leak${leaks > 1 ? 's' : ''}`)))
if (leakCount) {
this.log(padSummaryTitle('Leaks'), c.bold(c.red(`${leakCount} leak${leakCount > 1 ? 's' : ''}`)))
}

this.log(padSummaryTitle('Start at'), this._timeStart)
Expand Down Expand Up @@ -789,22 +788,35 @@ export abstract class BaseReporter implements Reporter {
const leaks = this.ctx.state.leakSet

if (leaks.size === 0) {
return
return 0
}

this.error(`\n${errorBanner(`Async Leaks ${leaks.size}`)}\n`)
const leakWithStacks = new Map<string, { leak: AsyncLeak; stacks: ParsedStack[] }>()

// Leaks can be duplicate, where type and position are identical
for (const leak of leaks) {
const filename = this.relative(leak.filename)

this.ctx.logger.error(c.red(`${leak.type} leaking in ${filename}`))

const stacks = parseStacktrace(leak.stack)

if (stacks.length === 0) {
continue
}

const filename = this.relative(leak.filename)
const key = `${filename}:${stacks[0].line}:${stacks[0].column}:${leak.type}`

if (leakWithStacks.has(key)) {
continue
}

leakWithStacks.set(key, { leak, stacks })
}

this.error(`\n${errorBanner(`Async Leaks ${leakWithStacks.size}`)}\n`)

for (const { leak, stacks } of leakWithStacks.values()) {
const filename = this.relative(leak.filename)
this.ctx.logger.error(c.red(`${leak.type} leaking in ${filename}`))

try {
const sourceCode = readFileSync(stacks[0].file, 'utf-8')

Expand All @@ -828,6 +840,8 @@ export abstract class BaseReporter implements Reporter {
{},
)
}

return leakWithStacks.size
}

reportBenchmarkSummary(files: File[]): void {
Expand Down
5 changes: 5 additions & 0 deletions packages/vitest/src/runtime/detect-async-leaks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,20 @@ export function detectAsyncLeaks(testFile: string, projectName: string): () => P
}

let stack = ''
const limit = Error.stackTraceLimit

// VitestModuleEvaluator's async wrapper of node:vm causes out-of-bound stack traces, simply skip it.
// Crash fixed in https://github.com/vitejs/vite/pull/21585
try {
Error.stackTraceLimit = 100
stack = new Error('VITEST_DETECT_ASYNC_LEAKS').stack || ''
}
catch {
return
}
finally {
Error.stackTraceLimit = limit
}

if (!stack.includes(testFile)) {
const trigger = resources.get(triggerAsyncId)
Expand Down
39 changes: 37 additions & 2 deletions test/cli/test/detect-async-leaks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,46 @@ test('fetch', async () => {
await fetch('https://vitest.dev').then(response => response.text())
})
`,

'packages/example/test/example-2.test.ts': `
import { createServer } from "node:http";

let setConnected = () => {}
let waitConnection = new Promise(resolve => (setConnected = resolve))

beforeAll(async () => {
const server = createServer((_, res) => {
setConnected();
setTimeout(() => res.end("Hello after 10 seconds!"), 10_000).unref();
});
await new Promise((resolve) => server.listen(5179, resolve));
return () => server.close();
});

test("is a leak", async () => {
fetch('http://localhost:5179');
await waitConnection;
});
`,
})

expect.soft(stdout).not.toContain('Leak')
expect.soft(stdout).toContain('Leaks 1 leak')

expect(stderr).toBe('')
expect(stderr).toMatchInlineSnapshot(`
"
⎯⎯⎯⎯⎯⎯⎯ Async Leaks 1 ⎯⎯⎯⎯⎯⎯⎯⎯

PROMISE leaking in packages/example/test/example-2.test.ts
15|
16| test("is a leak", async () => {
17| fetch('http://localhost:5179');
| ^
18| await waitConnection;
19| });
❯ packages/example/test/example-2.test.ts:17:9

"
`)
})

test('fs handle', async () => {
Expand Down
Loading