feat: support merging reports for non-sharded multi-environment runs
As a developer using Vitest in CI with a matrix strategy (e.g., linux/macos/windows, or node 20/22), I want to merge test reports from multiple platform runs into a single report, so that I can get a unified view of test results across all environments.
Currently --merge-reports only supports merging sharded runs — where test files are split across shards with no overlap. When the same tests are run on multiple platforms (a common CI pattern), merging those blob reports produces duplicate unlabeled entries with no way to tell which result came from which platform.
Playwright supports this via their merge-reports + tag config, and it's a frequently needed workflow.
Use explicit tags to distinguish blobs from different environments. The blob reporter would accept a tag option that gets stored in the blob metadata and injected into each test's tags array during merge. This builds on Vitest's existing tag infrastructure (filtering, reporter display).
Blob reporter config:
// vitest.config.ts
export default defineConfig({
test: {
reporter: [['blob', {
tag: process.env.CI_PLATFORM, // e.g., "linux", "windows", "node-22"
}]],
},
})Merge behavior:
- Each blob carries its tag in metadata
- On merge, the tag is prepended to every test's
tagsarray (e.g.,["linux", ...]) - Same test file from different blobs becomes distinct entries, each labeled by platform
- Reporters already understand tags, so results are immediately distinguishable
Example CI workflow:
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- run: vitest run --reporter=blob
env:
CI_PLATFORM: ${{ matrix.os }}
- uses: actions/upload-artifact@v4
with:
name: blob-${{ matrix.os }}
path: .vitest-reports/
merge:
needs: test
steps:
- uses: actions/download-artifact@v4
with:
path: .vitest-reports/
merge-multiple: true
- run: vitest --merge-reports --reporter=default --reporter=json- Auto-detect platform (
process.platform,process.arch) and inject as tags without explicit config. Simpler for users but less flexible — doesn't cover non-OS dimensions like Node version or custom environments. - Playwright's approach: automatic ID deconfliction (salt duplicate IDs) + separate explicit
tagconfig for labeling. We could start with the simpler combined approach (tag = deconflict + label) and add automatic deconfliction later if needed.
Playwright has mature support for this via their merge-reports CLI + blob reporter.
- Blob format: ZIP files containing
report.jsonl(line-delimited JSON events) +resources/dir for attachments - Metadata per blob: version, userAgent,
name(environment label), shard info,pathSeparator - CLI:
npx playwright merge-reports [dir]with--reporterand--configoptions
Two separate concerns:
1. ID Deconfliction (automatic, "dumb")
IdsPatcher(merge.ts#L406-483) maintains a global set of test IDs- Any duplicate ID across blobs gets salted with the blob's index → becomes a unique entry
- This is purely mechanical — it doesn't know why duplicates exist
- For sharded runs, duplicates don't normally occur (files are split, not duplicated), so salting is a no-op
- For cross-platform runs, same tests run everywhere → all duplicates get salted → separate entries
2. Labeling (requires explicit user config)
- Without config, duplicated tests appear as separate entries but are unlabeled — you can't tell which came from which platform
- Users must set
TestConfig.tag(e.g.,tag: '@linux') to distinguish environments - Legacy:
PWTEST_BOT_NAMEenv var sets the blob'snamefield (being deprecated,blob.tsline 55:"TODO: remove after some time, recommend config.tag instead") - Tags are prepended to test tags on merge, e.g.,
@linux,@windows GlobalErrorPatcheralso tags unhandled errors with the environment name
From Playwright docs on merging across environments:
If you want to run the same tests in multiple environments... you need to differentiate these environments. Use
TestConfig.tagproperty to tag all tests with the environment name.
-
Path Separator Patching:
PathSeparatorPatcherconverts\↔/based on blob'spathSeparatormetadata vs current platform -
Root Directory Validation: All blobs must share the same
rootDir, or user provides-cconfig with explicittestDir. Clear error message when cross-OS rootDirs differ.
blob-linux.zip blob-win.zip blob-mac.zip
| | |
v v v
extract JSONL, parse events
| | |
IdsPatcher: salt duplicate test IDs (automatic)
PathSeparatorPatcher: normalize paths
AttachmentPathPatcher: fix resource refs
GlobalErrorPatcher: tag errors with env name
WorkerIndexPatcher: adjust worker indices
|
v
merged event stream → reporters (html, json, etc.)
Test IDs are SHA1 of [project=${projectId}]${filePath}\x1e${titleHierarchy} (suiteUtils.ts#L59-61).
Same test on different platforms gets the same base ID → that's why salting is needed for cross-platform merges.
Tuple-based JSON (not JSONL):
type MergeReport = [
vitestVersion: string,
files: File[],
errors: unknown[],
coverage: unknown,
executionTime: number,
environmentModules: MergeReportEnvironmentModules,
]Key files:
- Blob reporter: packages/vitest/src/node/reporters/blob.ts
- Merge logic: packages/vitest/src/node/core.ts#L592
- CLI option: packages/vitest/src/node/cli/cli-config.ts#L819
- Read all blob JSON files from
.vitest-reports/ - Validate ALL blobs have identical Vitest version (and match current)
- Restore module graphs per project/environment
- Flatten all
File[]arrays, sort bystartTime - Report merged files to reporters
- Merge errors + delegate coverage merge to provider
| Limitation | Detail |
|---|---|
| No duplicate handling | If same test file appears in multiple blobs, it shows up multiple times with no distinction |
| No environment metadata | Blobs don't store platform/environment info |
| No path normalization | No \ ↔ / conversion for cross-OS |
| Strict version match | All blobs must be exact same Vitest version |
| Naming assumes sharding | Files named blob-{shardIndex}-{shardCount}.json |
| No rootDir validation | Silently merges blobs from different project roots |
| File-level dedup only | No mechanism to distinguish same file run on different platforms |
Option A: Playwright-style (separate entries, tagged)
- Same test from different platforms → separate test entries with platform tag
- Pro: Simple, each result is independent, familiar to Playwright users
- Con: Test count inflates (3 platforms x 100 tests = 300 entries)
Option B: Grouped results (single entry, multiple results)
- Same test → single entry with results array per platform
- Pro: Natural "matrix" view, clear pass/fail per platform
- Con: More complex data model, reporter changes needed
Option C: Hybrid (configurable)
- Default to separate entries (simpler), offer grouped view in HTML reporter
- Pro: Flexibility
- Con: Complexity
Add environment/platform metadata to the blob format:
type MergeReport = [
vitestVersion: string,
files: File[],
errors: unknown[],
coverage: unknown,
executionTime: number,
environmentModules: MergeReportEnvironmentModules,
metadata?: BlobMetadata, // NEW - optional for backward compat
]
interface BlobMetadata {
name?: string // user-defined label, e.g. "linux", "node-22"
platform?: string // auto-detected: process.platform
arch?: string // auto-detected: process.arch
nodeVersion?: string // auto-detected: process.version
pathSeparator?: string // auto-detected: path.sep
shard?: { index: number; count: number }
}// vitest.config.ts
export default defineConfig({
test: {
reporter: [['blob', {
name: process.env.CI_PLATFORM || 'default', // environment label
}]],
},
})For merge:
// or CLI: vitest --merge-reports --merge-reports.testDir=/path
export default defineConfig({
test: {
mergeReports: {
path: '.vitest-reports',
testDir: '/normalized/path', // override for cross-OS rootDir mismatch
},
},
})- Duplicate file handling: When same test file appears in multiple blobs:
- Automatic: just works (files appear multiple times, each tagged)
- Tag each
Fileand its tasks with the blob's environment name viatask.meta - Optionally prefix project name:
"my-project [linux]","my-project [windows]"
- Path normalization: Convert path separators based on blob metadata
- Relaxed version check: Consider allowing patch version differences, or make it a warning
- Default/list reporter: Show platform tag next to test name for cross-platform merges
- JSON reporter: Include platform metadata in output
- HTML reporter (vitest-ui): Platform filter/grouping in the UI
- JUnit reporter: Use
<testsuite>attributes or<properties>for platform info
Current: blob-{shardIndex}-{shardCount}.json — assumes sharding.
For cross-platform, need a naming scheme that doesn't collide:
- Option:
blob-{name}.jsonwhennameis set (e.g.,blob-linux.json) - Option:
blob-{name}-{shardIndex}-{shardCount}.jsonfor sharded cross-platform - Or just let users set
outputFileexplicitly per platform
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- run: vitest run --reporter=blob
env:
VITEST_BLOB_NAME: ${{ matrix.os }}
- uses: actions/upload-artifact@v4
with:
name: blob-${{ matrix.os }}
path: .vitest-reports/
merge:
needs: test
steps:
- uses: actions/download-artifact@v4
with:
path: .vitest-reports/
merge-multiple: true
- run: vitest --merge-reports --reporter=default --reporter=json- Phase 1: Blob metadata (platform info + name) — backward compatible
- Phase 2: Path normalization in merge — needed for Windows blobs
- Phase 3: Duplicate test handling with platform tagging (like Playwright: automatic deconflict + explicit label config)
- Phase 4: Reporter enhancements (especially HTML/UI grouping)
- Should the blob format change from JSON tuple to something more extensible (object with named fields)?
- Tuple is fragile for adding fields; an object
{ version, files, errors, ... }would be cleaner - Could support both formats during transition
- Tuple is fragile for adding fields; an object
- How to handle coverage merge across platforms? (same file, different coverage data)
- Istanbul/v8 providers would need to merge coverage from identical source files
- Should
--merge-reportsaccept a glob pattern instead of just a directory?- Would make it easier to merge artifacts from different CI jobs
- How strict should version matching be? Playwright doesn't seem to enforce version match at all
- Should we support merging blobs from different Vitest projects (different configs)?
- Blob file naming: how to handle cross-platform + sharded combos?