Skip to content

Commit

Permalink
test: editing file in a workspace reruns tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Apr 8, 2023
1 parent bde49db commit 9a35fd0
Show file tree
Hide file tree
Showing 15 changed files with 150 additions and 33 deletions.
50 changes: 30 additions & 20 deletions packages/vitest/src/node/core.ts
@@ -1,6 +1,6 @@
import { existsSync, promises as fs } from 'node:fs'
import type { ViteDevServer } from 'vite'
import { basename, dirname, normalize, relative } from 'pathe'
import { basename, dirname, join, normalize, relative } from 'pathe'
import fg from 'fast-glob'
import mm from 'micromatch'
import c from 'picocolors'
Expand Down Expand Up @@ -151,20 +151,23 @@ export class Vitest {
}

private async resolveWorkspaces(options: UserConfig) {
const rootFiles = await fs.readdir(this.config.root)
const workspacesConfigPath = workspacesFiles.find((configFile) => {
const configDir = dirname(this.server.config.configFile || this.config.root)
const rootFiles = await fs.readdir(configDir)
const workspacesConfigName = workspacesFiles.find((configFile) => {
return rootFiles.includes(configFile)
})

if (!workspacesConfigPath)
if (!workspacesConfigName)
return [await this.createCoreWorkspace(options)]

const workspacesModule = await this.runner.executeFile(workspacesConfigPath) as {
const workspacesConcigPath = join(configDir, workspacesConfigName)

const workspacesModule = await this.runner.executeFile(workspacesConcigPath) as {
default: string[]
}

if (!workspacesModule.default || !Array.isArray(workspacesModule.default))
throw new Error(`Workspaces config file ${workspacesConfigPath} must export a default array of workspace paths`)
throw new Error(`Workspaces config file ${workspacesConcigPath} must export a default array of workspace paths`)

const workspacesGlobMatches = workspacesModule.default.map((workspacePath) => {
return workspacePath.replace('<rootDir>', this.config.root)
Expand Down Expand Up @@ -453,7 +456,7 @@ export class Vitest {
}

private _rerunTimer: any
private async scheduleRerun(triggerId: string) {
private async scheduleRerun(triggerId: string[]) {
const currentCount = this.restartsCount
clearTimeout(this._rerunTimer)
await this.runningPromise
Expand Down Expand Up @@ -492,7 +495,9 @@ export class Vitest {
if (this.coverageProvider && this.config.coverage.cleanOnRerun)
await this.coverageProvider.clean()

await this.report('onWatcherRerun', files, triggerId)
const triggerIds = new Set(triggerId.map(id => relative(this.config.root, id)))
const triggerLabel = Array.from(triggerIds).join(', ')
await this.report('onWatcherRerun', files, triggerLabel)

await this.runFiles(files.flatMap(file => this.getWorkspacesByTestFile(file)))

Expand Down Expand Up @@ -525,8 +530,8 @@ export class Vitest {
id = slash(id)
updateLastChanged(id)
const needsRerun = this.handleFileChanged(id)
if (needsRerun)
this.scheduleRerun(id)
if (needsRerun.length)
this.scheduleRerun(needsRerun)
}
const onUnlink = (id: string) => {
id = slash(id)
Expand All @@ -546,7 +551,7 @@ export class Vitest {
if (await this.isTargetFile(id)) {
this.changedTests.add(id)
await this.cache.stats.updateStats(id)
this.scheduleRerun(id)
this.scheduleRerun([id])
}
}
const watcher = this.server.watcher
Expand All @@ -571,32 +576,36 @@ export class Vitest {
/**
* @returns A value indicating whether rerun is needed (changedTests was mutated)
*/
private handleFileChanged(id: string): boolean {
private handleFileChanged(id: string): string[] {
if (this.changedTests.has(id) || this.invalidates.has(id))
return false
return []

if (mm.isMatch(id, this.config.forceRerunTriggers)) {
this.state.getFilepaths().forEach(file => this.changedTests.add(file))
return true
return []
}

const workspaces = this.getModuleWorkspaces(id)
if (!workspaces.length)
return false
return []

const files: string[] = []

for (const { server, browser } of workspaces) {
const mod = server.moduleGraph.getModuleById(id) || browser?.moduleGraph.getModuleById(id)
if (!mod) {
// files with `?v=` query from the browser
const mods = browser?.moduleGraph.getModulesByFile(id)
if (!mods?.size)
return false
return []
let rerun = false
mods.forEach((m) => {
if (m.id && this.handleFileChanged(m.id))
rerun = true
})
return rerun
if (rerun)
files.push(id)
continue
}

// remove queries from id
Expand All @@ -606,7 +615,8 @@ export class Vitest {

if (this.state.filesMap.has(id)) {
this.changedTests.add(id)
return true
files.push(id)
continue
}

let rerun = false
Expand All @@ -620,10 +630,10 @@ export class Vitest {
})

if (rerun)
return rerun
files.push(id)
}

return false
return files
}

private async reportCoverage(allTestsRun: boolean) {
Expand Down
6 changes: 5 additions & 1 deletion packages/vitest/src/node/reporters/base.ts
Expand Up @@ -80,7 +80,11 @@ export abstract class BaseReporter implements Reporter {
if (this.ctx.config.logHeapUsage && task.result.heap != null)
suffix += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`)

logger.log(` ${getStateSymbol(task)} ${task.name} ${suffix}`)
let title = ` ${getStateSymbol(task)} `
if (task.projectName)
title += formatProjectName(task.projectName)
title += `${task.name} ${suffix}`
logger.log(title)

// print short errors, full errors will be at the end in summary
for (const test of failed) {
Expand Down
7 changes: 5 additions & 2 deletions packages/vitest/src/node/reporters/verbose.ts
Expand Up @@ -3,7 +3,7 @@ import type { TaskResultPack } from '../../types'
import { getFullName } from '../../utils'
import { F_RIGHT } from '../../utils/figures'
import { DefaultReporter } from './default'
import { getStateSymbol } from './renderers/utils'
import { formatProjectName, getStateSymbol } from './renderers/utils'

export class VerboseReporter extends DefaultReporter {
constructor() {
Expand All @@ -17,7 +17,10 @@ export class VerboseReporter extends DefaultReporter {
for (const pack of packs) {
const task = this.ctx.state.idMap.get(pack[0])
if (task && task.type === 'test' && task.result?.state && task.result?.state !== 'run') {
let title = ` ${getStateSymbol(task)} ${getFullName(task, c.dim(' > '))}`
let title = ` ${getStateSymbol(task)} `
if (task.suite?.projectName)
title += formatProjectName(task.suite.projectName)
title += getFullName(task, c.dim(' > '))
if (this.ctx.config.logHeapUsage && task.result.heap != null)
title += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`)
this.ctx.logger.log(title)
Expand Down
6 changes: 3 additions & 3 deletions packages/vitest/src/node/workspace.ts
Expand Up @@ -66,8 +66,8 @@ export class VitestWorkspace {
public ctx: Vitest,
) { }

getName() {
return this.config.name
getName(): string {
return this.config.name || dirname(this.path).split('/').pop() || ''
}

isCore() {
Expand Down Expand Up @@ -228,7 +228,7 @@ export class VitestWorkspace {
},
},
snapshotOptions: {
...this.ctx.config.snapshotOptions,
...this.config.snapshotOptions,
resolveSnapshotPath: undefined,
},
onConsoleLog: undefined!,
Expand Down
6 changes: 3 additions & 3 deletions test/watch/test/file-watching.test.ts
Expand Up @@ -31,7 +31,7 @@ test('editing source file triggers re-run', async () => {
writeFileSync(sourceFile, editFile(sourceFileContent), 'utf8')

await vitest.waitForOutput('New code running')
await vitest.waitForOutput('RERUN math.ts')
await vitest.waitForOutput('RERUN ../math.ts')
await vitest.waitForOutput('1 passed')
})

Expand All @@ -41,7 +41,7 @@ test('editing test file triggers re-run', async () => {
writeFileSync(testFile, editFile(testFileContent), 'utf8')

await vitest.waitForOutput('New code running')
await vitest.waitForOutput('RERUN math.test.ts')
await vitest.waitForOutput('RERUN ../math.test.ts')
await vitest.waitForOutput('1 passed')
})

Expand Down Expand Up @@ -71,7 +71,7 @@ describe('browser', () => {
writeFileSync(sourceFile, editFile(sourceFileContent), 'utf8')

await vitest.waitForOutput('New code running')
await vitest.waitForOutput('RERUN math.ts')
await vitest.waitForOutput('RERUN ../math.ts')
await vitest.waitForOutput('1 passed')

vitest.write('q')
Expand Down
9 changes: 6 additions & 3 deletions test/watch/test/utils.ts
@@ -1,9 +1,12 @@
import { afterEach } from 'vitest'
import { execa } from 'execa'
import { type Options, execa } from 'execa'
import stripAnsi from 'strip-ansi'

export async function startWatchMode(...args: string[]) {
const subprocess = execa('vitest', ['--root', 'fixtures', ...args])
export async function startWatchMode(options?: Options | string, ...args: string[]) {
if (typeof options === 'string')
args.unshift(options)
const argsWithRoot = args.includes('--root') ? args : ['--root', 'fixtures', ...args]
const subprocess = execa('vitest', argsWithRoot, typeof options === 'string' ? undefined : options)

let setDone: (value?: unknown) => void
const isDone = new Promise(resolve => (setDone = resolve))
Expand Down
66 changes: 66 additions & 0 deletions test/watch/test/workspaces.test.ts
@@ -0,0 +1,66 @@
import { fileURLToPath } from 'node:url'
import { readFileSync, writeFileSync } from 'node:fs'
import { afterAll, it } from 'vitest'
import { dirname, resolve } from 'pathe'
import { startWatchMode } from './utils'

const file = fileURLToPath(import.meta.url)
const dir = dirname(file)
const root = resolve(dir, '..', '..', 'workspaces')
const config = resolve(root, 'vitest.config.ts')

const srcMathFile = resolve(root, 'src', 'math.ts')
const specSpace2File = resolve(root, 'space_2', 'test', 'node.spec.ts')

const srcMathContent = readFileSync(srcMathFile, 'utf-8')
const specSpace2Content = readFileSync(specSpace2File, 'utf-8')

function startVitest() {
return startWatchMode(
{ cwd: root, env: { TEST_WATCH: 'true' } },
'--root',
root,
'--config',
config,
'--no-coverage',
)
}

afterAll(() => {
writeFileSync(srcMathFile, srcMathContent, 'utf8')
writeFileSync(specSpace2File, specSpace2Content, 'utf8')
})

it('editing a test file in a suite with workspaces reruns test', async () => {
const vitest = await startVitest()

writeFileSync(specSpace2File, `${specSpace2Content}\n`, 'utf8')

await vitest.waitForOutput('RERUN space_2/test/node.spec.ts x1')
await vitest.waitForOutput('|space_2| test/node.spec.ts')
await vitest.waitForOutput('Test Files 1 passed')
})

it('editing a file that is imported in different workspaces reruns both files', async () => {
const vitest = await startVitest()

writeFileSync(srcMathFile, `${srcMathContent}\n`, 'utf8')

await vitest.waitForOutput('RERUN src/math.ts')
await vitest.waitForOutput('|space_3| math.space-test.ts')
await vitest.waitForOutput('|space_1| test/math.spec.ts')
await vitest.waitForOutput('Test Files 2 passed')
})

it('filters by test name inside a workspace', async () => {
const vitest = await startVitest()

vitest.write('t')

await vitest.waitForOutput('Input test name pattern')

vitest.write('2 x 2 = 4\n')

await vitest.waitForOutput('Test name pattern: /2 x 2 = 4/')
await vitest.waitForOutput('Test Files 1 passed')
})
2 changes: 1 addition & 1 deletion test/watch/vitest.config.ts
Expand Up @@ -6,7 +6,7 @@ export default defineConfig({
include: ['test/**/*.test.*'],

// For Windows CI mostly
testTimeout: process.env.CI ? 30_000 : 5_000,
testTimeout: process.env.CI ? 30_000 : 10_000,

// Test cases may have side effects, e.g. files under fixtures/ are modified on the fly to trigger file watchers
singleThread: true,
Expand Down
6 changes: 6 additions & 0 deletions test/workspaces/space_1/test/math.spec.ts
@@ -0,0 +1,6 @@
import { expect, test } from 'vitest'
import { sum } from '../../src/math'

test('3 + 3 = 6', () => {
expect(sum(3, 3)).toBe(6)
})
5 changes: 5 additions & 0 deletions test/workspaces/space_3/math.space-test.ts
@@ -1,6 +1,11 @@
import { expect, test } from 'vitest'
import { sum } from '../src/math'
import { multiple } from './src/multiply'

test('2 x 2 = 4', () => {
expect(multiple(2, 2)).toBe(4)
})

test('2 + 2 = 4', () => {
expect(sum(2, 2)).toBe(4)
})
1 change: 1 addition & 0 deletions test/workspaces/space_shared/setup.jsdom.ts
@@ -0,0 +1 @@
Object.defineProperty(global, 'testValue', { value: 'jsdom' })
1 change: 1 addition & 0 deletions test/workspaces/space_shared/setup.node.ts
@@ -0,0 +1 @@
Object.defineProperty(global, 'testValue', { value: 'node' })
9 changes: 9 additions & 0 deletions test/workspaces/space_shared/test.spec.ts
@@ -0,0 +1,9 @@
import { expect, it } from 'vitest'

declare global {
const testValue: string
}

it('the same file works with different workspaces', () => {
expect(testValue).toBe(expect.getState().environment === 'node' ? 'node' : 'jsdom')
})
3 changes: 3 additions & 0 deletions test/workspaces/src/math.ts
@@ -0,0 +1,3 @@
export function sum(a: number, b: number) {
return a + b
}
6 changes: 6 additions & 0 deletions test/workspaces/vitest.config.ts
@@ -1,5 +1,11 @@
import { defineConfig } from 'vitest/config'

if (process.env.TEST_WATCH) {
// Patch stdin on the process so that we can fake it to seem like a real interactive terminal and pass the TTY checks
process.stdin.isTTY = true
process.stdin.setRawMode = () => process.stdin
}

export default defineConfig({
test: {
coverage: {
Expand Down

0 comments on commit 9a35fd0

Please sign in to comment.