Skip to content

Commit

Permalink
feat: support standalone mode (#5565)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored May 2, 2024
1 parent b9a411d commit bdce0a2
Show file tree
Hide file tree
Showing 14 changed files with 187 additions and 73 deletions.
7 changes: 4 additions & 3 deletions docs/guide/cli-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
| `--coverage.cleanOnRerun` | Clean coverage report on watch rerun (default: true) |
| `--coverage.reportsDirectory <path>` | Directory to write coverage report to (default: ./coverage) |
| `--coverage.reporter <name>` | Coverage reporters to use. Visit [`coverage.reporter`](https://vitest.dev/config/#coverage-reporter) for more information (default: `["text", "html", "clover", "json"]`) |
| `--coverage.reportOnFailure` | Generate coverage report even when tests fail (default: false) |
| `--coverage.allowExternal` | Collect coverage of files outside the project root (default: false) |
| `--coverage.skipFull` | Do not show files with 100% statement, branch, and function coverage (default: false) |
| `--coverage.reportOnFailure` | Generate coverage report even when tests fail (default: `false`) |
| `--coverage.allowExternal` | Collect coverage of files outside the project root (default: `false`) |
| `--coverage.skipFull` | Do not show files with 100% statement, branch, and function coverage (default: `false`) |
| `--coverage.thresholds.100` | Shortcut to set all coverage thresholds to 100 (default: `false`) |
| `--coverage.thresholds.perFile` | Check thresholds per file. See `--coverage.thresholds.lines`, `--coverage.thresholds.functions`, `--coverage.thresholds.branches` and `--coverage.thresholds.statements` for the actual thresholds (default: `false`) |
| `--coverage.thresholds.autoUpdate` | Update threshold values: "lines", "functions", "branches" and "statements" to configuration file when current coverage is above the configured thresholds (default: `false`) |
Expand Down Expand Up @@ -119,3 +119,4 @@
| `--segfaultRetry <times>` | Retry the test suite if it crashes due to a segfault (default: `true`) |
| `--no-color` | Removes colors from the console output |
| `--clearScreen` | Clear terminal screen when re-running tests during watch mode (default: `true`) |
| `--standalone` | Start Vitest without running tests. File filters will be ignored, tests will be running only on change (default: `false`) |
26 changes: 25 additions & 1 deletion packages/ui/client/composables/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { WebSocketStatus } from '@vueuse/core'
import type { ErrorWithDiff, File, ResolvedConfig } from 'vitest'
import type { Ref } from 'vue'
import { reactive } from 'vue'
import { relative } from 'pathe'
import { generateHash } from '@vitest/runner/utils'
import type { RunState } from '../../../types'
import { ENTRY_URL, isReport } from '../../constants'
import { parseError } from '../error'
Expand Down Expand Up @@ -84,7 +86,29 @@ watch(
client.rpc.getConfig(),
client.rpc.getUnhandledErrors(),
])
client.state.collectFiles(files)
if (_config.standalone) {
const filenames = await client.rpc.getTestFiles()
const files = filenames.map<File>(([name, filepath]) => {
const path = relative(_config.root, filepath)
return {
filepath,
name: path,
id: /* #__PURE__ */ generateHash(`${path}${name || ''}`),
mode: 'skip',
type: 'suite',
result: {
state: 'skip',
},
meta: {},
tasks: [],
projectName: name,
}
})
client.state.collectFiles(files)
}
else {
client.state.collectFiles(files)
}
unhandledErrors.value = (errors || []).map(parseError)
config.value = _config
})
Expand Down
4 changes: 4 additions & 0 deletions packages/vitest/src/api/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ export function setup(vitestOrWorkspace: Vitest | WorkspaceProject, _server?: Vi
getProvidedContext() {
return 'ctx' in vitestOrWorkspace ? vitestOrWorkspace.getProvidedContext() : ({} as any)
},
async getTestFiles() {
const spec = await ctx.globTestFiles()
return spec.map(([project, file]) => [project.getName(), file]) as [string, string][]
},
},
{
post: msg => ws.send(msg),
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface WebSocketHandlers {
getCountOfFailedTests: () => number
sendLog: (log: UserConsoleLog) => void
getFiles: () => File[]
getTestFiles: () => Promise<[name: string, file: string][]>
getPaths: () => string[]
getConfig: () => ResolvedConfig
resolveSnapshotPath: (testPath: string) => string
Expand Down
10 changes: 8 additions & 2 deletions packages/vitest/src/node/cli/cli-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,17 @@ export async function startVitest(
})

ctx.onAfterSetServer(() => {
ctx.start(cliFilters)
if (ctx.config.standalone)
ctx.init()
else
ctx.start(cliFilters)
})

try {
await ctx.start(cliFilters)
if (ctx.config.standalone)
await ctx.init()
else
await ctx.start(cliFilters)
}
catch (e) {
process.exitCode = 1
Expand Down
9 changes: 6 additions & 3 deletions packages/vitest/src/node/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,13 +202,13 @@ export const cliOptionsConfig: VitestCLIOptions = {
array: true,
},
reportOnFailure: {
description: 'Generate coverage report even when tests fail (default: false)',
description: 'Generate coverage report even when tests fail (default: `false`)',
},
allowExternal: {
description: 'Collect coverage of files outside the project root (default: false)',
description: 'Collect coverage of files outside the project root (default: `false`)',
},
skipFull: {
description: 'Do not show files with 100% statement, branch, and function coverage (default: false)',
description: 'Do not show files with 100% statement, branch, and function coverage (default: `false`)',
},
thresholds: {
description: null,
Expand Down Expand Up @@ -601,6 +601,9 @@ export const cliOptionsConfig: VitestCLIOptions = {
clearScreen: {
description: 'Clear terminal screen when re-running tests during watch mode (default: `true`)',
},
standalone: {
description: 'Start Vitest without running tests. File filters will be ignored, tests will be running only on change (default: `false`)',
},

// disable CLI options
cliExclude: null,
Expand Down
3 changes: 3 additions & 0 deletions packages/vitest/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ export function resolveConfig(
resolved.shard = { index, count }
}

if (resolved.standalone && !resolved.watch)
throw new Error(`Vitest standalone mode requires --watch`)

if (resolved.maxWorkers)
resolved.maxWorkers = Number(resolved.maxWorkers)

Expand Down
45 changes: 40 additions & 5 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { CancelReason, File } from '@vitest/runner'
import { ViteNodeServer } from 'vite-node/server'
import type { defineWorkspace } from 'vitest/config'
import type { ArgumentsType, CoverageProvider, OnServerRestartHandler, Reporter, ResolvedConfig, UserConfig, UserWorkspaceConfig, VitestRunMode } from '../types'
import { hasFailed, noop, slash, toArray, wildcardPatternToRegExp } from '../utils'
import { getTasks, hasFailed, noop, slash, toArray, wildcardPatternToRegExp } from '../utils'
import { getCoverageProvider } from '../integrations/coverage'
import { CONFIG_NAMES, configFiles, workspacesFiles as workspaceFiles } from '../constants'
import { rootDir } from '../paths'
Expand Down Expand Up @@ -418,6 +418,25 @@ export class Vitest {
await this.report('onWatcherStart')
}

async init() {
this._onClose = []

try {
await this.initCoverageProvider()
await this.coverageProvider?.clean(this.config.coverage.clean)
await this.initBrowserProviders()
}
finally {
await this.report('onInit', this)
}

// populate test files cache so watch mode can trigger a file rerun
await this.globTestFiles()

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

private async getTestDependencies(filepath: WorkspaceSpec, deps = new Set<string>()) {
const addImports = async ([project, filepath]: WorkspaceSpec) => {
if (deps.has(filepath))
Expand Down Expand Up @@ -607,14 +626,24 @@ export class Vitest {
if (pattern === '')
this.filenamePattern = undefined

this.configOverride.testNamePattern = pattern ? new RegExp(pattern) : undefined
const testNamePattern = pattern ? new RegExp(pattern) : undefined
this.configOverride.testNamePattern = testNamePattern
// filter only test files that have tests matching the pattern
if (testNamePattern) {
files = files.filter((filepath) => {
const files = this.state.getFiles([filepath])
return !files.length || files.some((file) => {
const tasks = getTasks(file)
return !tasks.length || tasks.some(task => testNamePattern.test(task.name))
})
})
}
await this.rerunFiles(files, trigger)
}

async changeFilenamePattern(pattern: string) {
async changeFilenamePattern(pattern: string, files: string[] = this.state.getFilepaths()) {
this.filenamePattern = pattern

const files = this.state.getFilepaths()
const trigger = this.filenamePattern ? 'change filename pattern' : 'reset filename pattern'

await this.rerunFiles(files, trigger)
Expand Down Expand Up @@ -758,8 +787,10 @@ export class Vitest {

const matchingProjects: WorkspaceProject[] = []
await Promise.all(this.projects.map(async (project) => {
if (await project.isTargetFile(id))
if (await project.isTargetFile(id)) {
matchingProjects.push(project)
project.testFilesList?.push(id)
}
}))

if (matchingProjects.length > 0) {
Expand Down Expand Up @@ -940,6 +971,10 @@ export class Vitest {
)))
}

public async getTestFilepaths() {
return this.globTestFiles().then(files => files.map(([, file]) => file))
}

public async globTestFiles(filters: string[] = []) {
const files: WorkspaceSpec[] = []
await Promise.all(this.projects.map(async (project) => {
Expand Down
5 changes: 4 additions & 1 deletion packages/vitest/src/node/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,10 @@ export class Logger {
if (this.ctx.coverageProvider)
this.log(c.dim(' Coverage enabled with ') + c.yellow(this.ctx.coverageProvider.name))

this.log()
if (this.ctx.config.standalone)
this.log(c.yellow(`\nVitest is running in standalone mode. Edit a test file to rerun tests.`))
else
this.log()
}

async printUnhandledErrors(errors: unknown[]) {
Expand Down
22 changes: 17 additions & 5 deletions packages/vitest/src/node/stdin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import readline from 'node:readline'
import type { Writable } from 'node:stream'
import c from 'picocolors'
import prompt from 'prompts'
import { relative } from 'pathe'
import { relative, resolve } from 'pathe'
import { getTests, isWindows, stdout } from '../utils'
import { toArray } from '../utils/base'
import type { Vitest } from './core'
Expand Down Expand Up @@ -73,8 +73,10 @@ export function registerConsoleShortcuts(ctx: Vitest, stdin: NodeJS.ReadStream =
if (name === 'u')
return ctx.updateSnapshot()
// rerun all tests
if (name === 'a' || name === 'return')
return ctx.changeNamePattern('')
if (name === 'a' || name === 'return') {
const files = await ctx.getTestFilepaths()
return ctx.changeNamePattern('', files, 'rerun all tests')
}
// rerun current pattern tests
if (name === 'r')
return ctx.rerunFiles()
Expand Down Expand Up @@ -113,7 +115,13 @@ export function registerConsoleShortcuts(ctx: Vitest, stdin: NodeJS.ReadStream =
})

on()
await ctx.changeNamePattern(filter?.trim() || '', undefined, 'change pattern')
const files = ctx.state.getFilepaths()
// if running in standalone mode, Vitest instance doesn't know about any test file
const cliFiles = ctx.config.standalone && !files.length
? await ctx.getTestFilepaths()
: undefined

await ctx.changeNamePattern(filter?.trim() || '', cliFiles, 'change pattern')
}

async function inputProjectName() {
Expand Down Expand Up @@ -143,8 +151,12 @@ export function registerConsoleShortcuts(ctx: Vitest, stdin: NodeJS.ReadStream =
on()

latestFilename = filter?.trim() || ''
const lastResults = watchFilter.getLastResults()

await ctx.changeFilenamePattern(latestFilename)
await ctx.changeFilenamePattern(
latestFilename,
filter && lastResults.length ? lastResults.map(i => resolve(ctx.config.root, i)) : undefined,
)
}

let rl: readline.Interface | undefined
Expand Down
4 changes: 4 additions & 0 deletions packages/vitest/src/node/watch-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,8 @@ export class WatchFilter {
// @ts-expect-error -- write() method has different signature on the union type
this.stdout.write(data)
}

public getLastResults() {
return this.results
}
}
9 changes: 9 additions & 0 deletions packages/vitest/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,15 @@ export interface UserConfig extends InlineConfig {
*/
config?: string | false | undefined

/**
* Do not run tests when Vitest starts.
*
* Vitest will only run tests if it's called programmatically or the test file changes.
*
* CLI file filters will be ignored.
*/
standalone?: boolean

/**
* Use happy-dom
*/
Expand Down
4 changes: 3 additions & 1 deletion test/reporters/tests/default.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ describe('default reporter', async () => {
// one file
vitest.write('p')
await vitest.waitForStdout('Input filename pattern')
vitest.write('a\n')
vitest.write('a')
await vitest.waitForStdout('a.test.ts')
vitest.write('\n')
await vitest.waitForStdout('Filename pattern: a')
await vitest.waitForStdout('Waiting for file changes...')
expect(vitest.stdout).contain('✓ a1 test')
Expand Down
Loading

0 comments on commit bdce0a2

Please sign in to comment.