[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(runner): add "queued" state (#6931)

Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>

authored by

Vladimir
Ari Perkkiö
and committed by
GitHub
(Dec 17, 2024, 4:20 PM +0100) 5f8d2091 4e60333d

+105 -30
+4
packages/runner/src/collect.ts
··· 107 107 config.allowOnly, 108 108 ) 109 109 110 + if (file.mode === 'queued') { 111 + file.mode = 'run' 112 + } 113 + 110 114 files.push(file) 111 115 } 112 116
+2 -2
packages/runner/src/run.ts
··· 196 196 export async function runTest(test: Test, runner: VitestRunner): Promise<void> { 197 197 await runner.onBeforeRunTask?.(test) 198 198 199 - if (test.mode !== 'run') { 199 + if (test.mode !== 'run' && test.mode !== 'queued') { 200 200 return 201 201 } 202 202 ··· 458 458 failTask(suite.result, e, runner.config.diffOptions) 459 459 } 460 460 461 - if (suite.mode === 'run') { 461 + if (suite.mode === 'run' || suite.mode === 'queued') { 462 462 if (!runner.config.passWithNoTests && !hasTests(suite)) { 463 463 suite.result.state = 'fail' 464 464 if (!suite.result.errors?.length) {
+5
test/reporters/fixtures/long-loading-task.test.ts
··· 1 + import { test } from 'vitest' 2 + 3 + await new Promise(r => setTimeout(r, 500)) 4 + 5 + test('works')
+14
test/reporters/tests/default.test.ts
··· 70 70 expect(result.stderr).not.toContain(`status: 'not found'`) 71 71 }) 72 72 73 + test('prints queued tests as soon as they are added', async () => { 74 + const { stdout, vitest } = await runVitest({ 75 + include: ['fixtures/long-loading-task.test.ts'], 76 + reporters: [['default', { isTTY: true, summary: true }]], 77 + config: 'fixtures/vitest.config.ts', 78 + watch: true, 79 + }) 80 + 81 + await vitest.waitForStdout('❯ fixtures/long-loading-task.test.ts [queued]') 82 + await vitest.waitForStdout('Waiting for file changes...') 83 + 84 + expect(stdout).toContain('✓ fixtures/long-loading-task.test.ts (1 test)') 85 + }) 86 + 73 87 test('prints skipped tests by default when a single file is run', async () => { 74 88 const { stdout } = await runVitest({ 75 89 include: ['fixtures/all-passing-or-skipped.test.ts'],
+6 -1
packages/browser/src/node/rpc.ts
··· 1 1 import type { ErrorWithDiff } from 'vitest' 2 - import type { BrowserCommandContext, ResolveSnapshotPathHandlerContext } from 'vitest/node' 2 + import type { BrowserCommandContext, ResolveSnapshotPathHandlerContext, TestModule } from 'vitest/node' 3 3 import type { WebSocket } from 'ws' 4 4 import type { BrowserServer } from './server' 5 5 import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from './types' ··· 74 74 _error.stacks = server.parseErrorStacktrace(_error) 75 75 } 76 76 ctx.state.catchError(error, type) 77 + }, 78 + async onQueued(file) { 79 + ctx.state.collectFiles(project, [file]) 80 + const testModule = ctx.state.getReportedEntity(file) as TestModule 81 + await ctx.report('onTestModuleQueued', testModule) 77 82 }, 78 83 async onCollected(files) { 79 84 ctx.state.collectFiles(project, files)
+1
packages/browser/src/node/types.ts
··· 6 6 resolveSnapshotPath: (testPath: string) => string 7 7 resolveSnapshotRawPath: (testPath: string, rawPath: string) => string 8 8 onUnhandledError: (error: unknown, type: string) => Promise<void> 9 + onQueued: (file: RunnerTestFile) => void 9 10 onCollected: (files?: RunnerTestFile[]) => Promise<void> 10 11 onTaskUpdate: (packs: TaskResultPack[]) => void 11 12 onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void
+2 -1
packages/runner/src/types/tasks.ts
··· 2 2 import type { FixtureItem } from '../fixture' 3 3 import type { ChainableFunction } from '../utils/chain' 4 4 5 - export type RunMode = 'run' | 'skip' | 'only' | 'todo' 5 + export type RunMode = 'run' | 'skip' | 'only' | 'todo' | 'queued' 6 6 export type TaskState = RunMode | 'pass' | 'fail' 7 7 8 8 export interface TaskBase { ··· 23 23 * - **only**: only this task and other tasks with `only` mode will run 24 24 * - **todo**: task is marked as a todo, alias for `skip` 25 25 * - **run**: task will run or already ran 26 + * - **queued**: task will start running next. It can only exist on the File 26 27 */ 27 28 mode: RunMode 28 29 /**
+4 -4
packages/runner/src/utils/collect.ts
··· 72 72 }) 73 73 74 74 // if all subtasks are skipped, mark as skip 75 - if (suite.mode === 'run') { 76 - if (suite.tasks.length && suite.tasks.every(i => i.mode !== 'run')) { 75 + if (suite.mode === 'run' || suite.mode === 'queued') { 76 + if (suite.tasks.length && suite.tasks.every(i => i.mode !== 'run' && i.mode !== 'queued')) { 77 77 suite.mode = 'skip' 78 78 } 79 79 } ··· 115 115 116 116 function skipAllTasks(suite: Suite) { 117 117 suite.tasks.forEach((t) => { 118 - if (t.mode === 'run') { 118 + if (t.mode === 'run' || t.mode === 'queued') { 119 119 t.mode = 'skip' 120 120 if (t.type === 'suite') { 121 121 skipAllTasks(t) ··· 172 172 id: generateFileHash(path, projectName), 173 173 name: path, 174 174 type: 'suite', 175 - mode: 'run', 175 + mode: 'queued', 176 176 filepath, 177 177 tasks: [], 178 178 meta: Object.create(null),
+2 -2
packages/vitest/src/typecheck/collect.ts
··· 1 - import type { File, Suite, Test } from '@vitest/runner' 1 + import type { File, RunMode, Suite, Test } from '@vitest/runner' 2 2 import type { Node } from 'estree' 3 3 import type { RawSourceMap } from 'vite-node' 4 4 import type { TestProject } from '../node/project' ··· 32 32 end: number 33 33 name: string 34 34 type: 'suite' | 'test' 35 - mode: 'run' | 'skip' | 'only' | 'todo' 35 + mode: RunMode 36 36 task: ParsedSuite | ParsedFile | ParsedTest 37 37 } 38 38
+1 -1
packages/vitest/src/typecheck/typechecker.ts
··· 112 112 if ('tasks' in task) { 113 113 markTasks(task.tasks) 114 114 } 115 - if (!task.result?.state && task.mode === 'run') { 115 + if (!task.result?.state && (task.mode === 'run' || task.mode === 'queued')) { 116 116 task.result = { 117 117 state: 'pass', 118 118 }
+1
packages/vitest/src/types/rpc.ts
··· 39 39 onPathsCollected: (paths: string[]) => void 40 40 onUserConsoleLog: (log: UserConsoleLog) => void 41 41 onUnhandledError: (err: unknown, type: string) => void 42 + onQueued: (file: File) => void 42 43 onCollected: (files: File[]) => Promise<void> 43 44 onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void 44 45 onTaskUpdate: (pack: TaskResultPack[]) => Promise<void>
+4
packages/browser/src/client/tester/runner.ts
··· 104 104 } 105 105 } 106 106 107 + onCollectStart = (file: File) => { 108 + return rpc().onQueued(file) 109 + } 110 + 107 111 onCollected = async (files: File[]): Promise<unknown> => { 108 112 files.forEach((file) => { 109 113 file.prepareDuration = state.durations.prepare
+6
packages/vitest/src/node/pools/rpc.ts
··· 1 1 import type { RawSourceMap } from 'vite-node' 2 2 import type { RuntimeRPC } from '../../types/rpc' 3 3 import type { TestProject } from '../project' 4 + import type { TestModule } from '../reporters/reported-tasks' 4 5 import type { ResolveSnapshotPathHandlerContext } from '../types/config' 5 6 import { mkdir, writeFile } from 'node:fs/promises' 6 7 import { join } from 'pathe' ··· 77 78 onPathsCollected(paths) { 78 79 ctx.state.collectPaths(paths) 79 80 return ctx.report('onPathsCollected', paths) 81 + }, 82 + onQueued(file) { 83 + ctx.state.collectFiles(project, [file]) 84 + const testModule = ctx.state.getReportedEntity(file) as TestModule 85 + return ctx.report('onTestModuleQueued', testModule) 80 86 }, 81 87 onCollected(files) { 82 88 ctx.state.collectFiles(project, files)
+2 -1
packages/vitest/src/node/reporters/base.ts
··· 75 75 if ( 76 76 !('filepath' in task) 77 77 || !task.result?.state 78 - || task.result?.state === 'run') { 78 + || task.result?.state === 'run' 79 + || task.result?.state === 'queued') { 79 80 return 80 81 } 81 82
+5
packages/vitest/src/node/reporters/default.ts
··· 1 1 import type { File, TaskResultPack } from '@vitest/runner' 2 2 import type { Vitest } from '../core' 3 3 import type { BaseOptions } from './base' 4 + import type { TestModule } from './reported-tasks' 4 5 import { BaseReporter } from './base' 5 6 import { SummaryReporter } from './summary' 6 7 ··· 26 27 if (this.options.summary) { 27 28 this.summary = new SummaryReporter() 28 29 } 30 + } 31 + 32 + onTestModuleQueued(file: TestModule) { 33 + this.summary?.onTestModuleQueued(file) 29 34 } 30 35 31 36 onInit(ctx: Vitest) {
+4 -3
packages/vitest/src/node/reporters/json.ts
··· 26 26 run: 'pending', 27 27 skip: 'skipped', 28 28 todo: 'todo', 29 + queued: 'pending', 29 30 } 30 31 31 32 export interface JsonAssertionResult { ··· 95 96 96 97 const numFailedTestSuites = suites.filter(s => s.result?.state === 'fail').length 97 98 const numPendingTestSuites = suites.filter( 98 - s => s.result?.state === 'run' || s.mode === 'todo', 99 + s => s.result?.state === 'run' || s.result?.state === 'queued' || s.mode === 'todo', 99 100 ).length 100 101 const numPassedTestSuites = numTotalTestSuites - numFailedTestSuites - numPendingTestSuites 101 102 ··· 104 105 ).length 105 106 const numPassedTests = tests.filter(t => t.result?.state === 'pass').length 106 107 const numPendingTests = tests.filter( 107 - t => t.result?.state === 'run' || t.mode === 'skip' || t.result?.state === 'skip', 108 + t => t.result?.state === 'run' || t.result?.state === 'queued' || t.mode === 'skip' || t.result?.state === 'skip', 108 109 ).length 109 110 const numTodoTests = tests.filter(t => t.mode === 'todo').length 110 111 const testResults: Array<JsonTestResult> = [] ··· 154 155 } satisfies JsonAssertionResult 155 156 }) 156 157 157 - if (tests.some(t => t.result?.state === 'run')) { 158 + if (tests.some(t => t.result?.state === 'run' || t.result?.state === 'queued')) { 158 159 this.ctx.logger.warn( 159 160 'WARNING: Some tests are still running when generating the JSON report.' 160 161 + 'This is likely an internal bug in Vitest.'
+3 -3
packages/vitest/src/node/reporters/reported-tasks.ts
··· 126 126 */ 127 127 public result(): TestResult | undefined { 128 128 const result = this.task.result 129 - if (!result || result.state === 'run') { 129 + if (!result || result.state === 'run' || result.state === 'queued') { 130 130 return undefined 131 131 } 132 132 const state = result.state === 'fail' ··· 175 175 public diagnostic(): TestDiagnostic | undefined { 176 176 const result = this.task.result 177 177 // startTime should always be available if the test has properly finished 178 - if (!result || result.state === 'run' || !result.startTime) { 178 + if (!result || result.state === 'run' || result.state === 'queued' || !result.startTime) { 179 179 return undefined 180 180 } 181 181 const duration = result.duration || 0 ··· 450 450 shuffle: boolean | undefined 451 451 retry: number | undefined 452 452 repeats: number | undefined 453 - mode: 'run' | 'only' | 'skip' | 'todo' 453 + mode: 'run' | 'only' | 'skip' | 'todo' | 'queued' 454 454 } 455 455 456 456 function buildOptions(
+21 -3
packages/vitest/src/node/reporters/summary.ts
··· 1 1 import type { File, Test } from '@vitest/runner' 2 2 import type { Vitest } from '../core' 3 3 import type { Reporter } from '../types/reporter' 4 + import type { TestModule } from './reported-tasks' 4 5 import type { HookOptions } from './task-parser' 5 6 import { getTests } from '@vitest/runner/utils' 6 7 import c from 'tinyrainbow' ··· 87 88 }) 88 89 } 89 90 91 + onTestModuleQueued(module: TestModule) { 92 + this.onTestFilePrepare(module.task) 93 + } 94 + 90 95 onPathsCollected(paths?: string[]) { 91 96 this.suites.total = (paths || []).length 92 97 } ··· 111 116 } 112 117 113 118 onTestFilePrepare(file: File) { 114 - if (this.allFinishedTests.has(file.id) || this.runningTests.has(file.id)) { 119 + if (this.runningTests.has(file.id)) { 120 + const stats = this.runningTests.get(file.id)! 121 + // if there are no tests, it means the test was queued but not collected 122 + if (!stats.total) { 123 + const total = getTests(file).length 124 + this.tests.total += total 125 + stats.total = total 126 + } 127 + return 128 + } 129 + 130 + if (this.allFinishedTests.has(file.id)) { 115 131 return 116 132 } 117 133 ··· 266 282 const file = test.file 267 283 let stats = this.runningTests.get(file.id) 268 284 269 - if (!stats) { 285 + if (!stats || stats.total === 0) { 270 286 // It's possible that that test finished before it's preparation was even reported 271 287 this.onTestFilePrepare(test.file) 272 288 stats = this.runningTests.get(file.id)! ··· 303 319 c.bold(c.yellow(` ${F_POINTER} `)) 304 320 + formatProjectName(testFile.projectName) 305 321 + testFile.filename 306 - + c.dim(` ${testFile.completed}/${testFile.total}`), 322 + + c.dim(!testFile.completed && !testFile.total 323 + ? ' [queued]' 324 + : ` ${testFile.completed}/${testFile.total}`), 307 325 ) 308 326 309 327 const slowTasks = [
+4 -5
packages/vitest/src/node/reporters/task-parser.ts
··· 39 39 const task = this.ctx.state.idMap.get(pack[0]) 40 40 41 41 if (task?.type === 'suite' && 'filepath' in task && task.result?.state) { 42 - if (task?.result?.state === 'run') { 42 + if (task?.result?.state === 'run' || task?.result?.state === 'queued') { 43 43 startingTestFiles.push(task) 44 44 } 45 45 else { ··· 55 55 } 56 56 57 57 if (task?.type === 'test') { 58 - if (task.result?.state === 'run') { 58 + if (task.result?.state === 'run' || task.result?.state === 'queued') { 59 59 startingTests.push(task) 60 60 } 61 61 else if (task.result?.hooks?.afterEach !== 'run') { ··· 65 65 66 66 if (task?.result?.hooks) { 67 67 for (const [hook, state] of Object.entries(task.result.hooks)) { 68 - if (state === 'run') { 68 + if (state === 'run' || state === 'queued') { 69 69 startingHooks.push({ name: hook, file: task.file, id: task.id, type: task.type }) 70 70 } 71 71 else { ··· 81 81 82 82 startingTestFiles.forEach(file => this.onTestFilePrepare(file)) 83 83 startingTests.forEach(test => this.onTestStart(test)) 84 - startingHooks.forEach(hook => this.onHookStart(hook), 85 - ) 84 + startingHooks.forEach(hook => this.onHookStart(hook)) 86 85 } 87 86 }
+1
packages/vitest/src/node/reporters/verbose.ts
··· 20 20 && task.type === 'test' 21 21 && task.result?.state 22 22 && task.result?.state !== 'run' 23 + && task.result?.state !== 'queued' 23 24 ) { 24 25 let title = ` ${getStateSymbol(task)} ` 25 26 if (task.file.projectName) {
+2
packages/vitest/src/node/types/reporter.ts
··· 2 2 import type { SerializedTestSpecification } from '../../runtime/types/utils' 3 3 import type { Awaitable, UserConsoleLog } from '../../types/general' 4 4 import type { Vitest } from '../core' 5 + import type { TestModule } from '../reporters/reported-tasks' 5 6 6 7 export interface Reporter { 7 8 onInit?: (ctx: Vitest) => void 8 9 onPathsCollected?: (paths?: string[]) => Awaitable<void> 9 10 onSpecsCollected?: (specs?: SerializedTestSpecification[]) => Awaitable<void> 11 + onTestModuleQueued?: (file: TestModule) => Awaitable<void> 10 12 onCollected?: (files?: File[]) => Awaitable<void> 11 13 onFinished?: ( 12 14 files: File[],
+1 -1
packages/vitest/src/runtime/runners/benchmark.ts
··· 35 35 const benchmarkGroup: Benchmark[] = [] 36 36 const benchmarkSuiteGroup = [] 37 37 for (const task of suite.tasks) { 38 - if (task.mode !== 'run') { 38 + if (task.mode !== 'run' && task.mode !== 'queued') { 39 39 continue 40 40 } 41 41
+6
packages/vitest/src/runtime/runners/index.ts
··· 68 68 return p 69 69 } 70 70 71 + const originalOnCollectStart = testRunner.onCollectStart 72 + testRunner.onCollectStart = async (file) => { 73 + await rpc().onQueued(file) 74 + await originalOnCollectStart?.call(testRunner, file) 75 + } 76 + 71 77 const originalOnCollected = testRunner.onCollected 72 78 testRunner.onCollected = async (files) => { 73 79 const state = getWorkerState()
+1 -1
packages/vitest/src/runtime/runners/test.ts
··· 91 91 test.mode = 'skip' 92 92 } 93 93 94 - if (test.mode !== 'run') { 94 + if (test.mode !== 'run' && test.mode !== 'queued') { 95 95 return 96 96 } 97 97
+1 -1
packages/vitest/src/node/reporters/renderers/utils.ts
··· 163 163 return pending 164 164 } 165 165 166 - if (task.result.state === 'run') { 166 + if (task.result.state === 'run' || task.result.state === 'queued') { 167 167 if (task.type === 'suite') { 168 168 return pointer 169 169 }
+2 -1
packages/vitest/src/node/reporters/benchmark/table/index.ts
··· 76 76 && task.type === 'suite' 77 77 && task.result?.state 78 78 && task.result?.state !== 'run' 79 + && task.result?.state !== 'queued' 79 80 ) { 80 81 // render static table when all benches inside single suite are finished 81 82 const benches = task.tasks.filter(t => t.meta.benchmark) 82 83 if ( 83 84 benches.length > 0 84 - && benches.every(t => t.result?.state !== 'run') 85 + && benches.every(t => t.result?.state !== 'run' && t.result?.state !== 'queued') 85 86 ) { 86 87 let title = ` ${getStateSymbol(task)} ${getFullName( 87 88 task,