[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.

feat: running vitest with `--related --watch` reruns non-affected tests if they were changed during a run (#3844)

authored by

Vladimir and committed by
GitHub
(Jul 30, 2023, 1:07 PM +0200) c9aeac4c 2e5847ec

+154 -54
+27 -20
test/test-utils/index.ts
··· 100 100 let setDone: (value?: unknown) => void 101 101 const isDone = new Promise(resolve => (setDone = resolve)) 102 102 103 - const vitest = { 103 + const cli = { 104 104 stdout: '', 105 105 stderr: '', 106 106 stdoutListeners: [] as (() => void)[], ··· 165 165 } 166 166 167 167 subprocess.stdout!.on('data', (data) => { 168 - vitest.stdout += stripAnsi(data.toString()) 169 - vitest.stdoutListeners.forEach(fn => fn()) 168 + cli.stdout += stripAnsi(data.toString()) 169 + cli.stdoutListeners.forEach(fn => fn()) 170 170 }) 171 171 172 172 subprocess.stderr!.on('data', (data) => { 173 - vitest.stderr += stripAnsi(data.toString()) 174 - vitest.stderrListeners.forEach(fn => fn()) 173 + cli.stderr += stripAnsi(data.toString()) 174 + cli.stderrListeners.forEach(fn => fn()) 175 175 }) 176 176 177 177 subprocess.on('exit', () => setDone()) ··· 181 181 if (subprocess.exitCode === null) 182 182 subprocess.kill() 183 183 184 - await vitest.isDone 184 + await cli.isDone 185 185 }) 186 186 187 - if (command !== 'vitest') { 188 - if (!args.includes('--watch')) 189 - await vitest.isDone 190 - else 191 - await vitest.waitForStdout('[vie-node] watcher is ready') 192 - return vitest 193 - } 194 - 195 - if (args.includes('--watch')) { // Wait for initial test run to complete 196 - await vitest.waitForStdout('Waiting for file changes') 197 - vitest.resetOutput() 187 + if (args.includes('--watch')) { 188 + if (command === 'vitest') // Wait for initial test run to complete 189 + await cli.waitForStdout('Waiting for file changes') 190 + // make sure watcher is ready 191 + await cli.waitForStdout('[debug] watcher is ready') 192 + cli.stdout = cli.stdout.replace('[debug] watcher is ready\n', '') 198 193 } 199 194 else { 200 - await vitest.isDone 195 + await cli.isDone 201 196 } 202 197 203 - return vitest 198 + return cli 204 199 } 205 200 206 201 export async function runVitestCli(_options?: Options | string, ...args: string[]) { 202 + process.env.VITE_TEST_WATCHER_DEBUG = 'true' 207 203 return runCli('vitest', _options, ...args) 208 204 } 209 205 210 206 export async function runViteNodeCli(_options?: Options | string, ...args: string[]) { 211 - process.env.VITE_NODE_WATCHER_DEBUG = 'true' 207 + process.env.VITE_TEST_WATCHER_DEBUG = 'true' 212 208 return runCli('vite-node', _options, ...args) 213 209 } 214 210 215 211 const originalFiles = new Map<string, string>() 212 + const createdFiles = new Set<string>() 216 213 afterEach(() => { 217 214 originalFiles.forEach((content, file) => { 218 215 fs.writeFileSync(file, content, 'utf-8') 219 216 }) 217 + createdFiles.forEach((file) => { 218 + fs.unlinkSync(file) 219 + }) 220 + originalFiles.clear() 221 + createdFiles.clear() 220 222 }) 223 + 224 + export function createFile(file: string, content: string) { 225 + createdFiles.add(file) 226 + fs.writeFileSync(file, content, 'utf-8') 227 + } 221 228 222 229 export function editFile(file: string, callback: (content: string) => string) { 223 230 const content = fs.readFileSync(file, 'utf-8')
+1 -1
test/watch/vitest.config.ts
··· 9 9 }, 10 10 11 11 // For Windows CI mostly 12 - testTimeout: process.env.CI ? 30_000 : 10_000, 12 + testTimeout: process.env.CI ? 60_000 : 10_000, 13 13 14 14 // Test cases may have side effects, e.g. files under fixtures/ are modified on the fly to trigger file watchers 15 15 singleThread: true,
+2
test/reporters/tests/default.test.ts
··· 32 32 test('rerun should undo', async () => { 33 33 const vitest = await run([], true, '-t', 'passed') 34 34 35 + vitest.resetOutput() 36 + 35 37 // one file 36 38 vitest.write('p') 37 39 await vitest.waitForStdout('Input filename pattern')
+7
test/vite-node/src/hmr-script.js
··· 1 + console.error('Hello!') 2 + 3 + if (import.meta.hot) { 4 + import.meta.hot.accept(() => { 5 + console.error('Accept') 6 + }) 7 + }
-7
test/vite-node/src/script.js
··· 1 - console.error('Hello!') 2 - 3 - if (import.meta.hot) { 4 - import.meta.hot.accept(() => { 5 - console.error('Accept') 6 - }) 7 - }
+1 -1
test/vite-node/test/hmr.test.ts
··· 3 3 import { editFile, runViteNodeCli } from '../../test-utils' 4 4 5 5 test('hmr.accept works correctly', async () => { 6 - const scriptFile = resolve(__dirname, '../src/script.js') 6 + const scriptFile = resolve(__dirname, '../src/hmr-script.js') 7 7 8 8 const viteNode = await runViteNodeCli('--watch', scriptFile) 9 9
+8 -1
test/watch/test/file-watching.test.ts
··· 1 1 import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs' 2 2 import { afterEach, describe, expect, test } from 'vitest' 3 3 4 - import { runVitestCli } from '../../test-utils' 4 + import * as testUtils from '../../test-utils' 5 5 6 6 const sourceFile = 'fixtures/math.ts' 7 7 const sourceFileContent = readFileSync(sourceFile, 'utf-8') ··· 17 17 18 18 const cliArgs = ['--root', 'fixtures', '--watch'] 19 19 const cleanups: (() => void)[] = [] 20 + 21 + async function runVitestCli(...args: string[]) { 22 + const vitest = await testUtils.runVitestCli(...args) 23 + if (args.includes('--watch')) 24 + vitest.resetOutput() 25 + return vitest 26 + } 20 27 21 28 function editFile(fileContent: string) { 22 29 return `// Modified by file-watching.test.ts
+22
test/watch/test/related.test.ts
··· 1 + import { test } from 'vitest' 2 + import { resolve } from 'pathe' 3 + import { editFile, runVitestCli } from '../../test-utils' 4 + 5 + const cliArgs = ['--root', 'fixtures', '--watch', '--changed'] 6 + 7 + test('when nothing is changed, run nothing but keep watching', async () => { 8 + const vitest = await runVitestCli(...cliArgs) 9 + 10 + await vitest.waitForStdout('No affected test files found') 11 + await vitest.waitForStdout('Waiting for file changes...') 12 + 13 + editFile(resolve(__dirname, '../fixtures/math.ts'), content => `${content}\n\n`) 14 + 15 + await vitest.waitForStdout('RERUN ../math.ts') 16 + await vitest.waitForStdout('1 passed') 17 + 18 + editFile(resolve(__dirname, '../fixtures/math.test.ts'), content => `${content}\n\n`) 19 + 20 + await vitest.waitForStdout('RERUN ../math.test.ts') 21 + await vitest.waitForStdout('1 passed') 22 + })
+8 -1
test/watch/test/stdin.test.ts
··· 1 1 import { rmSync, writeFileSync } from 'node:fs' 2 2 import { afterEach, expect, test } from 'vitest' 3 3 4 - import { runVitestCli } from '../../test-utils' 4 + import * as testUtils from '../../test-utils' 5 + 6 + async function runVitestCli(...args: string[]) { 7 + const vitest = await testUtils.runVitestCli(...args) 8 + if (args.includes('--watch')) 9 + vitest.resetOutput() 10 + return vitest 11 + } 5 12 6 13 const cliArgs = ['--root', 'fixtures', '--watch'] 7 14 const cleanups: (() => void)[] = []
+1
test/watch/test/stdout.test.ts
··· 12 12 13 13 test('console.log is visible on test re-run', async () => { 14 14 const vitest = await runVitestCli('--root', 'fixtures', '--watch') 15 + vitest.resetOutput() 15 16 const testCase = ` 16 17 test('test with logging', () => { 17 18 console.log('First')
+4 -2
test/watch/test/workspaces.test.ts
··· 26 26 }) 27 27 ` 28 28 29 - function startVitest() { 30 - return runVitestCli( 29 + async function startVitest() { 30 + const vitest = await runVitestCli( 31 31 { cwd: root, env: { TEST_WATCH: 'true' } }, 32 32 '--root', 33 33 root, ··· 36 36 '--watch', 37 37 '--no-coverage', 38 38 ) 39 + vitest.resetOutput() 40 + return vitest 39 41 } 40 42 41 43 afterEach(() => {
+16 -2
packages/vite-node/src/hmr/emitter.ts
··· 30 30 return { 31 31 name: 'vite-node:hmr', 32 32 33 + config() { 34 + // chokidar fsevents is unstable on macos when emitting "ready" event 35 + if (process.platform === 'darwin' && process.env.VITE_TEST_WATCHER_DEBUG) { 36 + return { 37 + server: { 38 + watch: { 39 + useFsEvents: false, 40 + usePolling: false, 41 + }, 42 + }, 43 + } 44 + } 45 + }, 46 + 33 47 configureServer(server) { 34 48 const _send = server.ws.send 35 49 server.emitter = emitter ··· 37 51 _send(payload) 38 52 emitter.emit('message', payload) 39 53 } 40 - if (process.env.VITE_NODE_WATCHER_DEBUG) { 54 + if (process.env.VITE_TEST_WATCHER_DEBUG) { 41 55 server.watcher.on('ready', () => { 42 56 // eslint-disable-next-line no-console 43 - console.log('[vie-node] watcher is ready') 57 + console.log('[debug] watcher is ready') 44 58 }) 45 59 } 46 60 },
+28 -15
packages/vitest/src/node/core.ts
··· 306 306 await this.globTestFiles(filters), 307 307 ) 308 308 309 + // if run with --changed, don't exit if no tests are found 309 310 if (!files.length) { 310 - const exitCode = this.config.passWithNoTests ? 0 : 1 311 - 312 311 await this.reportCoverage(true) 312 + 313 313 this.logger.printNoTestFound(filters) 314 314 315 - process.exit(exitCode) 315 + if (!this.config.watch || !(this.config.changed || this.config.related?.length)) { 316 + const exitCode = this.config.passWithNoTests ? 0 : 1 317 + process.exit(exitCode) 318 + } 316 319 } 317 320 318 - // populate once, update cache on watch 319 - await this.cache.stats.populateStats(this.config.root, files) 321 + // all subsequent runs will treat this as a fresh run 322 + this.config.changed = false 323 + this.config.related = undefined 320 324 321 - await this.runFiles(files) 325 + if (files.length) { 326 + // populate once, update cache on watch 327 + await this.cache.stats.populateStats(this.config.root, files) 328 + 329 + await this.runFiles(files) 330 + } 322 331 323 332 await this.reportCoverage(true) 324 333 ··· 326 335 await this.report('onWatcherStart') 327 336 } 328 337 329 - private async getTestDependencies(filepath: WorkspaceSpec) { 330 - const deps = new Set<string>() 331 - 338 + private async getTestDependencies(filepath: WorkspaceSpec, deps = new Set<string>()) { 332 339 const addImports = async ([project, filepath]: WorkspaceSpec) => { 333 - const transformed = await project.vitenode.transformRequest(filepath) 340 + if (deps.has(filepath)) 341 + return 342 + const mod = project.server.moduleGraph.getModuleById(filepath) 343 + const transformed = mod?.ssrTransformResult || await project.vitenode.transformRequest(filepath) 334 344 if (!transformed) 335 345 return 336 346 const dependencies = [...transformed.deps || [], ...transformed.dynamicDeps || []] 337 - for (const dep of dependencies) { 347 + await Promise.all(dependencies.map(async (dep) => { 338 348 const path = await this.server.pluginContainer.resolveId(dep, filepath, { ssr: true }) 339 349 const fsPath = path && !path.external && path.id.split('?')[0] 340 350 if (fsPath && !fsPath.includes('node_modules') && !deps.has(fsPath) && existsSync(fsPath)) { ··· 342 352 343 353 await addImports([project, fsPath]) 344 354 } 345 - } 355 + })) 346 356 } 347 357 348 358 await addImports(filepath) ··· 373 383 return specs 374 384 375 385 // don't run anything if no related sources are found 376 - if (!related.length) 386 + // if we are in watch mode, we want to process all tests 387 + if (!this.config.watch && !related.length) 377 388 return [] 378 389 379 390 const testGraphs = await Promise.all( ··· 653 664 654 665 const files: string[] = [] 655 666 656 - for (const { server, browser } of projects) { 667 + for (const project of projects) { 668 + const { server, browser } = project 657 669 const mod = server.moduleGraph.getModuleById(id) || browser?.moduleGraph.getModuleById(id) 658 670 if (!mod) { 659 671 // files with `?v=` query from the browser ··· 675 687 676 688 this.invalidates.add(id) 677 689 678 - if (this.state.filesMap.has(id)) { 690 + // one of test files that we already run, or one of test files that we can run 691 + if (this.state.filesMap.has(id) || project.isTestFile(id)) { 679 692 this.changedTests.add(id) 680 693 files.push(id) 681 694 continue
+9 -4
packages/vitest/src/node/logger.ts
··· 98 98 if (config.watchExclude) 99 99 this.console.error(c.dim('watch exclude: ') + c.yellow(config.watchExclude.join(comma))) 100 100 101 - if (config.passWithNoTests) 102 - this.log(`No ${config.mode} files found, exiting with code 0\n`) 103 - else 104 - this.error(c.red(`\nNo ${config.mode} files found, exiting with code 1`)) 101 + if (config.watch && (config.changed || config.related?.length)) { 102 + this.log(`No affected ${config.mode} files found\n`) 103 + } 104 + else { 105 + if (config.passWithNoTests) 106 + this.log(`No ${config.mode} files found, exiting with code 0\n`) 107 + else 108 + this.error(c.red(`\nNo ${config.mode} files found, exiting with code 1`)) 109 + } 105 110 } 106 111 107 112 printBanner() {
+8
packages/vitest/src/node/workspace.ts
··· 73 73 closingPromise: Promise<unknown> | undefined 74 74 browserProvider: BrowserProvider | undefined 75 75 76 + testFilesList: string[] = [] 77 + 76 78 constructor( 77 79 public path: string | number, 78 80 public ctx: Vitest, ··· 132 134 })) 133 135 } 134 136 137 + this.testFilesList = testFiles 138 + 135 139 return testFiles 140 + } 141 + 142 + isTestFile(id: string) { 143 + return this.testFilesList.includes(id) 136 144 } 137 145 138 146 async globFiles(include: string[], exclude: string[], cwd: string) {
+12
packages/vitest/src/node/plugins/index.ts
··· 90 90 }, 91 91 } 92 92 93 + // chokidar fsevents is unstable on macos when emitting "ready" event 94 + if (process.platform === 'darwin' && process.env.VITE_TEST_WATCHER_DEBUG) { 95 + config.server!.watch!.useFsEvents = false 96 + config.server!.watch!.usePolling = false 97 + } 98 + 93 99 const classNameStrategy = (typeof testConfig.css !== 'boolean' && testConfig.css?.modules?.classNameStrategy) || 'stable' 94 100 95 101 if (classNameStrategy !== 'scoped') { ··· 154 160 } 155 161 }, 156 162 async configureServer(server) { 163 + if (options.watch && process.env.VITE_TEST_WATCHER_DEBUG) { 164 + server.watcher.on('ready', () => { 165 + // eslint-disable-next-line no-console 166 + console.log('[debug] watcher is ready') 167 + }) 168 + } 157 169 try { 158 170 await ctx.setServer(options, server, userConfig) 159 171 if (options.api && options.watch)