Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Clover Merge Laravel Package

d0x2f/clover-merge

d0x2f/clover-merge merges multiple Clover XML coverage reports into one consolidated file. Designed for Laravel/PHP projects, it helps aggregate coverage from parallel test runs and CI jobs so results can be passed cleanly to dashboards and reporting tools.

View on GitHub
Deep Wiki
Context7

Getting Started

d0x2f/clover-merge is a small, framework-agnostic utility that takes two or more Clover XML files (the format PHPUnit produces with --coverage-clover) and writes a single combined Clover document. It solves the "I ran my test suite in shards / in parallel and now my coverage tool only accepts one file" problem.

v1.0.5 adds a coverage gate. The new --enforce/-e flag makes the CLI exit non-zero when the merged coverage falls below a threshold, which turns the merge step into a CI quality gate as well as a concatenation step.

Install (dev only):

composer require --dev d0x2f/clover-merge

Where to look first:

  1. README.md — small surface, but the CLI flags (-e, -o, positional inputs) are the actual day-to-day API now.
  2. src/ — the whole implementation is essentially one class wrapping DOMDocument, plus a thin CLI entry point.
  3. composer.json — confirm the bin entry (you want vendor/bin/clover-merge) and the PHP constraint before planning around it.

First use case — merge two local runs and gate at 80%:

vendor/bin/phpunit --testsuite=Unit    --coverage-clover build/clover-unit.xml
vendor/bin/phpunit --testsuite=Feature --coverage-clover build/clover-feature.xml

mkdir -p build
vendor/bin/clover-merge -e 80 -o build/clover.xml build/clover-unit.xml build/clover-feature.xml

Expected output (from the release notes):

Files Discovered: 2
Final Coverage: 4/5 (80.00%)

Exit code 0 when the threshold is met, 1 when it isn't. The output file is a normal Clover file you can feed straight to Codecov, Coveralls, SonarQube, or phpunit --coverage-text pipelines. If you prefer the class API, it's still there — read the class for the exact signature, it takes a minute.

Implementation Patterns

1. CI matrix sharding, with the gate in the merge job. Split tests across jobs, each emits its own clover as an artifact, and the final job merges and enforces.

# .github/workflows/coverage.yml (abridged)
jobs:
  test:
    strategy:
      matrix: { shard: [1, 2, 3, 4] }
    steps:
      - run: vendor/bin/phpunit --coverage-clover build/clover-${{ matrix.shard }}.xml
      - uses: actions/upload-artifact@v4
        with: { name: clover-${{ matrix.shard }}, path: build/clover-*.xml }

  coverage:
    needs: test
    steps:
      - uses: actions/download-artifact@v4
        with: { path: build, pattern: clover-* }
      - run: composer install --no-interaction
      - run: mkdir -p build/final
      - run: vendor/bin/clover-merge -e 85 -o build/final/clover.xml build/clover-*.xml
      - uses: codecov/codecov-action@v4
        with: { files: build/final/clover.xml }

Note the ordering: -e exits non-zero, so anything you want to run regardless of the gate (uploading the artifacts for debugging, posting the number to a PR comment) must run before the merge step or use if: always().

2. Wrap it in an Artisan command — especially now, so the threshold lives in config. The exit-code contract is easy to lose inside a wrapper; be deliberate about propagating it.

// app/Console/Commands/MergeClover.php
protected $signature = 'clover:merge {--out=build/clover.xml} {--min=}';

public function handle(): int
{
    $inputs = glob(base_path('build/clover-*.xml')) ?: [];
    if ($inputs === []) {
        $this->error('No clover files found.');
        return self::FAILURE;
    }

    $min = $this->option('min') ?? config('coverage.min');
    $args = array_filter([$min !== null ? "-e {$min}" : null, "-o {$this->option('out')}"]);
    $cmd = implode(' ', array_map('escapeshellarg', [base_path('vendor/bin/clover-merge'), ...$args, ...$inputs]));

    passthru($cmd, $exitCode);
    return $exitCode === 0 ? self::SUCCESS : self::FAILURE;
}

