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
25 changes: 23 additions & 2 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { CoverageProvider, ResolvedCoverageOptions } from './types/coverage
import type { Reporter } from './types/reporter'
import type { TestRunResult } from './types/tests'
import type { VCSProvider } from './vcs/vcs'
import { rm } from 'node:fs/promises'
import os, { tmpdir } from 'node:os'
import { SnapshotManager } from '@vitest/snapshot/manager'
import { deepClone, deepMerge, nanoid, noop, toArray } from '@vitest/utils/helpers'
Expand Down Expand Up @@ -169,7 +170,7 @@ export class Vitest {
/** @internal */ _resolver!: VitestResolver
/** @internal */ _fetcher!: VitestFetchFunction
/** @internal */ _fsCache!: FileSystemModuleCache
/** @internal */ _tmpDir = join(tmpdir(), nanoid())
/** @internal */ _tmpDir: string | undefined = join(tmpdir(), nanoid())
/** @internal */ _traces!: Traces
/** @internal */ _harness: PluginHarness
/** @internal */ _exitTimeout: ReturnType<typeof setTimeout> | undefined
Expand Down Expand Up @@ -252,12 +253,14 @@ export class Vitest {
this._fsCache = new FileSystemModuleCache(this)
this.snapshot = new SnapshotManager({ ...resolved.snapshotOptions })
this._resolver = new VitestResolver(this.viteConfig.cacheDir, resolved)
// a closed run removes the temp dir, so a restart must allocate a new one
const tmpDir = (this._tmpDir ??= join(tmpdir(), nanoid()))
this._fetcher = createFetchModuleFunction(
this._resolver,
resolved,
this._fsCache,
this._traces,
this._tmpDir,
tmpDir,
)
}

Expand Down Expand Up @@ -1695,12 +1698,30 @@ export class Vitest {

this._checkUnhandledErrors(errors)
})
// the pool is down: forked workers read tmp module copies from disk
await this._clearTmpDir()
await this._traces?.finish()
})()
}
return this.closingPromise
}

/**
* Removes the root temp directory with the tmp module copies,
* mirroring `TestProject.clearTmpDir()`. The reference is cleared before
* the removal so a repeated close is a no-op.
*/
private async _clearTmpDir(): Promise<void> {
if (!this._tmpDir) {
return
}
const tmpDir = this._tmpDir
this._tmpDir = undefined
try {
await rm(tmpDir, { recursive: true, force: true })
} catch {}
}

/**
* Closes all projects and exit the process
* @param force If true, the process will exit immediately after closing the projects.
Expand Down
6 changes: 6 additions & 0 deletions test/node-runner/fixtures/root-tmpdir/basic.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { expect, test } from 'vitest'
import { value } from './lib.js'

test('it passes', () => {
expect(value).toBe(42)
})
1 change: 1 addition & 0 deletions test/node-runner/fixtures/root-tmpdir/lib.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const value = 42
49 changes: 49 additions & 0 deletions test/node-runner/test/root-tmpdir.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { existsSync } from 'node:fs'
import { mkdtemp, readdir, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import test from 'node:test'
import { createVitest } from 'vitest/node'

await test('closing Vitest removes the root temp directory holding tmp module copies', async (t) => {
// the root `Vitest._tmpDir` and the per-project `TestProject.tmpDir` are
// bare nanoid dirs under `os.tmpdir()`: redirect it to a sandbox and assert
// it is empty again after a successful run (issue #11224)
const sandbox = await mkdtemp(join(tmpdir(), 'vitest-root-tmpdir-'))
const envNames = ['TMPDIR', 'TEMP', 'TMP']
const previous = envNames.map((name) => process.env[name])
for (const name of envNames) {
process.env[name] = sandbox
}
t.after(async () => {
envNames.forEach((name, i) => {
if (previous[i] === undefined) {
delete process.env[name]
} else {
process.env[name] = previous[i]
}
})
await rm(sandbox, { recursive: true, force: true })
})

// `createVitest` + `start` instead of `startVitest`: the latter already
// closes the instance when the run ends
const vitest = await createVitest({
root: './fixtures/root-tmpdir',
// the forks pool is the one that reads tmp module copies from disk
pool: 'forks',
reporters: [{}],
})
t.after(() => vitest.close())

await vitest.start()

// sanity: the run wrote tmp module copies into the root temp dir, so the
// leak this test covers really happened before `close()`
t.assert.ok(vitest._tmpDir, 'root temp dir should be set during a run')
t.assert.ok(existsSync(vitest._tmpDir), 'root temp dir should exist during a run')

await vitest.close()

t.assert.deepEqual(await readdir(sandbox), [])
})
Loading