Skip to content

Commit dbbbe43

Browse files
authored
feat!: remove the empty suite from the runner (#5435)
1 parent 2a80e95 commit dbbbe43

File tree

27 files changed

+694
-221
lines changed

27 files changed

+694
-221
lines changed

packages/runner/src/collect.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { relative } from 'pathe'
22
import { processError } from '@vitest/utils/error'
3-
import type { File } from './types'
3+
import type { File, SuiteHooks } from './types'
44
import type { VitestRunner } from './types/runner'
55
import { calculateSuiteHash, generateHash, interpretTaskModes, someTasksAreOnly } from './utils/collect'
6-
import { clearCollectorContext, getDefaultSuite } from './suite'
6+
import { clearCollectorContext, createSuiteHooks, getDefaultSuite } from './suite'
77
import { getHooks, setHooks } from './map'
88
import { collectorContext } from './context'
99
import { runSetupFiles } from './setup'
@@ -26,7 +26,9 @@ export async function collectTests(paths: string[], runner: VitestRunner): Promi
2626
tasks: [],
2727
meta: Object.create(null),
2828
projectName: config.name,
29+
file: undefined!,
2930
}
31+
file.file = file
3032

3133
clearCollectorContext(filepath, runner)
3234

@@ -41,24 +43,27 @@ export async function collectTests(paths: string[], runner: VitestRunner): Promi
4143

4244
const defaultTasks = await getDefaultSuite().collect(file)
4345

44-
setHooks(file, getHooks(defaultTasks))
46+
const fileHooks = createSuiteHooks()
47+
mergeHooks(fileHooks, getHooks(defaultTasks))
4548

4649
for (const c of [...defaultTasks.tasks, ...collectorContext.tasks]) {
47-
if (c.type === 'test') {
48-
file.tasks.push(c)
49-
}
50-
else if (c.type === 'custom') {
51-
file.tasks.push(c)
52-
}
53-
else if (c.type === 'suite') {
50+
if (c.type === 'test' || c.type === 'custom' || c.type === 'suite') {
5451
file.tasks.push(c)
5552
}
5653
else if (c.type === 'collector') {
5754
const suite = await c.collect(file)
58-
if (suite.name || suite.tasks.length)
55+
if (suite.name || suite.tasks.length) {
56+
mergeHooks(fileHooks, getHooks(suite))
5957
file.tasks.push(suite)
58+
}
59+
}
60+
else {
61+
// check that types are exhausted
62+
c satisfies never
6063
}
6164
}
65+
66+
setHooks(file, fileHooks)
6267
file.collectDuration = now() - collectStart
6368
}
6469
catch (e) {
@@ -74,8 +79,23 @@ export async function collectTests(paths: string[], runner: VitestRunner): Promi
7479
const hasOnlyTasks = someTasksAreOnly(file)
7580
interpretTaskModes(file, config.testNamePattern, hasOnlyTasks, false, config.allowOnly)
7681

82+
file.tasks.forEach((task) => {
83+
// task.suite refers to the internal default suite object
84+
// it should not be reported
85+
if (task.suite?.id === '')
86+
delete task.suite
87+
})
7788
files.push(file)
7889
}
7990

8091
return files
8192
}
93+
94+
function mergeHooks(baseHooks: SuiteHooks, hooks: SuiteHooks): SuiteHooks {
95+
for (const _key in hooks) {
96+
const key = _key as keyof SuiteHooks
97+
baseHooks[key].push(...hooks[key] as any)
98+
}
99+
100+
return baseHooks
101+
}

