Skip to content

Commit ed4e042

Browse files
AriPerkkiosheremet-va
authored andcommitted
fix: isolate workers between envs and workspaces
1 parent 8dd5ea5 commit ed4e042

File tree

12 files changed

+160
-13
lines changed

12 files changed

+160
-13
lines changed

packages/vitest/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@
154154
"std-env": "^3.3.3",
155155
"strip-literal": "^1.0.1",
156156
"tinybench": "^2.5.0",
157-
"tinypool": "^0.6.0",
157+
"tinypool": "^0.7.0",
158158
"vite": "^3.0.0 || ^4.0.0",
159159
"vite-node": "workspace:*",
160160
"why-is-node-running": "^2.2.2"

packages/vitest/src/node/pools/threads.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,29 @@ export function createThreadsPool(ctx: Vitest, { execArgv, env }: PoolProcessOpt
147147

148148
if (multipleThreads.length) {
149149
const filesByEnv = await groupFilesByEnv(multipleThreads)
150-
const promises = Object.values(filesByEnv).flat()
151-
const results = await Promise.allSettled(promises
152-
.map(({ file, environment, project }) => runFiles(project, getConfig(project), [file], environment, invalidates)))
150+
const files = Object.values(filesByEnv).flat()
151+
const results: PromiseSettledResult<void>[] = []
152+
153+
if (ctx.config.isolate) {
154+
results.push(...await Promise.allSettled(files.map(({ file, environment, project }) =>
155+
runFiles(project, getConfig(project), [file], environment, invalidates))))
156+
}
157+
else {
158+
// When isolation is disabled, we still need to isolate environments and workspace projects from each other.
159+
// Tasks are still running parallel but environments are isolated between tasks.
160+
const grouped = groupBy(files, ({ project, environment }) => project.getName() + environment.name + JSON.stringify(environment.options))
161+
162+
for (const group of Object.values(grouped)) {
163+
// Push all files to pool's queue
164+
results.push(...await Promise.allSettled(group.map(({ file, environment, project }) =>
165+
runFiles(project, getConfig(project), [file], environment, invalidates))))
166+
167+
// Once all tasks are running or finished, recycle worker for isolation.
168+
// On-going workers will run in the previous environment.
169+
await new Promise<void>(resolve => pool.queueSize === 0 ? resolve() : pool.once('drain', resolve))
170+
await pool.recycleWorkers()
171+
}
172+
}
153173

154174
const errors = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected').map(r => r.reason)
155175
if (errors.length > 0)
@@ -162,7 +182,6 @@ export function createThreadsPool(ctx: Vitest, { execArgv, env }: PoolProcessOpt
162182
Object.keys(filesByEnv).filter(env => !envsOrder.includes(env)),
163183
)
164184

165-
// always run environments isolated between each other
166185
for (const env of envs) {
167186
const files = filesByEnv[env]
168187

@@ -171,12 +190,13 @@ export function createThreadsPool(ctx: Vitest, { execArgv, env }: PoolProcessOpt
171190

172191
const filesByOptions = groupBy(files, ({ project, environment }) => project.getName() + JSON.stringify(environment.options))
173192

174-
const promises = Object.values(filesByOptions).map(async (files) => {
193+
for (const files of Object.values(filesByOptions)) {
194+
// Always run environments isolated between each other
195+
await pool.recycleWorkers()
196+
175197
const filenames = files.map(f => f.file)
176198
await runFiles(files[0].project, getConfig(files[0].project), filenames, files[0].environment, invalidates)
177-
})
178-
179-
await Promise.all(promises)
199+
}
180200
}
181201
}
182202
}

pnpm-lock.yaml

Lines changed: 16 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @vitest-environment happy-dom
3+
*/
4+
5+
import { expect, test } from 'vitest'
6+
7+
test('Leaking globals not found', async () => {
8+
(globalThis as any).__leaking_from_happy_dom = 'leaking'
9+
expect((globalThis as any).__leaking_from_node).toBe(undefined)
10+
expect((globalThis as any).__leaking_from_jsdom).toBe(undefined)
11+
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
5+
import { expect, test } from 'vitest'
6+
7+
test('Leaking globals not found', async () => {
8+
(globalThis as any).__leaking_from_jsdom = 'leaking'
9+
expect((globalThis as any).__leaking_from_node).toBe(undefined)
10+
expect((globalThis as any).__leaking_from_happy_dom).toBe(undefined)
11+
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
5+
import { expect, test } from 'vitest'
6+
7+
test('Leaking globals not found', async () => {
8+
(globalThis as any).__leaking_from_node = 'leaking'
9+
expect((globalThis as any).__leaking_from_jsdom).toBe(undefined)
10+
expect((globalThis as any).__leaking_from_happy_dom).toBe(undefined)
11+
})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { expect, test } from 'vitest'
2+
3+
test('Leaking globals not found', async () => {
4+
expect((globalThis as any).__leaking_from_workspace_project).toBe(undefined)
5+
6+
;(globalThis as any).__leaking_from_workspace_project = 'leaking'
7+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { defineConfig } from 'vitest/config'
2+
3+
export default defineConfig({
4+
test: {},
5+
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { resolve } from 'node:path'
2+
import { fileURLToPath } from 'node:url'
3+
import { defineWorkspace } from 'vitest/config'
4+
5+
const __filename = fileURLToPath(import.meta.url)
6+
const __dirname = resolve(__filename, '..')
7+
8+
export default defineWorkspace([
9+
{
10+
test: {
11+
name: 'Project #1',
12+
root: resolve(__dirname, './project'),
13+
},
14+
},
15+
{
16+
test: {
17+
name: 'Project #2',
18+
root: resolve(__dirname, './project'),
19+
},
20+
},
21+
])

test/env-mixed/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "@vitest/test-env-mixed",
3+
"private": true,
4+
"scripts": {
5+
"test": "vitest"
6+
},
7+
"devDependencies": {
8+
"happy-dom": "latest",
9+
"vite": "latest",
10+
"vitest": "workspace:*"
11+
}
12+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { type UserConfig, expect, test } from 'vitest'
2+
3+
import { runVitest } from '../../test-utils'
4+
5+
const configs: UserConfig[] = [
6+
{ isolate: false, singleThread: true },
7+
{ isolate: false, singleThread: false },
8+
{ isolate: false, threads: true, minThreads: 1, maxThreads: 1 },
9+
{ isolate: false, threads: false },
10+
]
11+
12+
test.each(configs)('should isolate environments when %s', async (config) => {
13+
const { stderr, stdout } = await runVitest({
14+
root: './fixtures',
15+
...config,
16+
})
17+
18+
expect(stderr).toBe('')
19+
20+
expect(stdout).toContain('✓ test/node.test.ts')
21+
expect(stdout).toContain('✓ test/jsdom.test.ts')
22+
expect(stdout).toContain('✓ test/happy-dom.test.ts')
23+
expect(stdout).toContain('✓ test/workspace-project.test.ts')
24+
expect(stdout).toContain('Test Files 8 passed (8)')
25+
expect(stdout).toContain('Tests 8 passed (8)')
26+
})

test/env-mixed/vitest.config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { defineConfig } from 'vitest/config'
2+
3+
export default defineConfig({
4+
test: {
5+
include: ['./test/**/*'],
6+
testTimeout: process.env.CI ? 30_000 : 10_000,
7+
chaiConfig: {
8+
truncateThreshold: 0,
9+
},
10+
},
11+
})

0 commit comments

Comments
 (0)