Skip to content

Commit

Permalink
feat: running vitest with --related --watch reruns non-affected tes…
Browse files Browse the repository at this point in the history
…ts if they were changed during a run (#3844)
  • Loading branch information
sheremet-va committed Jul 30, 2023
1 parent 2e5847e commit c9aeac4
Show file tree
Hide file tree
Showing 15 changed files with 147 additions and 47 deletions.
18 changes: 16 additions & 2 deletions packages/vite-node/src/hmr/emitter.ts
Expand Up @@ -30,17 +30,31 @@ export function viteNodeHmrPlugin(): Plugin {
return {
name: 'vite-node:hmr',

config() {
// chokidar fsevents is unstable on macos when emitting "ready" event
if (process.platform === 'darwin' && process.env.VITE_TEST_WATCHER_DEBUG) {
return {
server: {
watch: {
useFsEvents: false,
usePolling: false,
},
},
}
}
},

configureServer(server) {
const _send = server.ws.send
server.emitter = emitter
server.ws.send = function (payload: HMRPayload) {
_send(payload)
emitter.emit('message', payload)
}
if (process.env.VITE_NODE_WATCHER_DEBUG) {
if (process.env.VITE_TEST_WATCHER_DEBUG) {
server.watcher.on('ready', () => {
// eslint-disable-next-line no-console
console.log('[vie-node] watcher is ready')
console.log('[debug] watcher is ready')
})
}
},
Expand Down
43 changes: 28 additions & 15 deletions packages/vitest/src/node/core.ts
Expand Up @@ -306,43 +306,53 @@ export class Vitest {
await this.globTestFiles(filters),
)

// if run with --changed, don't exit if no tests are found
if (!files.length) {
const exitCode = this.config.passWithNoTests ? 0 : 1

await this.reportCoverage(true)

this.logger.printNoTestFound(filters)

process.exit(exitCode)
if (!this.config.watch || !(this.config.changed || this.config.related?.length)) {
const exitCode = this.config.passWithNoTests ? 0 : 1
process.exit(exitCode)
}
}

// populate once, update cache on watch
await this.cache.stats.populateStats(this.config.root, files)
// all subsequent runs will treat this as a fresh run
this.config.changed = false
this.config.related = undefined

await this.runFiles(files)
if (files.length) {
// populate once, update cache on watch
await this.cache.stats.populateStats(this.config.root, files)

await this.runFiles(files)
}

await this.reportCoverage(true)

if (this.config.watch)
await this.report('onWatcherStart')
}

private async getTestDependencies(filepath: WorkspaceSpec) {
const deps = new Set<string>()

private async getTestDependencies(filepath: WorkspaceSpec, deps = new Set<string>()) {
const addImports = async ([project, filepath]: WorkspaceSpec) => {
const transformed = await project.vitenode.transformRequest(filepath)
if (deps.has(filepath))
return
const mod = project.server.moduleGraph.getModuleById(filepath)
const transformed = mod?.ssrTransformResult || await project.vitenode.transformRequest(filepath)
if (!transformed)
return
const dependencies = [...transformed.deps || [], ...transformed.dynamicDeps || []]
for (const dep of dependencies) {
await Promise.all(dependencies.map(async (dep) => {
const path = await this.server.pluginContainer.resolveId(dep, filepath, { ssr: true })
const fsPath = path && !path.external && path.id.split('?')[0]
if (fsPath && !fsPath.includes('node_modules') && !deps.has(fsPath) && existsSync(fsPath)) {
deps.add(fsPath)

await addImports([project, fsPath])
}
}
}))
}

await addImports(filepath)
Expand Down Expand Up @@ -373,7 +383,8 @@ export class Vitest {
return specs

// don't run anything if no related sources are found
if (!related.length)
// if we are in watch mode, we want to process all tests
if (!this.config.watch && !related.length)
return []

const testGraphs = await Promise.all(
Expand Down Expand Up @@ -653,7 +664,8 @@ export class Vitest {

const files: string[] = []

for (const { server, browser } of projects) {
for (const project of projects) {
const { server, browser } = project
const mod = server.moduleGraph.getModuleById(id) || browser?.moduleGraph.getModuleById(id)
if (!mod) {
// files with `?v=` query from the browser
Expand All @@ -675,7 +687,8 @@ export class Vitest {

this.invalidates.add(id)

if (this.state.filesMap.has(id)) {
// one of test files that we already run, or one of test files that we can run
if (this.state.filesMap.has(id) || project.isTestFile(id)) {
this.changedTests.add(id)
files.push(id)
continue
Expand Down
13 changes: 9 additions & 4 deletions packages/vitest/src/node/logger.ts
Expand Up @@ -98,10 +98,15 @@ export class Logger {
if (config.watchExclude)
this.console.error(c.dim('watch exclude: ') + c.yellow(config.watchExclude.join(comma)))

if (config.passWithNoTests)
this.log(`No ${config.mode} files found, exiting with code 0\n`)
else
this.error(c.red(`\nNo ${config.mode} files found, exiting with code 1`))
if (config.watch && (config.changed || config.related?.length)) {
this.log(`No affected ${config.mode} files found\n`)
}
else {
if (config.passWithNoTests)
this.log(`No ${config.mode} files found, exiting with code 0\n`)
else
this.error(c.red(`\nNo ${config.mode} files found, exiting with code 1`))
}
}

printBanner() {
Expand Down
12 changes: 12 additions & 0 deletions packages/vitest/src/node/plugins/index.ts
Expand Up @@ -90,6 +90,12 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t
},
}

// chokidar fsevents is unstable on macos when emitting "ready" event
if (process.platform === 'darwin' && process.env.VITE_TEST_WATCHER_DEBUG) {
config.server!.watch!.useFsEvents = false
config.server!.watch!.usePolling = false
}

const classNameStrategy = (typeof testConfig.css !== 'boolean' && testConfig.css?.modules?.classNameStrategy) || 'stable'

if (classNameStrategy !== 'scoped') {
Expand Down Expand Up @@ -154,6 +160,12 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t
}
},
async configureServer(server) {
if (options.watch && process.env.VITE_TEST_WATCHER_DEBUG) {
server.watcher.on('ready', () => {
// eslint-disable-next-line no-console
console.log('[debug] watcher is ready')
})
}
try {
await ctx.setServer(options, server, userConfig)
if (options.api && options.watch)
Expand Down
8 changes: 8 additions & 0 deletions packages/vitest/src/node/workspace.ts
Expand Up @@ -73,6 +73,8 @@ export class WorkspaceProject {
closingPromise: Promise<unknown> | undefined
browserProvider: BrowserProvider | undefined

testFilesList: string[] = []

constructor(
public path: string | number,
public ctx: Vitest,
Expand Down Expand Up @@ -132,9 +134,15 @@ export class WorkspaceProject {
}))
}

this.testFilesList = testFiles

return testFiles
}

isTestFile(id: string) {
return this.testFilesList.includes(id)
}

async globFiles(include: string[], exclude: string[], cwd: string) {
const globOptions: fg.Options = {
absolute: true,
Expand Down
2 changes: 2 additions & 0 deletions test/reporters/tests/default.test.ts
Expand Up @@ -32,6 +32,8 @@ describe('default reporter', async () => {
test('rerun should undo', async () => {
const vitest = await run([], true, '-t', 'passed')

vitest.resetOutput()

// one file
vitest.write('p')
await vitest.waitForStdout('Input filename pattern')
Expand Down
47 changes: 27 additions & 20 deletions test/test-utils/index.ts
Expand Up @@ -100,7 +100,7 @@ export async function runCli(command: string, _options?: Options | string, ...ar
let setDone: (value?: unknown) => void
const isDone = new Promise(resolve => (setDone = resolve))

const vitest = {
const cli = {
stdout: '',
stderr: '',
stdoutListeners: [] as (() => void)[],
Expand Down Expand Up @@ -165,13 +165,13 @@ export async function runCli(command: string, _options?: Options | string, ...ar
}

subprocess.stdout!.on('data', (data) => {
vitest.stdout += stripAnsi(data.toString())
vitest.stdoutListeners.forEach(fn => fn())
cli.stdout += stripAnsi(data.toString())
cli.stdoutListeners.forEach(fn => fn())
})

subprocess.stderr!.on('data', (data) => {
vitest.stderr += stripAnsi(data.toString())
vitest.stderrListeners.forEach(fn => fn())
cli.stderr += stripAnsi(data.toString())
cli.stderrListeners.forEach(fn => fn())
})

subprocess.on('exit', () => setDone())
Expand All @@ -181,44 +181,51 @@ export async function runCli(command: string, _options?: Options | string, ...ar
if (subprocess.exitCode === null)
subprocess.kill()

await vitest.isDone
await cli.isDone
})

if (command !== 'vitest') {
if (!args.includes('--watch'))
await vitest.isDone
else
await vitest.waitForStdout('[vie-node] watcher is ready')
return vitest
}

if (args.includes('--watch')) { // Wait for initial test run to complete
await vitest.waitForStdout('Waiting for file changes')
vitest.resetOutput()
if (args.includes('--watch')) {
if (command === 'vitest') // Wait for initial test run to complete
await cli.waitForStdout('Waiting for file changes')
// make sure watcher is ready
await cli.waitForStdout('[debug] watcher is ready')
cli.stdout = cli.stdout.replace('[debug] watcher is ready\n', '')
}
else {
await vitest.isDone
await cli.isDone
}

return vitest
return cli
}

export async function runVitestCli(_options?: Options | string, ...args: string[]) {
process.env.VITE_TEST_WATCHER_DEBUG = 'true'
return runCli('vitest', _options, ...args)
}

export async function runViteNodeCli(_options?: Options | string, ...args: string[]) {
process.env.VITE_NODE_WATCHER_DEBUG = 'true'
process.env.VITE_TEST_WATCHER_DEBUG = 'true'
return runCli('vite-node', _options, ...args)
}

const originalFiles = new Map<string, string>()
const createdFiles = new Set<string>()
afterEach(() => {
originalFiles.forEach((content, file) => {
fs.writeFileSync(file, content, 'utf-8')
})
createdFiles.forEach((file) => {
fs.unlinkSync(file)
})
originalFiles.clear()
createdFiles.clear()
})

export function createFile(file: string, content: string) {
createdFiles.add(file)
fs.writeFileSync(file, content, 'utf-8')
}

export function editFile(file: string, callback: (content: string) => string) {
const content = fs.readFileSync(file, 'utf-8')
if (!originalFiles.has(file))
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion test/vite-node/test/hmr.test.ts
Expand Up @@ -3,7 +3,7 @@ import { resolve } from 'pathe'
import { editFile, runViteNodeCli } from '../../test-utils'

test('hmr.accept works correctly', async () => {
const scriptFile = resolve(__dirname, '../src/script.js')
const scriptFile = resolve(__dirname, '../src/hmr-script.js')

const viteNode = await runViteNodeCli('--watch', scriptFile)

Expand Down
9 changes: 8 additions & 1 deletion test/watch/test/file-watching.test.ts
@@ -1,7 +1,7 @@
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { afterEach, describe, expect, test } from 'vitest'

import { runVitestCli } from '../../test-utils'
import * as testUtils from '../../test-utils'

const sourceFile = 'fixtures/math.ts'
const sourceFileContent = readFileSync(sourceFile, 'utf-8')
Expand All @@ -18,6 +18,13 @@ const forceTriggerFileContent = readFileSync(forceTriggerFile, 'utf-8')
const cliArgs = ['--root', 'fixtures', '--watch']
const cleanups: (() => void)[] = []

async function runVitestCli(...args: string[]) {
const vitest = await testUtils.runVitestCli(...args)
if (args.includes('--watch'))
vitest.resetOutput()
return vitest
}

function editFile(fileContent: string) {
return `// Modified by file-watching.test.ts
${fileContent}
Expand Down
22 changes: 22 additions & 0 deletions test/watch/test/related.test.ts
@@ -0,0 +1,22 @@
import { test } from 'vitest'
import { resolve } from 'pathe'
import { editFile, runVitestCli } from '../../test-utils'

const cliArgs = ['--root', 'fixtures', '--watch', '--changed']

test('when nothing is changed, run nothing but keep watching', async () => {
const vitest = await runVitestCli(...cliArgs)

await vitest.waitForStdout('No affected test files found')
await vitest.waitForStdout('Waiting for file changes...')

editFile(resolve(__dirname, '../fixtures/math.ts'), content => `${content}\n\n`)

await vitest.waitForStdout('RERUN ../math.ts')
await vitest.waitForStdout('1 passed')

editFile(resolve(__dirname, '../fixtures/math.test.ts'), content => `${content}\n\n`)

await vitest.waitForStdout('RERUN ../math.test.ts')
await vitest.waitForStdout('1 passed')
})
9 changes: 8 additions & 1 deletion test/watch/test/stdin.test.ts
@@ -1,7 +1,14 @@
import { rmSync, writeFileSync } from 'node:fs'
import { afterEach, expect, test } from 'vitest'

import { runVitestCli } from '../../test-utils'
import * as testUtils from '../../test-utils'

async function runVitestCli(...args: string[]) {
const vitest = await testUtils.runVitestCli(...args)
if (args.includes('--watch'))
vitest.resetOutput()
return vitest
}

const cliArgs = ['--root', 'fixtures', '--watch']
const cleanups: (() => void)[] = []
Expand Down
1 change: 1 addition & 0 deletions test/watch/test/stdout.test.ts
Expand Up @@ -12,6 +12,7 @@ afterEach(() => {

test('console.log is visible on test re-run', async () => {
const vitest = await runVitestCli('--root', 'fixtures', '--watch')
vitest.resetOutput()
const testCase = `
test('test with logging', () => {
console.log('First')
Expand Down

0 comments on commit c9aeac4

Please sign in to comment.