packages/runner/src/run.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,14 @@ export async function callSuiteHook<T extends keyof SuiteHooks>(
5757
const sequence = runner.config.sequence.hooks
5858

5959
const callbacks: HookCleanupCallback[] = []
60-
if (name === 'beforeEach' && suite.suite) {
60+
// stop at file level
61+
const parentSuite: Suite | null = 'filepath' in suite
62+
? null
63+
: (suite.suite || suite.file)
64+
65+
if (name === 'beforeEach' && parentSuite) {
6166
callbacks.push(
62-
...await callSuiteHook(suite.suite, currentTask, name, runner, args),
67+
...await callSuiteHook(parentSuite, currentTask, name, runner, args),
6368
)
6469
}
6570

@@ -77,9 +82,9 @@ export async function callSuiteHook<T extends keyof SuiteHooks>(
7782

7883
updateSuiteHookState(currentTask, name, 'pass', runner)
7984

80-
if (name === 'afterEach' && suite.suite) {
85+
if (name === 'afterEach' && parentSuite) {
8186
callbacks.push(
82-
...await callSuiteHook(suite.suite, currentTask, name, runner, args),
87+
...await callSuiteHook(parentSuite, currentTask, name, runner, args),
8388
)
8489
}
8590

@@ -150,6 +155,8 @@ export async function runTest(test: Test | Custom, runner: VitestRunner) {
150155

151156
setCurrentTest(test)
152157

158+
const suite = test.suite || test.file
159+
153160
const repeats = test.repeats ?? 0
154161
for (let repeatCount = 0; repeatCount <= repeats; repeatCount++) {
155162
const retry = test.retry ?? 0
@@ -160,7 +167,7 @@ export async function runTest(test: Test | Custom, runner: VitestRunner) {
160167

161168
test.result.repeatCount = repeatCount
162169

163-
beforeEachCleanups = await callSuiteHook(test.suite, test, 'beforeEach', runner, [test.context, test.suite])
170+
beforeEachCleanups = await callSuiteHook(suite, test, 'beforeEach', runner, [test.context, suite])
164171

165172
if (runner.runTask) {
166173
await runner.runTask(test)
@@ -202,7 +209,7 @@ export async function runTest(test: Test | Custom, runner: VitestRunner) {
202209
}
203210

204211
try {
205-
await callSuiteHook(test.suite, test, 'afterEach', runner, [test.context, test.suite])
212+
await callSuiteHook(suite, test, 'afterEach', runner, [test.context, suite])
206213
await callCleanupHooks(beforeEachCleanups)
207214
await callFixtureCleanup(test.context)
208215
}

packages/runner/src/suite.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,15 @@ export function getRunner() {
4040
return runner
4141
}
4242

43+
function createDefaultSuite(runner: VitestRunner) {
44+
const config = runner.config.sequence
45+
const api = config.shuffle ? suite.shuffle : suite
46+
return api('', { concurrent: config.concurrent }, () => {})
47+
}
48+
4349
export function clearCollectorContext(filepath: string, currentRunner: VitestRunner) {
4450
if (!defaultSuite)
45-
defaultSuite = currentRunner.config.sequence.shuffle ? suite.shuffle('') : currentRunner.config.sequence.concurrent ? suite.concurrent('') : suite('')
51+
defaultSuite = createDefaultSuite(currentRunner)
4652
runner = currentRunner
4753
currentTestFilepath = filepath
4854
collectorContext.tasks.length = 0
@@ -121,6 +127,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
121127
fails: options.fails,
122128
context: undefined!,
123129
type: 'custom',
130+
file: undefined!,
124131
retry: options.retry ?? runner.config.retry,
125132
repeats: options.repeats,
126133
mode: options.only ? 'only' : options.skip ? 'skip' : options.todo ? 'todo' : 'run',
@@ -211,10 +218,10 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
211218
name,
212219
mode,
213220
each,
221+
file: undefined!,
214222
shuffle,
215223
tasks: [],
216224
meta: Object.create(null),
217-
projectName: '',
218225
}
219226

220227
if (runner && includeLocation && runner.config.includeTaskLocation) {
@@ -236,7 +243,10 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
236243
initSuite(false)
237244
}
238245

239-
async function collect(file?: File) {
246+
async function collect(file: File) {
247+
if (!file)
248+
throw new TypeError('File is required to collect tasks.')
249+
240250
factoryQueue.length = 0
241251
if (factory)
242252
await runWithSuite(collector, () => factory(test))
@@ -251,8 +261,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
251261

252262
allChildren.forEach((task) => {
253263
task.suite = suite
254-
if (file)
255-
task.file = file
264+
task.file = file
256265
})
257266

258267
return suite

packages/runner/src/types/tasks.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export interface TaskBase {
1414
concurrent?: boolean
1515
shuffle?: boolean
1616
suite?: Suite
17-
file?: File
1817
result?: TaskResult
1918
retry?: number
2019
repeats?: number
@@ -25,7 +24,7 @@ export interface TaskBase {
2524
}
2625

2726
export interface TaskPopulated extends TaskBase {
28-
suite: Suite
27+
file: File
2928
pending?: boolean
3029
result?: TaskResult
3130
fails?: boolean
@@ -54,14 +53,14 @@ export interface TaskResult {
5453
export type TaskResultPack = [id: string, result: TaskResult | undefined, meta: TaskMeta]
5554

5655
export interface Suite extends TaskBase {
56+
file: File
5757
type: 'suite'
5858
tasks: Task[]
59-
filepath?: string
60-
projectName: string
6159
}
6260

6361
export interface File extends Suite {
6462
filepath: string
63+
projectName: string
6564
collectDuration?: number
6665
setupDuration?: number
6766
}
@@ -301,7 +300,7 @@ export interface SuiteCollector<ExtraContext = {}> {
301300
test: TestAPI<ExtraContext>
302301
tasks: (Suite | Custom<ExtraContext> | Test<ExtraContext> | SuiteCollector<ExtraContext>)[]
303302
task: (name: string, options?: TaskCustomOptions) => Custom<ExtraContext>
304-
collect: (file?: File) => Promise<Suite>
303+
collect: (file: File) => Promise<Suite>
305304
clear: () => void
306305
on: <T extends keyof SuiteHooks<ExtraContext>>(name: T, ...fn: SuiteHooks<ExtraContext>[T]) => void
307306
}

packages/runner/src/utils/tasks.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,14 @@ export function getNames(task: Task) {
4848
const names = [task.name]
4949
let current: Task | undefined = task
5050

51-
while (current?.suite || current?.file) {
52-
current = current.suite || current.file
51+
while (current?.suite) {
52+
current = current.suite
5353
if (current?.name)
5454
names.unshift(current.name)
5555
}
5656

57+
if (current !== task.file)
58+
names.unshift(task.file.name)
59+
5760
return names
5861
}

packages/vitest/src/integrations/chai/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { getCurrentTest } from '@vitest/runner'
77
import { ASYMMETRIC_MATCHERS_OBJECT, GLOBAL_EXPECT, addCustomEqualityTesters, getState, setState } from '@vitest/expect'
88
import type { Assertion, ExpectStatic } from '@vitest/expect'
99
import type { MatcherState } from '../../types/chai'
10-
import { getFullName } from '../../utils/tasks'
11-
import { getCurrentEnvironment } from '../../utils/global'
10+
import { getTestName } from '../../utils/tasks'
11+
import { getCurrentEnvironment, getWorkerState } from '../../utils/global'
1212

1313
export function createExpect(test?: TaskPopulated) {
1414
const expect = ((value: any, message?: string): Assertion => {
@@ -31,6 +31,7 @@ export function createExpect(test?: TaskPopulated) {
3131
// @ts-expect-error global is not typed
3232
const globalState = getState(globalThis[GLOBAL_EXPECT]) || {}
3333

34+
const testPath = getTestFile(test)
3435
setState<MatcherState>({
3536
// this should also add "snapshotState" that is added conditionally
3637
...globalState,
@@ -40,8 +41,8 @@ export function createExpect(test?: TaskPopulated) {
4041
expectedAssertionsNumber: null,
4142
expectedAssertionsNumberErrorGen: null,
4243
environment: getCurrentEnvironment(),
43-
testPath: test ? test.suite.file?.filepath : globalState.testPath,
44-
currentTestName: test ? getFullName(test as Test) : globalState.currentTestName,
44+
testPath,
45+
currentTestName: test ? getTestName(test as Test) : globalState.currentTestName,
4546
}, expect)
4647

4748
// @ts-expect-error untyped
@@ -89,6 +90,13 @@ export function createExpect(test?: TaskPopulated) {
8990
return expect
9091
}
9192

93+
function getTestFile(test?: TaskPopulated) {
94+
if (test)
95+
return test.file.filepath
96+
const state = getWorkerState()
97+
return state.filepath
98+
}
99+
92100
const globalExpect = createExpect()
93101

94102
Object.defineProperty(globalThis, GLOBAL_EXPECT, {

packages/vitest/src/integrations/snapshot/chai.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => {
4242
if (!test)
4343
return {}
4444
return {
45-
filepath: test.file?.filepath,
45+
filepath: test.file.filepath,
4646
name: getNames(test).slice(1).join(' > '),
4747
}
4848
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ export abstract class BaseReporter implements Reporter {
326326

327327
logger.log(`\n${c.cyan(c.inverse(c.bold(' BENCH ')))} ${c.cyan('Summary')}\n`)
328328
for (const bench of topBenches) {
329-
const group = bench.suite
329+
const group = bench.suite || bench.file
330330
if (!group)
331331
continue
332332
const groupName = getFullName(group, c.dim(' > '))

packages/vitest/src/node/reporters/junit.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ export class JUnitReporter implements Reporter {
248248
// NOTE: not used in JUnitReporter
249249
context: null as any,
250250
suite: null as any,
251+
file: null as any,
251252
} satisfies Task)
252253
}
253254

packages/vitest/src/node/reporters/renderers/listRenderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ function renderTree(tasks: Task[], options: ListRendererOptions, level = 0, maxR
9696
let suffix = ''
9797
let prefix = ` ${getStateSymbol(task)} `
9898

99-
if (level === 0 && task.type === 'suite' && task.projectName)
99+
if (level === 0 && task.type === 'suite' && 'projectName' in task)
100100
prefix += formatProjectName(task.projectName)
101101

102102
if (task.type === 'test' && task.result?.retryCount && task.result.retryCount > 0)

0 commit comments

Comments
 (0)