Skip to content

Commit

Permalink
feat: pass down meta information to Node.js process (#3449)
Browse files Browse the repository at this point in the history
Co-authored-by: Anjorin Damilare <damilareanjorin1@gmail.com>
  • Loading branch information
sheremet-va and dammy001 committed May 30, 2023
1 parent 39432e8 commit e39adea
Show file tree
Hide file tree
Showing 24 changed files with 417 additions and 159 deletions.
4 changes: 4 additions & 0 deletions docs/.vitepress/config.ts
Expand Up @@ -128,6 +128,10 @@ export default withPwa(defineConfig({
text: 'Runner API',
link: '/advanced/runner',
},
{
text: 'Task Metadata',
link: '/advanced/metadata',
},
],
},
],
Expand Down
74 changes: 74 additions & 0 deletions docs/advanced/metadata.md
@@ -0,0 +1,74 @@
# Task Metadata

::: warning
Vitest exposes experimental private API. Breaking changes might not follow semver, please pin Vitest's version when using it.
:::

If you are developing a custom reporter or using Vitest Node.js API, you might find it useful to pass data from tests that are being executed in various contexts to your reporter or custom Vitest handler.

To accomplish this, relying on the [test context](/guide/test-context) is not feasible since it cannot be serialized. However, with Vitest, you can utilize the `meta` property available on every task (suite or test) to share data between your tests and the Node.js process. It's important to note that this communication is one-way only, as the `meta` property can only be modified from within the test context. Any changes made within the Node.js context will not be visible in your tests.

You can populate `meta` property on test context or inside `beforeAll`/`afterAll` hooks for suite tasks.

```ts
afterAll((suite) => {
suite.meta.done = true
})

test('custom', ({ task }) => {
task.meta.custom = 'some-custom-handler'
})
```

Once a test is completed, Vitest will send a task including the result and `meta` to the Node.js process using RPC. To intercept and process this task, you can utilize the `onTaskUpdate` method available in your reporter implementation:

```ts
// custom-reporter.js
export default {
// you can intercept packs if needed
onTaskUpdate(packs) {
const [id, result, meta] = packs[0]
},
// meta is located on every task inside "onFinished"
onFinished(files) {
files[0].meta.done === true
files[0].tasks[0].meta.custom === 'some-custom-handler'
}
}
```

::: warning
Vitest can send several tasks at the same time if several tests are completed in a short period of time.
:::

::: danger BEWARE
Vitest uses different methods to communicate with the Node.js process.

