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, '