diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 010cff2d6db0..792b9717d454 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -116,6 +116,8 @@ jobs:
- name: Test
run: pnpm run test:ci
+ env:
+ VITEST_CI_BLOB_LABEL: ${{ matrix.os }}-node-${{ matrix.node_version }}
- name: Test Examples
run: pnpm run test:examples
@@ -130,6 +132,17 @@ jobs:
path: test/ui/test-results/
retention-days: 30
+ - uses: actions/upload-artifact@v7
+ if: ${{ !cancelled() }}
+ with:
+ name: vitest-blob-${{ matrix.os }}-node-${{ matrix.node_version }}
+ path: |
+ README.md
+ test/core/.vitest-reports
+ test/cli/.vitest-reports
+ retention-days: 1
+ include-hidden-files: true
+
test-cached:
needs: changed
name: 'Cache&Test: node-${{ matrix.node_version }}, ${{ matrix.os }}'
@@ -251,3 +264,48 @@ jobs:
name: playwright-report-rolldown
path: rolldown/test/ui/test-results/
retention-days: 30
+
+ merge-reports:
+ needs: test
+ if: ${{ !cancelled() }}
+ runs-on: ubuntu-latest
+ name: Merge Reports
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: ./.github/actions/setup-and-cache
+
+ - name: Install
+ run: pnpm i
+
+ - name: Build
+ run: pnpm run build
+
+ - uses: actions/download-artifact@v4
+ with:
+ pattern: vitest-blob-*
+ merge-multiple: true
+
+ - name: Merge reports
+ continue-on-error: true
+ run: pnpm --filter=./test/core --filter=./test/cli --no-bail --sequential test --merge-reports --reporter=html
+
+ - name: Merge reports html
+ id: merge-html
+ run: |
+ mkdir -p html-all
+ cp -rf test/core/html html-all/core
+ cp -rf test/cli/html html-all/cli
+ echo "short_sha=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT
+
+ - uses: actions/upload-artifact@v7
+ id: upload-report
+ with:
+ name: vitest-ci-report-${{ steps.merge-html.outputs.short_sha }}
+ path: html-all
+ retention-days: 7
+
+ - name: Viewer link in summary
+ run: |
+ echo "[View HTML report](https://viewer.vitest.dev/?url=${{ steps.upload-report.outputs.artifact-url }})" >> $GITHUB_STEP_SUMMARY
diff --git a/docs/guide/cli.md b/docs/guide/cli.md
index 014731736a30..6bcad4cf1a03 100644
--- a/docs/guide/cli.md
+++ b/docs/guide/cli.md
@@ -225,7 +225,7 @@ You cannot use this option with `--watch` enabled (enabled in dev by default).
:::
::: tip
-If `--reporter=blob` is used without an output file, the default path will include the current shard config to avoid collisions with other Vitest processes.
+If `--reporter=blob` is used without an output file, the default path will include the current shard config and blob label from `VITEST_BLOB_LABEL` or the blob reporter `label` option to avoid collisions with other Vitest processes.
:::
### merge-reports
diff --git a/docs/guide/improving-performance.md b/docs/guide/improving-performance.md
index efbbc804d3ac..82556b187f8a 100644
--- a/docs/guide/improving-performance.md
+++ b/docs/guide/improving-performance.md
@@ -132,6 +132,12 @@ Collect the results stored in `.vitest-reports` directory from each machine and
vitest run --merge-reports
```
+When running the same shards across multiple environments, set the `VITEST_BLOB_LABEL` environment variable so merged reports can display them separately:
+
+```sh
+VITEST_BLOB_LABEL=linux vitest run --reporter=blob --shard=1/3
+```
+
::: details GitHub Actions example
This setup is also used at https://github.com/vitest-tests/test-sharding.
@@ -144,9 +150,10 @@ on:
- main
jobs:
tests:
- runs-on: ubuntu-latest
+ runs-on: ${{ matrix.os }}
strategy:
matrix:
+ os: [ubuntu-latest, macos-latest]
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
@@ -163,12 +170,14 @@ jobs:
- name: Run tests
run: pnpm run test --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
+ env:
+ VITEST_BLOB_LABEL: ${{ matrix.os }}
- name: Upload blob report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
- name: blob-report-${{ matrix.shardIndex }}
+ name: blob-report-${{ matrix.os }}-${{ matrix.shardIndex }}
path: .vitest-reports/*
include-hidden-files: true
retention-days: 1
@@ -177,7 +186,7 @@ jobs:
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
- name: blob-attachments-${{ matrix.shardIndex }}
+ name: blob-attachments-${{ matrix.os }}-${{ matrix.shardIndex }}
path: .vitest-attachments/**
include-hidden-files: true
retention-days: 1
diff --git a/docs/guide/reporters.md b/docs/guide/reporters.md
index 34307954fc15..1364beffee77 100644
--- a/docs/guide/reporters.md
+++ b/docs/guide/reporters.md
@@ -687,13 +687,32 @@ By default, stores all results in `.vitest-reports` folder, but can be overridde
npx vitest --reporter=blob --outputFile=reports/blob-1.json
```
-We recommend using this reporter if you are running Vitest on different machines with the [`--shard`](/guide/cli#shard) flag.
-All blob reports can be merged into any report by using `--merge-reports` command at the end of your CI pipeline:
+We recommend using this reporter if you are running Vitest on different machines with the [`--shard`](/guide/cli#shard) flag or across multiple environments (e.g., linux/macos/windows). All blob reports can be merged into any report by using `--merge-reports` command at the end of your CI pipeline:
```bash
npx vitest --merge-reports=reports --reporter=json --reporter=default
```
+When running the same tests across multiple environments, use the `VITEST_BLOB_LABEL` environment variable to distinguish each environment's blob. Vitest reads labels at merge time and displays results separately:
+
+```bash
+VITEST_BLOB_LABEL=linux vitest run --reporter=blob
+```
+
+You can also provide the label via the blob reporter option:
+
+```ts [vitest.config.ts]
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ reporters: [
+ ['blob', { label: 'linux' }],
+ ],
+ },
+})
+```
+
Blob reporter output doesn't include file-based [attachments](/api/advanced/artifacts.html#testattachment).
Make sure to merge [`attachmentsDir`](/config/attachmentsdir) separately alongside blob reports on CI when using this feature.
diff --git a/packages/runner/src/collect.ts b/packages/runner/src/collect.ts
index b422a3079400..7d89da6a2915 100644
--- a/packages/runner/src/collect.ts
+++ b/packages/runner/src/collect.ts
@@ -47,7 +47,14 @@ export async function collectTests(
const fileTags: string[] = typeof spec === 'string' ? [] : (spec.fileTags || [])
- const file = createFileTask(filepath, config.root, config.name, runner.pool, runner.viteEnvironment)
+ const file = createFileTask(
+ filepath,
+ config.root,
+ config.name,
+ runner.pool,
+ runner.viteEnvironment,
+ { __vitest_label__: config.mergeReportsLabel },
+ )
file.tags = fileTags
file.shuffle = config.sequence.shuffle
diff --git a/packages/runner/src/types/runner.ts b/packages/runner/src/types/runner.ts
index d05ab8056004..cd3fd571ab38 100644
--- a/packages/runner/src/types/runner.ts
+++ b/packages/runner/src/types/runner.ts
@@ -45,6 +45,7 @@ export interface VitestRunnerConfig {
tags: TestTagDefinition[]
tagsFilter: string[] | undefined
strictTags: boolean
+ mergeReportsLabel: string | undefined
}
/**
diff --git a/packages/runner/src/utils/collect.ts b/packages/runner/src/utils/collect.ts
index 4c741ac2f478..0a727488708f 100644
--- a/packages/runner/src/utils/collect.ts
+++ b/packages/runner/src/utils/collect.ts
@@ -193,23 +193,29 @@ export function calculateSuiteHash(parent: Suite): void {
})
}
+interface HashMeta {
+ typecheck?: boolean
+ __vitest_label__?: string
+}
+
export function createFileTask(
filepath: string,
root: string,
projectName: string | undefined,
pool?: string,
viteEnvironment?: string,
+ meta?: HashMeta,
): File {
const path = relative(root, filepath)
const file: File = {
- id: generateFileHash(path, projectName),
+ id: generateFileHash(path, projectName, meta),
name: path,
fullName: path,
type: 'suite',
mode: 'queued',
filepath,
tasks: [],
- meta: Object.create(null),
+ meta: Object.assign(Object.create(null), meta),
projectName,
file: undefined!,
pool,
@@ -228,8 +234,15 @@ export function createFileTask(
export function generateFileHash(
file: string,
projectName: string | undefined,
+ meta?: HashMeta,
): string {
- return /* @__PURE__ */ generateHash(`${file}${projectName || ''}`)
+ const seed = [
+ file,
+ projectName || '',
+ meta?.typecheck ? '__typecheck__' : '',
+ meta?.__vitest_label__ || '',
+ ].join('\0')
+ return generateHash(seed)
}
export function findTestFileStackTrace(testFilePath: string, error: string): ParsedStack | undefined {
diff --git a/packages/ui/client/components/FileDetails.vue b/packages/ui/client/components/FileDetails.vue
index 0c9e65a06f8e..7b016cd542bc 100644
--- a/packages/ui/client/components/FileDetails.vue
+++ b/packages/ui/client/components/FileDetails.vue
@@ -60,6 +60,8 @@ const isTypecheck = computed(() => {
return !!current.value?.meta?.typecheck
})
+const label = computed(() => current.value?.meta?.__vitest_label__)
+
function open() {
const filePath = current.value?.filepath
if (filePath) {
@@ -206,6 +208,7 @@ const tags = computed(() => {
+
{{ label }}
{
+
{{ label }}
{{ projectName }}
diff --git a/packages/ui/client/components/views/ViewTestReport.vue b/packages/ui/client/components/views/ViewTestReport.vue
index 19e841bf6408..77c0146e1d4f 100644
--- a/packages/ui/client/components/views/ViewTestReport.vue
+++ b/packages/ui/client/components/views/ViewTestReport.vue
@@ -25,6 +25,7 @@ const failed = computed(() => {
const kWellKnownMeta = new Set([
'benchmark',
'typecheck',
+ 'label',
])
const meta = computed(() => {
return Object.entries(props.test.meta).filter(([name]) => {
diff --git a/packages/ui/client/composables/explorer/types.ts b/packages/ui/client/composables/explorer/types.ts
index a51056f3853e..16cc9305d3b1 100644
--- a/packages/ui/client/composables/explorer/types.ts
+++ b/packages/ui/client/composables/explorer/types.ts
@@ -63,6 +63,7 @@ export interface FileTreeNode extends ParentTreeNode {
type: 'file'
filepath: string
typecheck: boolean | undefined
+ label?: string
projectName?: string
projectNameColor: string
collectDuration?: number
diff --git a/packages/ui/client/composables/explorer/utils.ts b/packages/ui/client/composables/explorer/utils.ts
index 294ecb5a0026..2ff576033c80 100644
--- a/packages/ui/client/composables/explorer/utils.ts
+++ b/packages/ui/client/composables/explorer/utils.ts
@@ -84,6 +84,7 @@ export function createOrUpdateFileNode(
if (fileNode) {
fileNode.typecheck = !!file.meta && 'typecheck' in file.meta
+ fileNode.label = file.meta?.__vitest_label__
fileNode.state = file.result?.state
fileNode.mode = file.mode
fileNode.duration = typeof file.result?.duration === 'number' ? Math.round(file.result.duration) : undefined
@@ -106,6 +107,7 @@ export function createOrUpdateFileNode(
children: new Set(),
tasks: [],
typecheck: !!file.meta && 'typecheck' in file.meta,
+ label: file.meta?.__vitest_label__,
indent: 0,
duration: typeof file.result?.duration === 'number' ? Math.round(file.result.duration) : undefined,
slow: false,
diff --git a/packages/utils/src/helpers.ts b/packages/utils/src/helpers.ts
index 7d2d74cca233..22f1c0f7f59a 100644
--- a/packages/utils/src/helpers.ts
+++ b/packages/utils/src/helpers.ts
@@ -429,3 +429,8 @@ export function deepMerge(
export function unique(array: T[]): T[] {
return Array.from(new Set(array))
}
+
+export function sanitizeFilePath(s: string): string {
+ // eslint-disable-next-line no-control-regex
+ return s.replace(/[\x00-\x2C\x2E\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-')
+}
diff --git a/packages/vitest/src/node/ast-collect.ts b/packages/vitest/src/node/ast-collect.ts
index 4e12e8a1b37a..b307d14dfbec 100644
--- a/packages/vitest/src/node/ast-collect.ts
+++ b/packages/vitest/src/node/ast-collect.ts
@@ -1,13 +1,12 @@
import type { File, Suite, Task, Test } from '@vitest/runner'
-import type { SerializedConfig } from '../runtime/config'
import type { TestError } from '../types/general'
import type { TestProject } from './project'
import { promises as fs } from 'node:fs'
import { originalPositionFor, TraceMap } from '@jridgewell/trace-mapping'
import {
calculateSuiteHash,
+ createFileTask as createFileTaskOriginal,
createTaskName,
- generateHash,
validateTags,
} from '@vitest/runner/utils'
import { unique } from '@vitest/utils/helpers'
@@ -285,21 +284,20 @@ function astParseFile(filepath: string, code: string) {
}
export function createFailedFileTask(project: TestProject, filepath: string, error: Error): File {
- const testFilepath = relative(project.config.root, filepath)
- const file: ParsedFile = {
+ const config = project.serializedConfig
+ const baseFile = createFileTaskOriginal(
filepath,
- type: 'suite',
- id: /* @__PURE__ */ generateHash(`${testFilepath}${project.config.name || ''}`),
- name: testFilepath,
- fullName: testFilepath,
+ config.root,
+ config.name,
+ config.pool,
+ undefined,
+ { typecheck: config.pool === 'typescript', __vitest_label__: config.mergeReportsLabel },
+ )
+ const file: ParsedFile = {
+ ...baseFile,
mode: 'run',
- tasks: [],
start: 0,
end: 0,
- projectName: project.name,
- meta: {},
- pool: project.browser ? 'browser' : project.config.pool,
- file: null!,
result: {
state: 'fail',
errors: serializeError(project, error),
@@ -332,28 +330,28 @@ function serializeError(ctx: TestProject, error: any): TestError[] {
}
function createFileTask(
+ project: TestProject,
testFilepath: string,
code: string,
requestMap: any,
- config: SerializedConfig,
filepath: string,
fileTags: string[] | undefined,
) {
const { definitions, ast } = astParseFile(testFilepath, code)
- const file: ParsedFile = {
+ const config = project.serializedConfig
+ const baseFile = createFileTaskOriginal(
filepath,
- type: 'suite',
- id: /* @__PURE__ */ generateHash(`${testFilepath}${config.name || ''}`),
- name: testFilepath,
- fullName: testFilepath,
+ config.root,
+ config.name,
+ config.pool,
+ undefined,
+ { typecheck: config.pool === 'typescript', __vitest_label__: config.mergeReportsLabel },
+ )
+ const file: ParsedFile = {
+ ...baseFile,
mode: 'run',
- tasks: [],
start: ast.start,
end: ast.end,
- projectName: config.name,
- meta: {},
- pool: 'browser',
- file: null!,
tags: fileTags || [],
}
file.file = file
@@ -503,10 +501,10 @@ export async function astCollectTests(
)
}
return createFileTask(
+ project,
testFilepath,
request.code,
request.map,
- project.serializedConfig,
filepath,
request.fileTags,
)
diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts
index 081f2545bd67..b0c66c794348 100644
--- a/packages/vitest/src/node/config/resolveConfig.ts
+++ b/packages/vitest/src/node/config/resolveConfig.ts
@@ -768,6 +768,16 @@ export function resolveConfig(
}
}
+ resolved.mergeReportsLabel = process.env.VITEST_BLOB_LABEL
+ for (const reporter of resolved.reporters) {
+ if (Array.isArray(reporter) && reporter[0] === 'blob') {
+ const options = reporter[1] as any
+ if (options && typeof options.label === 'string') {
+ resolved.mergeReportsLabel = options.label
+ }
+ }
+ }
+
if (resolved.changed) {
resolved.passWithNoTests ??= true
}
diff --git a/packages/vitest/src/node/config/serializeConfig.ts b/packages/vitest/src/node/config/serializeConfig.ts
index c496766123d9..b482caa934d2 100644
--- a/packages/vitest/src/node/config/serializeConfig.ts
+++ b/packages/vitest/src/node/config/serializeConfig.ts
@@ -146,6 +146,7 @@ export function serializeConfig(project: TestProject): SerializedConfig {
tags: config.tags || [],
tagsFilter: config.tagsFilter,
strictTags: config.strictTags ?? true,
+ mergeReportsLabel: config.mergeReportsLabel,
slowTestThreshold:
config.slowTestThreshold
?? globalConfig.slowTestThreshold
diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts
index 93834a7f3806..3be2ae131351 100644
--- a/packages/vitest/src/node/core.ts
+++ b/packages/vitest/src/node/core.ts
@@ -620,7 +620,7 @@ export class Vitest {
const specifications: TestSpecification[] = []
for (const file of files) {
const project = this.getProjectByName(file.projectName || '')
- const specification = project.createSpecification(file.filepath, undefined, file.pool)
+ const specification = project.createSpecification(file.filepath, undefined, file.pool, file.meta)
specifications.push(specification)
}
diff --git a/packages/vitest/src/node/project.ts b/packages/vitest/src/node/project.ts
index b6f687db692e..55d52853222e 100644
--- a/packages/vitest/src/node/project.ts
+++ b/packages/vitest/src/node/project.ts
@@ -1,3 +1,4 @@
+import type { TaskMeta } from '@vitest/runner/types'
import type { GlobOptions } from 'tinyglobby'
import type { DevEnvironment, ViteDevServer, InlineConfig as ViteInlineConfig } from 'vite'
import type { ModuleRunner } from 'vite/module-runner'
@@ -152,12 +153,15 @@ export class TestProject {
locationsOrOptions?: number[] | TestSpecificationOptions | undefined,
/** @internal */
pool?: string,
+ /** @internal */
+ metaOverride?: TaskMeta,
): TestSpecification {
return new TestSpecification(
this,
moduleId,
pool || getFilePoolName(this),
locationsOrOptions,
+ metaOverride,
)
}
@@ -551,6 +555,7 @@ export class TestProject {
server.config,
)
this._config.api.token = this.vitest.config.api.token
+ this._config.mergeReportsLabel = this.vitest.config.mergeReportsLabel
this._setHash()
for (const _providedKey in this.config.provide) {
const providedKey = _providedKey as keyof ProvidedContext
diff --git a/packages/vitest/src/node/reporters/base.ts b/packages/vitest/src/node/reporters/base.ts
index edd1fd63b5e2..132a83a9dd40 100644
--- a/packages/vitest/src/node/reporters/base.ts
+++ b/packages/vitest/src/node/reporters/base.ts
@@ -335,6 +335,11 @@ export abstract class BaseReporter implements Reporter {
title += ` ${c.bgBlue(c.bold(' TS '))}`
}
+ const label = this.ctx.state.blobs && entity.task.file.meta.__vitest_label__
+ if (label) {
+ title += ` ${c.bgCyan(c.bold(` ${label} `))}`
+ }
+
return title
}
@@ -936,8 +941,9 @@ export abstract class BaseReporter implements Reporter {
name += c.dim(` [ ${this.relative(filepath)} ]`)
}
+ const label = this.ctx.state.blobs && task.file?.meta?.__vitest_label__
this.ctx.logger.error(
- `${c.bgRed(c.bold(' FAIL '))} ${formatProjectName(project)}${name}`,
+ `${c.bgRed(c.bold(' FAIL '))} ${formatProjectName(project)}${label ? `${c.bgCyan(c.bold(` ${label} `))} ` : ''}${name}`,
)
}
diff --git a/packages/vitest/src/node/reporters/blob.ts b/packages/vitest/src/node/reporters/blob.ts
index 32dd65948bd7..036fbdbf5f62 100644
--- a/packages/vitest/src/node/reporters/blob.ts
+++ b/packages/vitest/src/node/reporters/blob.ts
@@ -7,12 +7,14 @@ import type { Reporter } from '../types/reporter'
import type { TestModule } from './reported-tasks'
import { existsSync } from 'node:fs'
import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises'
+import { sanitizeFilePath } from '@vitest/utils/helpers'
import { parse, stringify } from 'flatted'
import { dirname, resolve } from 'pathe'
import { getOutputFile } from '../../utils/config-helpers'
export interface BlobOptions {
outputFile?: string
+ label?: string
}
export class BlobReporter implements Reporter {
@@ -50,9 +52,12 @@ export class BlobReporter implements Reporter {
= this.options.outputFile ?? getOutputFile(this.ctx.config, 'blob')
if (!outputFile) {
const shard = this.ctx.config.shard
- outputFile = shard
- ? `.vitest-reports/blob-${shard.index}-${shard.count}.json`
- : '.vitest-reports/blob.json'
+ const filename = [
+ 'blob',
+ this.ctx.config.mergeReportsLabel,
+ shard && `${shard.index}-${shard.count}`,
+ ].filter(Boolean).join('-')
+ outputFile = `.vitest-reports/${sanitizeFilePath(filename)}.json`
}
const environmentModules: MergeReportEnvironmentModules = {}
diff --git a/packages/vitest/src/node/reporters/summary.ts b/packages/vitest/src/node/reporters/summary.ts
index 26a236cd16ec..11ac84ee6918 100644
--- a/packages/vitest/src/node/reporters/summary.ts
+++ b/packages/vitest/src/node/reporters/summary.ts
@@ -38,7 +38,7 @@ interface RunningModule extends Pick {
projectColor: TestModule['project']['color']
hook?: Omit
tests: Map
- typecheck: boolean
+ meta: TestModule['task']['meta']
}
/**
@@ -288,11 +288,13 @@ export class SummaryReporter implements Reporter {
const summary = ['']
for (const testFile of Array.from(this.runningModules.values()).sort(sortRunningModules)) {
- const typecheck = testFile.typecheck ? `${c.bgBlue(c.bold(' TS '))} ` : ''
+ const typecheck = testFile.meta.typecheck ? `${c.bgBlue(c.bold(' TS '))} ` : ''
+ const label = this.ctx.state.blobs && testFile.meta.__vitest_label__ ? `${c.bgCyan(c.bold(` ${testFile.meta.__vitest_label__} `))} ` : ''
summary.push(
c.bold(c.yellow(` ${F_POINTER} `))
+ formatProjectName({ name: testFile.projectName, color: testFile.projectColor })
+ typecheck
+ + label
+ testFile.filename
+ c.dim(!testFile.completed && !testFile.total
? ' [queued]'
@@ -398,6 +400,6 @@ function initializeStats(module: TestModule): RunningModule {
projectName: module.project.name,
projectColor: module.project.color,
tests: new Map(),
- typecheck: !!module.task.meta.typecheck,
+ meta: module.task.meta,
}
}
diff --git a/packages/vitest/src/node/state.ts b/packages/vitest/src/node/state.ts
index 89159941c1ba..76ea5b71ce4d 100644
--- a/packages/vitest/src/node/state.ts
+++ b/packages/vitest/src/node/state.ts
@@ -145,17 +145,17 @@ export class StateManager {
collectFiles(project: TestProject, files: File[] = []): void {
files.forEach((file) => {
const existing = this.filesMap.get(file.filepath) || []
- const otherFiles = existing.filter(
- i => i.projectName !== file.projectName || i.meta.typecheck !== file.meta.typecheck,
- )
const currentFile = existing.find(
- i => i.projectName === file.projectName,
+ i => i.projectName === file.projectName
+ && i.meta.typecheck === file.meta.typecheck
+ && i.meta.__vitest_label__ === file.meta.__vitest_label__,
)
// keep logs for the previous file because it should always be initiated before the collections phase
// which means that all logs are collected during the collection and not inside tests
if (currentFile) {
file.logs = currentFile.logs
}
+ const otherFiles = existing.filter(i => i !== currentFile)
otherFiles.push(file)
this.filesMap.set(file.filepath, otherFiles)
this.updateId(file, project)
diff --git a/packages/vitest/src/node/test-run.ts b/packages/vitest/src/node/test-run.ts
index b130052dcfaa..6493627585dd 100644
--- a/packages/vitest/src/node/test-run.ts
+++ b/packages/vitest/src/node/test-run.ts
@@ -17,7 +17,7 @@ import assert from 'node:assert'
import { createHash } from 'node:crypto'
import { existsSync, readFileSync } from 'node:fs'
import { copyFile, mkdir, writeFile } from 'node:fs/promises'
-import { isPrimitive } from '@vitest/utils/helpers'
+import { isPrimitive, sanitizeFilePath } from '@vitest/utils/helpers'
import { serializeValue } from '@vitest/utils/serialize'
import { parseErrorStacktrace } from '@vitest/utils/source-map'
import { extractSourcemapFromFile } from '@vitest/utils/source-map/node'
@@ -311,8 +311,3 @@ export class TestRun {
}
}
}
-
-function sanitizeFilePath(s: string): string {
- // eslint-disable-next-line no-control-regex
- return s.replace(/[\x00-\x2C\x2E\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-')
-}
diff --git a/packages/vitest/src/node/test-specification.ts b/packages/vitest/src/node/test-specification.ts
index 74ef5d6a1f53..133251c347df 100644
--- a/packages/vitest/src/node/test-specification.ts
+++ b/packages/vitest/src/node/test-specification.ts
@@ -1,3 +1,4 @@
+import type { TaskMeta } from '@vitest/runner/types'
import type { SerializedTestSpecification } from '../runtime/types/utils'
import type { TestProject } from './project'
import type { TestModule } from './reporters/reported-tasks'
@@ -55,17 +56,14 @@ export class TestSpecification {
moduleId: string,
pool: Pool,
testLinesOrOptions?: number[] | TestSpecificationOptions | undefined,
+ // merge-reports uses the original `file.meta` from the test run
+ metaOverride?: TaskMeta,
) {
const projectName = project.config.name
- const hashName = pool !== 'typescript'
- ? projectName
- : projectName
- // https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/typecheck/collect.ts#L58
- ? `${projectName}:__typecheck__`
- : '__typecheck__'
this.taskId = generateFileHash(
relative(project.config.root, moduleId),
- hashName,
+ projectName,
+ metaOverride ?? { typecheck: pool === 'typescript', __vitest_label__: project.config.mergeReportsLabel },
)
this.project = project
this.moduleId = moduleId
diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts
index 7fad9efd8695..552423b6a476 100644
--- a/packages/vitest/src/node/types/config.ts
+++ b/packages/vitest/src/node/types/config.ts
@@ -1217,6 +1217,7 @@ export interface ResolvedConfig
vmMemoryLimit?: UserConfig['vmMemoryLimit']
dumpDir?: string
tagsFilter?: string[]
+ mergeReportsLabel?: string
experimental: Omit['experimental'], 'importDurations'> & {
importDurations: {
diff --git a/packages/vitest/src/runtime/config.ts b/packages/vitest/src/runtime/config.ts
index 27bdb2b75e30..43bfac7fc626 100644
--- a/packages/vitest/src/runtime/config.ts
+++ b/packages/vitest/src/runtime/config.ts
@@ -148,6 +148,7 @@ export interface SerializedConfig {
tags: TestTagDefinition[]
tagsFilter: string[] | undefined
strictTags: boolean
+ mergeReportsLabel: string | undefined
slowTestThreshold: number | undefined
isAgent: boolean
}
diff --git a/packages/vitest/src/typecheck/collect.ts b/packages/vitest/src/typecheck/collect.ts
index c6c5e571d90d..213512002293 100644
--- a/packages/vitest/src/typecheck/collect.ts
+++ b/packages/vitest/src/typecheck/collect.ts
@@ -3,13 +3,12 @@ import type { Rollup } from 'vite'
import type { TestProject } from '../node/project'
import {
calculateSuiteHash,
+ createFileTask,
createTaskName,
- generateHash,
interpretTaskModes,
someTasksAreOnly,
} from '@vitest/runner/utils'
import { ancestor as walkAst } from 'acorn-walk'
-import { relative } from 'pathe'
import { parseAstAsync } from 'vite'
interface ParsedFile extends File {
@@ -53,22 +52,19 @@ export async function collectTests(
return null
}
const ast = await parseAstAsync(request.code)
- const testFilepath = relative(ctx.config.root, filepath)
const projectName = ctx.name
- const typecheckSubprojectName = projectName ? `${projectName}:__typecheck__` : '__typecheck__'
const file: ParsedFile = {
- filepath,
- type: 'suite',
- id: generateHash(`${testFilepath}${typecheckSubprojectName}`),
- name: testFilepath,
- fullName: testFilepath,
- mode: 'run',
- tasks: [],
+ ...createFileTask(
+ filepath,
+ ctx.config.root,
+ projectName,
+ undefined,
+ undefined,
+ { typecheck: true },
+ ),
start: ast.start,
end: ast.end,
- projectName,
- meta: { typecheck: true },
- file: null!,
+ mode: 'run',
}
file.file = file
const definitions: LocalCallDefinition[] = []
diff --git a/packages/vitest/src/types/global.ts b/packages/vitest/src/types/global.ts
index 53297e36e85e..30140cf9a1bd 100644
--- a/packages/vitest/src/types/global.ts
+++ b/packages/vitest/src/types/global.ts
@@ -110,6 +110,7 @@ declare module '@vitest/runner' {
interface TaskMeta {
typecheck?: boolean
benchmark?: boolean
+ __vitest_label__?: string
}
interface File {
diff --git a/packages/ws-client/src/state.ts b/packages/ws-client/src/state.ts
index 4c93940c044c..b5a1d1762418 100644
--- a/packages/ws-client/src/state.ts
+++ b/packages/ws-client/src/state.ts
@@ -47,19 +47,19 @@ export class StateManager {
collectFiles(files: File[] = []): void {
files.forEach((file) => {
const existing = this.filesMap.get(file.filepath) || []
- const otherProject = existing.filter(
- i => i.projectName !== file.projectName || i.meta.typecheck !== file.meta.typecheck,
- )
const currentFile = existing.find(
- i => i.projectName === file.projectName,
+ i => i.projectName === file.projectName
+ && i.meta.typecheck === file.meta.typecheck
+ && i.meta.__vitest_label__ === file.meta.__vitest_label__,
)
// keep logs for the previous file because it should always be initiated before the collections phase
// which means that all logs are collected during the collection and not inside tests
if (currentFile) {
file.logs = currentFile.logs
}
- otherProject.push(file)
- this.filesMap.set(file.filepath, otherProject)
+ const otherFiles = existing.filter(i => i !== currentFile)
+ otherFiles.push(file)
+ this.filesMap.set(file.filepath, otherFiles)
this.updateId(file)
})
}
diff --git a/test/cli/test/reported-tasks.test.ts b/test/cli/test/reported-tasks.test.ts
index d5248674c291..4769fe082b27 100644
--- a/test/cli/test/reported-tasks.test.ts
+++ b/test/cli/test/reported-tasks.test.ts
@@ -320,20 +320,26 @@ it('can create new test specifications', ({ testModule }) => {
const testSuite = [...testModule.children.suites()][0]
const suiteSpec = testSuite.toTestSpecification()
expect(suiteSpec.moduleId).toBe(testModule.moduleId)
- expect(suiteSpec.testIds).toEqual([
- '-1008553841_11_0',
- '-1008553841_11_1',
- '-1008553841_11_2_0',
- '-1008553841_11_2_1',
- '-1008553841_11_2_2',
- '-1008553841_11_2_3',
- ])
+ expect(suiteSpec.testIds).toMatchInlineSnapshot(`
+ [
+ "1763725585_11_0",
+ "1763725585_11_1",
+ "1763725585_11_2_0",
+ "1763725585_11_2_1",
+ "1763725585_11_2_2",
+ "1763725585_11_2_3",
+ ]
+ `)
expect(suiteSpec.project).toBe(testModule.project)
const testCase = testSuite.children.at(0) as TestCase
const caseSpec = testCase.toTestSpecification()
expect(caseSpec.moduleId).toBe(testModule.moduleId)
- expect(caseSpec.testIds).toEqual(['-1008553841_11_0'])
+ expect(caseSpec.testIds).toMatchInlineSnapshot(`
+ [
+ "1763725585_11_0",
+ ]
+ `)
expect(caseSpec.project).toBe(testModule.project)
})
diff --git a/test/cli/test/reporters/__snapshots__/html.test.ts.snap b/test/cli/test/reporters/__snapshots__/html.test.ts.snap
index 8bf3047d364a..1a9f41e7524b 100644
--- a/test/cli/test/reporters/__snapshots__/html.test.ts.snap
+++ b/test/cli/test/reporters/__snapshots__/html.test.ts.snap
@@ -182,7 +182,7 @@ exports[`html reporter > resolves to "passing" status for test file "all-passing
"file": [Circular],
"fullName": "all-passing-or-skipped.test.ts > 3 + 3 = 6",
"fullTestName": "3 + 3 = 6",
- "id": "1111755131_1",
+ "id": "1804288165_1",
"location": {
"column": 6,
"line": 7,
diff --git a/test/cli/test/reporters/merge-reports.test.ts b/test/cli/test/reporters/merge-reports.test.ts
index 2f445b5c20f9..ba70d3a32ea3 100644
--- a/test/cli/test/reporters/merge-reports.test.ts
+++ b/test/cli/test/reporters/merge-reports.test.ts
@@ -1,8 +1,9 @@
+import type { RunVitestConfig } from '#test-utils'
import type { File, Test } from '@vitest/runner/types'
import type { TestUserConfig, Vitest } from 'vitest/node'
import { rmSync } from 'node:fs'
import { resolve } from 'node:path'
-import { runVitest } from '#test-utils'
+import { runVitest, useFS } from '#test-utils'
import { playwright } from '@vitest/browser-playwright'
import { createFileTask } from '@vitest/runner/utils'
import { beforeEach, expect, test } from 'vitest'
@@ -452,6 +453,7 @@ function trimReporterOutput(report: string) {
const rows = report
.replace(/\d+ms/g, '