[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(pool): capture workers `stdio` to logger (#8809)

authored by

Ari Perkkiö and committed by
GitHub
(Oct 27, 2025, 10:20 AM +0100) fb95fc73 7c8255f3

+92 -24
+7 -13
test/coverage-test/vitest.config.ts
··· 8 8 const BROWSER_TESTS = 'test/**.browser.test.ts' 9 9 const FIXTURES = '**/fixtures/**' 10 10 11 - const config = defineConfig({ 12 - test: { 13 - pool: 'threads', 14 - setupFiles: ['./setup.ts'], 15 - }, 16 - }) 17 - 18 11 export default defineConfig({ 19 12 test: { 20 13 reporters: 'verbose', 21 14 isolate: false, 15 + setupFiles: ['./setup.ts'], 22 16 projects: [ 23 17 // Test cases for v8-provider 24 18 { 19 + extends: true, 25 20 test: { 26 - ...config.test, 27 21 name: { label: 'v8', color: 'green' }, 28 22 env: { COVERAGE_PROVIDER: 'v8' }, 29 23 include: [GENERIC_TESTS, V8_TESTS], ··· 39 33 40 34 // Test cases for istanbul-provider 41 35 { 36 + extends: true, 42 37 test: { 43 - ...config.test, 44 38 name: { label: 'istanbul', color: 'magenta' }, 45 39 env: { COVERAGE_PROVIDER: 'istanbul' }, 46 40 include: [GENERIC_TESTS, ISTANBUL_TESTS], ··· 56 50 57 51 // Test cases for custom-provider 58 52 { 53 + extends: true, 59 54 test: { 60 - ...config.test, 61 55 name: { label: 'custom', color: 'yellow' }, 62 56 env: { COVERAGE_PROVIDER: 'custom' }, 63 57 include: [CUSTOM_TESTS], ··· 67 61 68 62 // Test cases for browser. Browser mode itself is activated by COVERAGE_BROWSER env var. 69 63 { 64 + extends: true, 70 65 test: { 71 - ...config.test, 72 66 name: { label: 'istanbul-browser', color: 'blue' }, 73 67 env: { COVERAGE_PROVIDER: 'istanbul', COVERAGE_BROWSER: 'true' }, 74 68 testTimeout: 15_000, ··· 98 92 }, 99 93 }, 100 94 { 95 + extends: true, 101 96 test: { 102 - ...config.test, 103 97 name: { label: 'v8-browser', color: 'red' }, 104 98 env: { COVERAGE_PROVIDER: 'v8', COVERAGE_BROWSER: 'true' }, 105 99 testTimeout: 15_000, ··· 131 125 132 126 // Test cases that aren't provider specific 133 127 { 128 + extends: true, 134 129 test: { 135 - ...config.test, 136 130 name: { label: 'unit', color: 'cyan' }, 137 131 include: [UNIT_TESTS], 138 132 typecheck: {
+3 -7
test/config/test/console.test.ts
··· 1 - import { expect, test, vi } from 'vitest' 1 + import { expect, test } from 'vitest' 2 2 import { runVitest } from '../../test-utils' 3 3 4 4 test('default intercept', async () => { ··· 9 9 }) 10 10 11 11 test.each(['threads', 'vmThreads'] as const)(`disable intercept pool=%s`, async (pool) => { 12 - // `disableConsoleIntercept: true` forwards workers console.error to main thread's stderr 13 - const spy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true) 14 - 15 - await runVitest({ 12 + const { stderr } = await runVitest({ 16 13 root: './fixtures/console', 17 14 disableConsoleIntercept: true, 18 15 pool, 19 16 }) 20 17 21 - const call = spy.mock.lastCall![0] 22 - expect(call.toString()).toBe('__test_console__\n') 18 + expect(stderr).toBe('__test_console__\n') 23 19 }) 24 20 25 21 test('group synchronous console logs', async () => {
+1 -1
test/config/test/node-sqlite.test.ts
··· 8 8 'vitest.config.ts': { 9 9 test: { 10 10 pool: 'forks', 11 - execArgv: ['--experimental-sqlite'], 11 + execArgv: ['--experimental-sqlite', '--no-warnings=ExperimentalWarning'], 12 12 }, 13 13 }, 14 14 'basic.test.ts': ts`
+12
test/config/test/pool.test.ts
··· 9 9 10 10 expect(config.pool).toBe(pool) 11 11 }) 12 + 13 + test('can capture worker\'s stdout and stderr', async () => { 14 + const { stdout, stderr } = await runVitest({ 15 + root: './fixtures/pool', 16 + include: ['write-to-stdout-and-stderr.test.ts'], 17 + pool, 18 + }) 19 + 20 + expect(stderr).toContain('Worker writing to stderr') 21 + expect(stdout).toContain('Worker writing to stdout') 22 + expect(stderr).toContain('MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 3 message listeners added to [TestFixturesCustomEmitter]') 23 + }) 12 24 }) 13 25 14 26 test('extended project inherits top-level pool related options', async () => {
+19
test/config/fixtures/pool/write-to-stdout-and-stderr.test.ts
··· 1 + import { test } from 'vitest' 2 + import EventEmitter from 'node:events' 3 + 4 + test('write to streams', () => { 5 + process.stdout.write('Worker writing to stdout') 6 + process.stderr.write('Worker writing to stderr') 7 + 8 + triggerNodeWarning() 9 + }) 10 + 11 + function triggerNodeWarning() { 12 + const emitter = new TestFixturesCustomEmitter() 13 + emitter.setMaxListeners(2) 14 + emitter.addListener('message', () => {}) 15 + emitter.addListener('message', () => {}) 16 + emitter.addListener('message', () => {}) 17 + } 18 + 19 + class TestFixturesCustomEmitter extends EventEmitter {}
+27
packages/vitest/src/node/pools/workers/forksWorker.ts
··· 1 1 import type { ChildProcess } from 'node:child_process' 2 + import type { Writable } from 'node:stream' 2 3 import type { SerializedConfig } from '../../types/config' 3 4 import type { PoolOptions, PoolWorker, WorkerRequest } from '../types' 4 5 import { fork } from 'node:child_process' ··· 17 18 protected env: Partial<NodeJS.ProcessEnv> 18 19 19 20 private _fork?: ChildProcess 21 + private stdout: NodeJS.WriteStream | Writable 22 + private stderr: NodeJS.WriteStream | Writable 20 23 21 24 constructor(options: PoolOptions) { 22 25 this.execArgv = options.execArgv 23 26 this.env = options.env 27 + this.stdout = options.project.vitest.logger.outputStream 28 + this.stderr = options.project.vitest.logger.errorStream 29 + 24 30 /** Loads {@link file://./../../../runtime/workers/forks.ts} */ 25 31 this.entrypoint = resolve(options.distPath, 'workers/forks.js') 26 32 } ··· 51 57 this._fork ||= fork(this.entrypoint, [], { 52 58 env: this.env, 53 59 execArgv: this.execArgv, 60 + stdio: 'pipe', 54 61 }) 62 + 63 + if (this._fork.stdout) { 64 + this.stdout.setMaxListeners(1 + this.stdout.getMaxListeners()) 65 + this._fork.stdout.pipe(this.stdout) 66 + } 67 + 68 + if (this._fork.stderr) { 69 + this.stderr.setMaxListeners(1 + this.stderr.getMaxListeners()) 70 + this._fork.stderr.pipe(this.stderr) 71 + } 55 72 } 56 73 57 74 async stop(): Promise<void> { ··· 79 96 fork.kill() 80 97 await waitForExit 81 98 clearTimeout(sigkillTimeout) 99 + 100 + if (fork.stdout) { 101 + fork.stdout?.unpipe(this.stdout) 102 + this.stdout.setMaxListeners(this.stdout.getMaxListeners() - 1) 103 + } 104 + 105 + if (fork.stderr) { 106 + fork.stderr?.unpipe(this.stderr) 107 + this.stderr.setMaxListeners(this.stderr.getMaxListeners() - 1) 108 + } 82 109 83 110 this._fork = undefined 84 111 }
+23 -3
packages/vitest/src/node/pools/workers/threadsWorker.ts
··· 1 + import type { Writable } from 'node:stream' 1 2 import type { PoolOptions, PoolWorker, WorkerRequest } from '../types' 2 3 import { resolve } from 'node:path' 3 4 import { Worker } from 'node:worker_threads' ··· 11 12 protected env: Partial<NodeJS.ProcessEnv> 12 13 13 14 private _thread?: Worker 15 + private stdout: NodeJS.WriteStream | Writable 16 + private stderr: NodeJS.WriteStream | Writable 14 17 15 18 constructor(options: PoolOptions) { 16 19 this.execArgv = options.execArgv 17 20 this.env = options.env 21 + this.stdout = options.project.vitest.logger.outputStream 22 + this.stderr = options.project.vitest.logger.errorStream 23 + 18 24 /** Loads {@link file://./../../../runtime/workers/threads.ts} */ 19 25 this.entrypoint = resolve(options.distPath, 'workers/threads.js') 20 26 } ··· 36 42 this._thread ||= new Worker(this.entrypoint, { 37 43 env: this.env, 38 44 execArgv: this.execArgv, 45 + stdout: true, 46 + stderr: true, 39 47 }) 48 + 49 + this.stdout.setMaxListeners(1 + this.stdout.getMaxListeners()) 50 + this._thread.stdout.pipe(this.stdout) 51 + 52 + this.stderr.setMaxListeners(1 + this.stderr.getMaxListeners()) 53 + this._thread.stderr.pipe(this.stderr) 40 54 } 41 55 42 56 async stop(): Promise<void> { 43 - await this.thread.terminate().then(() => { 44 - this._thread = undefined 45 - }) 57 + await this.thread.terminate() 58 + 59 + this._thread?.stdout?.unpipe(this.stdout) 60 + this.stdout.setMaxListeners(this.stdout.getMaxListeners() - 1) 61 + 62 + this._thread?.stderr?.unpipe(this.stderr) 63 + this.stderr.setMaxListeners(this.stderr.getMaxListeners() - 1) 64 + 65 + this._thread = undefined 46 66 } 47 67 48 68 deserialize(data: unknown): unknown {