- If Vitest runs tests inside worker threads, it will send data via [message port](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort)
- If Vitest uses child process, the data will be send as a serialized Buffer via [`process.send`](https://nodejs.org/api/process.html#processsendmessage-sendhandle-options-callback) API
- If Vitest run tests in the browser, the data will be stringified using [flatted](https://www.npmjs.com/package/flatted) package

The general rule of thumb is that you can send almost anything, except for functions, Promises, regexp (`v8.stringify` cannot serialize it, but you can send a string version and parse it in the Node.js process yourself), and other non-serializable data, but you can have cyclic references inside.

Also, make sure you serialize [Error properties](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#error_types) before you set them.
:::

You can also get this information from Vitest state when tests finished running:

```ts
const vitest = await createVitest('test')
await vitest.start()
vitest.state.getFiles()[0].meta.done === true
vitest.state.getFiles()[0].tasks[0].meta.custom === 'some-custom-handler'
```

It's also possible to extend type definitions when using TypeScript:

```ts
declare module 'vitest' {
interface TaskMeta {
done?: boolean
custom?: string
}
}
```
4 changes: 2 additions & 2 deletions docs/guide/test-context.md
Expand Up @@ -15,13 +15,13 @@ import { it } from 'vitest'

it('should work', (ctx) => {
// prints name of the test
console.log(ctx.meta.name)
console.log(ctx.task.name)
})
```

## Built-in Test Context

#### `context.meta`
#### `context.task`

A readonly object containing metadata about the test.

Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/client/runner.ts
@@ -1,4 +1,4 @@
import type { File, TaskResult, Test } from '@vitest/runner'
import type { File, TaskResultPack, Test } from '@vitest/runner'
import { rpc } from './rpc'
import type { ResolvedConfig } from '#types'

Expand Down Expand Up @@ -49,7 +49,7 @@ export function createBrowserRunner(original: any, coverageModule: CoverageHandl
return rpc().onCollected(files)
}

onTaskUpdate(task: [string, TaskResult | undefined][]): Promise<void> {
onTaskUpdate(task: TaskResultPack[]): Promise<void> {
return rpc().onTaskUpdate(task)
}

Expand Down
1 change: 1 addition & 0 deletions packages/runner/src/collect.ts
Expand Up @@ -24,6 +24,7 @@ export async function collectTests(paths: string[], runner: VitestRunner): Promi
mode: 'run',
filepath,
tasks: [],
meta: Object.create(null),
projectName: config.name,
}

Expand Down
1 change: 1 addition & 0 deletions packages/runner/src/context.ts
Expand Up @@ -47,6 +47,7 @@ export function createTestContext(test: Test, runner: VitestRunner): TestContext
} as unknown as TestContext

context.meta = test
context.task = test

context.onTestFailed = (fn) => {
test.onFailed ||= []
Expand Down
15 changes: 11 additions & 4 deletions packages/runner/src/run.ts
@@ -1,7 +1,7 @@
import limit from 'p-limit'
import { getSafeTimers, shuffle } from '@vitest/utils'
import type { VitestRunner } from './types/runner'
import type { File, HookCleanupCallback, HookListener, SequenceHooks, Suite, SuiteHooks, Task, TaskResult, TaskState, Test } from './types'
import type { File, HookCleanupCallback, HookListener, SequenceHooks, Suite, SuiteHooks, Task, TaskMeta, TaskResult, TaskResultPack, TaskState, Test } from './types'
import { partitionSuiteChildren } from './utils/suite'
import { getFn, getHooks } from './map'
import { collectTests } from './collect'
Expand Down Expand Up @@ -70,12 +70,12 @@ export async function callSuiteHook<T extends keyof SuiteHooks>(
return callbacks
}

const packs = new Map<string, TaskResult | undefined>()
const packs = new Map<string, [TaskResult | undefined, TaskMeta]>()
let updateTimer: any
let previousUpdate: Promise<void> | undefined

export function updateTask(task: Task, runner: VitestRunner) {
packs.set(task.id, task.result)
packs.set(task.id, [task.result, task.meta])

const { clearTimeout, setTimeout } = getSafeTimers()

Expand All @@ -91,7 +91,14 @@ async function sendTasksUpdate(runner: VitestRunner) {
await previousUpdate

if (packs.size) {
const p = runner.onTaskUpdate?.(Array.from(packs))
const taskPacks = Array.from(packs).map<TaskResultPack>(([id, task]) => {
return [
id,
task[0],
task[1],
]
})
const p = runner.onTaskUpdate?.(taskPacks)
packs.clear()
return p
}
Expand Down
3 changes: 3 additions & 0 deletions packages/runner/src/suite.ts
Expand Up @@ -87,6 +87,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
fails: this.fails,
retry: options?.retry,
repeats: options?.repeats,
meta: Object.create(null),
} as Omit<Test, 'context'> as Test

if (this.concurrent || concurrent)
Expand Down Expand Up @@ -116,6 +117,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
name,
type: 'custom',
mode: self.only ? 'only' : self.skip ? 'skip' : self.todo ? 'todo' : 'run',
meta: Object.create(null),
}
tasks.push(task)
return task
Expand Down Expand Up @@ -150,6 +152,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
each,
shuffle,
tasks: [],
meta: Object.create(null),
}

setHooks(suite, createSuiteHooks())
Expand Down
4 changes: 2 additions & 2 deletions packages/runner/src/types/runner.ts
@@ -1,4 +1,4 @@
import type { File, SequenceHooks, SequenceSetupFiles, Suite, TaskResult, Test, TestContext } from './tasks'
import type { File, SequenceHooks, SequenceSetupFiles, Suite, TaskResultPack, Test, TestContext } from './tasks'

export interface VitestRunnerConfig {
root: string
Expand Down Expand Up @@ -86,7 +86,7 @@ export interface VitestRunner {
/**
* Called, when a task is updated. The same as "onTaskUpdate" in a reporter, but this is running in the same thread as tests.
*/
onTaskUpdate?(task: [string, TaskResult | undefined][]): Promise<void>
onTaskUpdate?(task: TaskResultPack[]): Promise<void>

/**
* Called before running all tests in collected paths.
Expand Down
21 changes: 15 additions & 6 deletions packages/runner/src/types/tasks.ts
Expand Up @@ -9,17 +9,19 @@ export interface TaskBase {
id: string
name: string
mode: RunMode
meta: TaskMeta
each?: boolean
concurrent?: boolean
shuffle?: boolean
suite?: Suite
file?: File
result?: TaskResult
retry?: number
meta?: any
repeats?: number
}

export interface TaskMeta {}

export interface TaskCustom extends TaskBase {
type: 'custom'
}
Expand All @@ -40,7 +42,7 @@ export interface TaskResult {
repeatCount?: number
}

export type TaskResultPack = [id: string, result: TaskResult | undefined]
export type TaskResultPack = [id: string, result: TaskResult | undefined, meta: TaskMeta]

export interface Suite extends TaskBase {
type: 'suite'
Expand Down Expand Up @@ -205,10 +207,10 @@ export type HookListener<T extends any[], Return = void> = (...args: T) => Await
export type HookCleanupCallback = (() => Awaitable<unknown>) | void

export interface SuiteHooks<ExtraContext = {}> {
beforeAll: HookListener<[Suite | File], HookCleanupCallback>[]
afterAll: HookListener<[Suite | File]>[]
beforeEach: HookListener<[TestContext & ExtraContext, Suite], HookCleanupCallback>[]
afterEach: HookListener<[TestContext & ExtraContext, Suite]>[]
beforeAll: HookListener<[Readonly<Suite | File>], HookCleanupCallback>[]
afterAll: HookListener<[Readonly<Suite | File>]>[]
beforeEach: HookListener<[TestContext & ExtraContext, Readonly<Suite>], HookCleanupCallback>[]
afterEach: HookListener<[TestContext & ExtraContext, Readonly<Suite>]>[]
}

export interface SuiteCollector<ExtraContext = {}> {
Expand All @@ -234,9 +236,16 @@ export interface RuntimeContext {
export interface TestContext {
/**
* Metadata of the current test
*
* @deprecated Use `task` instead
*/
meta: Readonly<Test>

/**
* Metadata of the current test
*/
task: Readonly<Test>

/**
* Extract hooks on test failed
*/
Expand Down
10 changes: 7 additions & 3 deletions packages/vitest/src/node/state.ts
Expand Up @@ -117,9 +117,12 @@ export class StateManager {
}

updateTasks(packs: TaskResultPack[]) {
for (const [id, result] of packs) {
if (this.idMap.has(id))
this.idMap.get(id)!.result = result
for (const [id, result, meta] of packs) {
const task = this.idMap.get(id)
if (task) {
task.result = result
task.meta = meta
}
}
}

Expand All @@ -146,6 +149,7 @@ export class StateManager {
result: {
state: 'skip',
},
meta: {},
// Cancelled files have not yet collected tests
tasks: [],
})))
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/typecheck/typechecker.ts
Expand Up @@ -290,6 +290,6 @@ export class Typechecker {
return Object.values(this._tests || {})
.map(({ file }) => getTasks(file))
.flat()
.map(i => [i.id, undefined] as TaskResultPack)
.map<TaskResultPack>(i => [i.id, undefined, { typecheck: true }])
}
}
5 changes: 5 additions & 0 deletions packages/vitest/src/types/global.ts
Expand Up @@ -34,6 +34,11 @@ declare module '@vitest/runner' {
expect: ExpectStatic
}

interface TaskMeta {
typecheck?: boolean
benchmark?: boolean
}

interface File {
prepareDuration?: number
environmentLoad?: number
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/types/tasks.ts
Expand Up @@ -21,4 +21,5 @@ export type {
RuntimeContext,
TestContext,
OnTestFailedHandler,
} from '@vitest/runner/types'
TaskMeta,
} from '@vitest/runner'

0 comments on commit e39adea

Please sign in to comment.