Skip to content

Commit 5f8d209

Browse files
sheremet-vaAriPerkkio
andauthoredDec 17, 2024··
feat(runner): add "queued" state (#6931)
Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>
1 parent 4e60333 commit 5f8d209

File tree

26 files changed

+105
-30
lines changed

26 files changed

+105
-30
lines changed
 

‎packages/browser/src/client/tester/runner.ts

+4
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ export function createBrowserRunner(
104104
}
105105
}
106106

107+
onCollectStart = (file: File) => {
108+
return rpc().onQueued(file)
109+
}
110+
107111
onCollected = async (files: File[]): Promise<unknown> => {
108112
files.forEach((file) => {
109113
file.prepareDuration = state.durations.prepare

‎packages/browser/src/node/rpc.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ErrorWithDiff } from 'vitest'
2-
import type { BrowserCommandContext, ResolveSnapshotPathHandlerContext } from 'vitest/node'
2+
import type { BrowserCommandContext, ResolveSnapshotPathHandlerContext, TestModule } from 'vitest/node'
33
import type { WebSocket } from 'ws'
44
import type { BrowserServer } from './server'
55
import type { WebSocketBrowserEvents, WebSocketBrowserHandlers } from './types'
@@ -75,6 +75,11 @@ export function setupBrowserRpc(server: BrowserServer) {
7575
}
7676
ctx.state.catchError(error, type)
7777
},
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)
82+
},
7883
async onCollected(files) {
7984
ctx.state.collectFiles(project, files)
8085
await ctx.report('onCollected', files)

‎packages/browser/src/node/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface WebSocketBrowserHandlers {
66
resolveSnapshotPath: (testPath: string) => string
77
resolveSnapshotRawPath: (testPath: string, rawPath: string) => string
88
onUnhandledError: (error: unknown, type: string) => Promise<void>
9+
onQueued: (file: RunnerTestFile) => void
910
onCollected: (files?: RunnerTestFile[]) => Promise<void>
1011
onTaskUpdate: (packs: TaskResultPack[]) => void
1112
onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void

‎packages/runner/src/collect.ts

+4
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ export async function collectTests(
107107
config.allowOnly,
108108
)
109109

110+
if (file.mode === 'queued') {
111+
file.mode = 'run'
112+
}
113+
110114
files.push(file)
111115
}
112116

‎packages/runner/src/run.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ async function callCleanupHooks(cleanups: HookCleanupCallback[]) {
196196
export async function runTest(test: Test, runner: VitestRunner): Promise<void> {
197197
await runner.onBeforeRunTask?.(test)
198198

199-
if (test.mode !== 'run') {
199+
if (test.mode !== 'run' && test.mode !== 'queued') {
200200
return
201201
}
202202

@@ -458,7 +458,7 @@ export async function runSuite(suite: Suite, runner: VitestRunner): Promise<void
458458
failTask(suite.result, e, runner.config.diffOptions)
459459
}
460460

461-
if (suite.mode === 'run') {
461+
if (suite.mode === 'run' || suite.mode === 'queued') {
462462
if (!runner.config.passWithNoTests && !hasTests(suite)) {
463463
suite.result.state = 'fail'
464464
if (!suite.result.errors?.length) {

‎packages/runner/src/types/tasks.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Awaitable, ErrorWithDiff } from '@vitest/utils'
22
import type { FixtureItem } from '../fixture'
33
import type { ChainableFunction } from '../utils/chain'
44

5-
export type RunMode = 'run' | 'skip' | 'only' | 'todo'
5+
export type RunMode = 'run' | 'skip' | 'only' | 'todo' | 'queued'
66
export type TaskState = RunMode | 'pass' | 'fail'
77

88
export interface TaskBase {
@@ -23,6 +23,7 @@ export interface TaskBase {
2323
* - **only**: only this task and other tasks with `only` mode will run
2424
* - **todo**: task is marked as a todo, alias for `skip`
2525
* - **run**: task will run or already ran
26+
* - **queued**: task will start running next. It can only exist on the File
2627
*/
2728
mode: RunMode
2829
/**

‎packages/runner/src/utils/collect.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ export function interpretTaskModes(
7272
})
7373

7474
// 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')) {
7777
suite.mode = 'skip'
7878
}
7979
}
@@ -115,7 +115,7 @@ export function someTasksAreOnly(suite: Suite): boolean {
115115

116116
function skipAllTasks(suite: Suite) {
117117
suite.tasks.forEach((t) => {
118-
if (t.mode === 'run') {
118+
if (t.mode === 'run' || t.mode === 'queued') {
119119
t.mode = 'skip'
120120
if (t.type === 'suite') {
121121
skipAllTasks(t)
@@ -172,7 +172,7 @@ export function createFileTask(
172172
id: generateFileHash(path, projectName),
173173
name: path,
174174
type: 'suite',
175-
mode: 'run',
175+
mode: 'queued',
176176
filepath,
177177
tasks: [],
178178
meta: Object.create(null),

‎packages/vitest/src/node/pools/rpc.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { RawSourceMap } from 'vite-node'
22
import type { RuntimeRPC } from '../../types/rpc'
33
import type { TestProject } from '../project'
4+
import type { TestModule } from '../reporters/reported-tasks'
45
import type { ResolveSnapshotPathHandlerContext } from '../types/config'
56
import { mkdir, writeFile } from 'node:fs/promises'
67
import { join } from 'pathe'
@@ -78,6 +79,11 @@ export function createMethodsRPC(project: TestProject, options: MethodsOptions =
7879
ctx.state.collectPaths(paths)
7980
return ctx.report('onPathsCollected', paths)
8081
},
82+
onQueued(file) {
83+
ctx.state.collectFiles(project, [file])
84+
const testModule = ctx.state.getReportedEntity(file) as TestModule
85+
return ctx.report('onTestModuleQueued', testModule)
86+
},
8187
onCollected(files) {
8288
ctx.state.collectFiles(project, files)
8389
return ctx.report('onCollected', files)

‎packages/vitest/src/node/reporters/base.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ export abstract class BaseReporter implements Reporter {
7575
if (
7676
!('filepath' in task)
7777
|| !task.result?.state
78-
|| task.result?.state === 'run') {
78+
|| task.result?.state === 'run'
79+
|| task.result?.state === 'queued') {
7980
return
8081
}
8182

‎packages/vitest/src/node/reporters/benchmark/table/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,13 @@ export class TableReporter extends BaseReporter {
7676
&& task.type === 'suite'
7777
&& task.result?.state
7878
&& task.result?.state !== 'run'
79+
&& task.result?.state !== 'queued'
7980
) {
8081
// render static table when all benches inside single suite are finished
8182
const benches = task.tasks.filter(t => t.meta.benchmark)
8283
if (
8384
benches.length > 0
84-
&& benches.every(t => t.result?.state !== 'run')
85+
&& benches.every(t => t.result?.state !== 'run' && t.result?.state !== 'queued')
8586
) {
8687
let title = ` ${getStateSymbol(task)} ${getFullName(
8788
task,

‎packages/vitest/src/node/reporters/default.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { File, TaskResultPack } from '@vitest/runner'
22
import type { Vitest } from '../core'
33
import type { BaseOptions } from './base'
4+
import type { TestModule } from './reported-tasks'
45
import { BaseReporter } from './base'
56
import { SummaryReporter } from './summary'
67

@@ -28,6 +29,10 @@ export class DefaultReporter extends BaseReporter {
2829
}
2930
}
3031

32+
onTestModuleQueued(file: TestModule) {
33+
this.summary?.onTestModuleQueued(file)
34+
}
35+
3136
onInit(ctx: Vitest) {
3237
super.onInit(ctx)
3338
this.summary?.onInit(ctx, { verbose: this.verbose })

‎packages/vitest/src/node/reporters/json.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const StatusMap: Record<TaskState, Status> = {
2626
run: 'pending',
2727
skip: 'skipped',
2828
todo: 'todo',
29+
queued: 'pending',
2930
}
3031

3132
export interface JsonAssertionResult {
@@ -95,7 +96,7 @@ export class JsonReporter implements Reporter {
9596

9697
const numFailedTestSuites = suites.filter(s => s.result?.state === 'fail').length
9798
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',
99100
).length
100101
const numPassedTestSuites = numTotalTestSuites - numFailedTestSuites - numPendingTestSuites
101102

@@ -104,7 +105,7 @@ export class JsonReporter implements Reporter {
104105
).length
105106
const numPassedTests = tests.filter(t => t.result?.state === 'pass').length
106107
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',
108109
).length
109110
const numTodoTests = tests.filter(t => t.mode === 'todo').length
110111
const testResults: Array<JsonTestResult> = []
@@ -154,7 +155,7 @@ export class JsonReporter implements Reporter {
154155
} satisfies JsonAssertionResult
155156
})
156157

157-
if (tests.some(t => t.result?.state === 'run')) {
158+
if (tests.some(t => t.result?.state === 'run' || t.result?.state === 'queued')) {
158159
this.ctx.logger.warn(
159160
'WARNING: Some tests are still running when generating the JSON report.'
160161
+ 'This is likely an internal bug in Vitest.'

‎packages/vitest/src/node/reporters/renderers/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ export function getStateSymbol(task: Task) {
163163
return pending
164164
}
165165

166-
if (task.result.state === 'run') {
166+
if (task.result.state === 'run' || task.result.state === 'queued') {
167167
if (task.type === 'suite') {
168168
return pointer
169169
}

‎packages/vitest/src/node/reporters/reported-tasks.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export class TestCase extends ReportedTaskImplementation {
126126
*/
127127
public result(): TestResult | undefined {
128128
const result = this.task.result
129-
if (!result || result.state === 'run') {
129+
if (!result || result.state === 'run' || result.state === 'queued') {
130130
return undefined
131131
}
132132
const state = result.state === 'fail'
@@ -175,7 +175,7 @@ export class TestCase extends ReportedTaskImplementation {
175175
public diagnostic(): TestDiagnostic | undefined {
176176
const result = this.task.result
177177
// 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) {
179179
return undefined
180180
}
181181
const duration = result.duration || 0
@@ -450,7 +450,7 @@ export interface TaskOptions {
450450
shuffle: boolean | undefined
451451
retry: number | undefined
452452
repeats: number | undefined
453-
mode: 'run' | 'only' | 'skip' | 'todo'
453+
mode: 'run' | 'only' | 'skip' | 'todo' | 'queued'
454454
}
455455

456456
function buildOptions(

‎packages/vitest/src/node/reporters/summary.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { File, Test } from '@vitest/runner'
22
import type { Vitest } from '../core'
33
import type { Reporter } from '../types/reporter'
4+
import type { TestModule } from './reported-tasks'
45
import type { HookOptions } from './task-parser'
56
import { getTests } from '@vitest/runner/utils'
67
import c from 'tinyrainbow'
@@ -87,6 +88,10 @@ export class SummaryReporter extends TaskParser implements Reporter {
8788
})
8889
}
8990

91+
onTestModuleQueued(module: TestModule) {
92+
this.onTestFilePrepare(module.task)
93+
}
94+
9095
onPathsCollected(paths?: string[]) {
9196
this.suites.total = (paths || []).length
9297
}
@@ -111,7 +116,18 @@ export class SummaryReporter extends TaskParser implements Reporter {
111116
}
112117

113118
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)) {
115131
return
116132
}
117133

@@ -266,7 +282,7 @@ export class SummaryReporter extends TaskParser implements Reporter {
266282
const file = test.file
267283
let stats = this.runningTests.get(file.id)
268284

269-
if (!stats) {
285+
if (!stats || stats.total === 0) {
270286
// It's possible that that test finished before it's preparation was even reported
271287
this.onTestFilePrepare(test.file)
272288
stats = this.runningTests.get(file.id)!
@@ -303,7 +319,9 @@ export class SummaryReporter extends TaskParser implements Reporter {
303319
c.bold(c.yellow(` ${F_POINTER} `))
304320
+ formatProjectName(testFile.projectName)
305321
+ testFile.filename
306-
+ c.dim(` ${testFile.completed}/${testFile.total}`),
322+
+ c.dim(!testFile.completed && !testFile.total
323+
? ' [queued]'
324+
: ` ${testFile.completed}/${testFile.total}`),
307325
)
308326

309327
const slowTasks = [

‎packages/vitest/src/node/reporters/task-parser.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class TaskParser {
3939
const task = this.ctx.state.idMap.get(pack[0])
4040

4141
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') {
4343
startingTestFiles.push(task)
4444
}
4545
else {
@@ -55,7 +55,7 @@ export class TaskParser {
5555
}
5656

5757
if (task?.type === 'test') {
58-
if (task.result?.state === 'run') {
58+
if (task.result?.state === 'run' || task.result?.state === 'queued') {
5959
startingTests.push(task)
6060
}
6161
else if (task.result?.hooks?.afterEach !== 'run') {
@@ -65,7 +65,7 @@ export class TaskParser {
6565

6666
if (task?.result?.hooks) {
6767
for (const [hook, state] of Object.entries(task.result.hooks)) {
68-
if (state === 'run') {
68+
if (state === 'run' || state === 'queued') {
6969
startingHooks.push({ name: hook, file: task.file, id: task.id, type: task.type })
7070
}
7171
else {
@@ -81,7 +81,6 @@ export class TaskParser {
8181

8282
startingTestFiles.forEach(file => this.onTestFilePrepare(file))
8383
startingTests.forEach(test => this.onTestStart(test))
84-
startingHooks.forEach(hook => this.onHookStart(hook),
85-
)
84+
startingHooks.forEach(hook => this.onHookStart(hook))
8685
}
8786
}

‎packages/vitest/src/node/reporters/verbose.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export class VerboseReporter extends DefaultReporter {
2020
&& task.type === 'test'
2121
&& task.result?.state
2222
&& task.result?.state !== 'run'
23+
&& task.result?.state !== 'queued'
2324
) {
2425
let title = ` ${getStateSymbol(task)} `
2526
if (task.file.projectName) {

‎packages/vitest/src/node/types/reporter.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import type { File, TaskResultPack } from '@vitest/runner'
22
import type { SerializedTestSpecification } from '../../runtime/types/utils'
33
import type { Awaitable, UserConsoleLog } from '../../types/general'
44
import type { Vitest } from '../core'
5+
import type { TestModule } from '../reporters/reported-tasks'
56

67
export interface Reporter {
78
onInit?: (ctx: Vitest) => void
89
onPathsCollected?: (paths?: string[]) => Awaitable<void>
910
onSpecsCollected?: (specs?: SerializedTestSpecification[]) => Awaitable<void>
11+
onTestModuleQueued?: (file: TestModule) => Awaitable<void>
1012
onCollected?: (files?: File[]) => Awaitable<void>
1113
onFinished?: (
1214
files: File[],

‎packages/vitest/src/runtime/runners/benchmark.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ async function runBenchmarkSuite(suite: Suite, runner: NodeBenchmarkRunner) {
3535
const benchmarkGroup: Benchmark[] = []
3636
const benchmarkSuiteGroup = []
3737
for (const task of suite.tasks) {
38-
if (task.mode !== 'run') {
38+
if (task.mode !== 'run' && task.mode !== 'queued') {
3939
continue
4040
}
4141

‎packages/vitest/src/runtime/runners/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ export async function resolveTestRunner(
6868
return p
6969
}
7070

71+
const originalOnCollectStart = testRunner.onCollectStart
72+
testRunner.onCollectStart = async (file) => {
73+
await rpc().onQueued(file)
74+
await originalOnCollectStart?.call(testRunner, file)
75+
}
76+
7177
const originalOnCollected = testRunner.onCollected
7278
testRunner.onCollected = async (files) => {
7379
const state = getWorkerState()

‎packages/vitest/src/runtime/runners/test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export class VitestTestRunner implements VitestRunner {
9191
test.mode = 'skip'
9292
}
9393

94-
if (test.mode !== 'run') {
94+
if (test.mode !== 'run' && test.mode !== 'queued') {
9595
return
9696
}
9797

‎packages/vitest/src/typecheck/collect.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { File, Suite, Test } from '@vitest/runner'
1+
import type { File, RunMode, Suite, Test } from '@vitest/runner'
22
import type { Node } from 'estree'
33
import type { RawSourceMap } from 'vite-node'
44
import type { TestProject } from '../node/project'
@@ -32,7 +32,7 @@ interface LocalCallDefinition {
3232
end: number
3333
name: string
3434
type: 'suite' | 'test'
35-
mode: 'run' | 'skip' | 'only' | 'todo'
35+
mode: RunMode
3636
task: ParsedSuite | ParsedFile | ParsedTest
3737
}
3838

‎packages/vitest/src/typecheck/typechecker.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export class Typechecker {
112112
if ('tasks' in task) {
113113
markTasks(task.tasks)
114114
}
115-
if (!task.result?.state && task.mode === 'run') {
115+
if (!task.result?.state && (task.mode === 'run' || task.mode === 'queued')) {
116116
task.result = {
117117
state: 'pass',
118118
}

‎packages/vitest/src/types/rpc.ts

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface RuntimeRPC {
3939
onPathsCollected: (paths: string[]) => void
4040
onUserConsoleLog: (log: UserConsoleLog) => void
4141
onUnhandledError: (err: unknown, type: string) => void
42+
onQueued: (file: File) => void
4243
onCollected: (files: File[]) => Promise<void>
4344
onAfterSuiteRun: (meta: AfterSuiteRunMeta) => void
4445
onTaskUpdate: (pack: TaskResultPack[]) => Promise<void>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from 'vitest'
2+
3+
await new Promise(r => setTimeout(r, 500))
4+
5+
test('works')

‎test/reporters/tests/default.test.ts

+14
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,20 @@ describe('default reporter', async () => {
7070
expect(result.stderr).not.toContain(`status: 'not found'`)
7171
})
7272

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+
7387
test('prints skipped tests by default when a single file is run', async () => {
7488
const { stdout } = await runVitest({
7589
include: ['fixtures/all-passing-or-skipped.test.ts'],

0 commit comments

Comments
 (0)
Please sign in to comment.