Run it from the vendor binary rather than reimplementing the gate in PHP — duplicating the arithmetic is how the wrapper and the tool end up disagreeing.

3. Composer script for local parallelism. If you use ParaTest, each worker needs its own clover file; merge afterwards and gate locally so you find out before pushing.

"scripts": {
  "test:coverage": [
    "mkdir -p build",
    "php artisan test --parallel --coverage-clover=build/clover-1.xml",
    "clover-merge -e 85 -o build/clover.xml build/clover-*.xml"
  ]
}

4. Pick one gate, not two. Before v1.0.5 you'd have used Codecov/Coveralls' threshold settings or a homemade check. Now you can gate in the build itself. Choosing both means two different percentages failing at two different stages with two different messages — pick the one that owns the number and let the other just report.

5. Normalize paths before merging. If shards run from different working directories (common in containers or matrix jobs), file paths inside <file name="..."> drift and the merged report looks like it covers two copies of your app — which also distorts the enforced percentage. Either always invoke PHPUnit from the repo root, or post-process the XML to rewrite absolute paths to repo-relative ones.

6. Upload once, everywhere. Run the merge before every consumer (Codecov, Coveralls, Sonar, text reports). The merged file — the one that passed the gate — is the canonical build artifact.

Gotchas and Tips

  • -e gates the total, not any single file. 4/5 (80.00%) is aggregate line coverage. A shard that silently produced an empty clover lowers your total and trips the gate; that's usually the desired signal, but it means flaky test discovery now fails CI with a coverage error rather than a test error. Check Files Discovered: in the output before blaming your tests.
  • Exit codes + shell semantics. Under set -e, make, or a Composer script chain, a failed gate aborts the rest of the script. That's the point — just don't put the upload after the enforced merge and then wonder why Codecov has no report on red builds.
  • Threshold format. The example passes 90, and the comparison is rendered as 80.00% < 90.00%. Pass a plain number; if you compute it from a variable, guard against empty strings (an empty -e argument may be parsed as the next token) and against locale-formatted decimals with commas.
  • The human-readable summary goes to stdout. Files Discovered: / Final Coverage: lines are stable enough to grep if you want to surface the number in a job summary, but treat them as display output, not a contract — parse the merged XML if you need machine-readable metrics.
  • Empty or zero-coverage clovers still break things. PHPUnit emits a valid but sparse document (sometimes with no <file> nodes) when nothing is covered. Validate with xmllint --noout build/clover-*.xml and skip empties before merging; with -e active, a single empty input can drag the merged total under the line.
  • Duplicate file entries are the subtle failure mode. If two shards both touch the same source file, the same <file> may appear in multiple inputs. Verify the merged hit counts against a single full run before trusting the number — double counting or max-of-hits semantics shift the enforced percentage in both directions.
  • Path identity matters more than the merge itself. app/Models/User.php, ./app/Models/User.php, and /home/runner/work/app/app/Models/User.php are three different "files" to a coverage reporter. Keep the working directory pinned across shards.
  • Output directory must exist. The writer won't mkdir -p for you — create build/ (or your chosen path) in the workflow/script first.
  • Debugging: when the merged file looks wrong, drop -e and diff the inputs with xmllint --format before blaming the merger. Most "merge bugs" are malformed or path-inconsistent inputs; the enforce flag just makes them louder.
  • Extension points are minimal by design. No plugin system, no service provider, no config file. For custom behavior (dedupe by path, rewrite root attributes, strip <metrics>), wrap the class and post-process the resulting DOMDocument rather than editing vendor/.
  • It's still a tiny, lightly maintained package. v1.0.5 shows it's alive, but there's no Laravel integration and no config surface — read composer.json for the PHP constraint, and keep a ~40-line DOMDocument-based merger plus a one-line percentage check in app/Support/ as a fallback if a future PHP upgrade makes it uninstallable. The Clover schema and the exit-code contract are stable enough that the swap is low-risk.
  • Keep it out of production. require-dev only; it belongs in your coverage pipeline, never in the runtime autoload path.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route