sweetchuck/junit-merger-cli
CLI tool to merge multiple JUnit XML reports into a single file. Reads file paths from stdin or arguments, outputs to stdout or a specified file. Supports DOM-based and fast substring handlers, with an option to recalculate testsuite counts.
composer require sweetchuck/junit-merger-cli --dev
storage/logs/junit/*.xml).find):
find storage/logs/junit -name '*.xml' | vendor/bin/junit-merger merge:files
Or with direct file arguments:
vendor/bin/junit-merger merge:files file1.xml file2.xml > merged.xml
Consolidate parallel test reports in CI:
# .github/workflows/tests.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: vendor/bin/pest --parallel --distribute=4
- run: find storage/logs/junit -name '*.xml' | vendor/bin/junit-merger merge:files > merged-report.xml
- uses: actions/upload-artifact@v3
with:
name: junit-report
path: merged-report.xml
CI Pipeline Integration:
--parallel).dom_read_write for accurate test suite stats:
find tests/_output -name '*.xml' | vendor/bin/junit-merger merge:files --handler='dom_read_write' > report.xml
sonar-scanner -Dsonar.junit.reportPaths=report.xml
Artisan Command Wrapper (for Laravel-native use):
// app/Console/Commands/MergeJUnitReports.php
class MergeJUnitReports extends Command {
protected $signature = 'junit:merge
{--output= : Output file path}
{--handler=dom_read_write : Merge handler}';
public function handle() {
$files = glob(storage_path('logs/junit/*.xml'));
$output = $this->option('output') ?? storage_path('logs/junit/merged.xml');
$handler = $this->option('handler');
$command = "vendor/bin/junit-merger merge:files --handler=$handler " . implode(' ', $files) . " > $output";
shell_exec($command);
$this->info("Merged {$files} into $output");
}
}
Usage:
php artisan junit:merge --output=report.xml --handler=dom_read
Dynamic File Discovery:
Storage facade to find files:
use Illuminate\Support\Facades\Storage;
$files = Storage::disk('local')->files('logs/junit');
dom_read_write for accuracy (recalculates <testsuite> attributes like tests, failures).substr for speed (assumes identical file formats, e.g., from same PHPUnit version).--output-file for clarity:
vendor/bin/junit-merger merge:files --output-file=report.xml file1.xml file2.xml
SimpleXML:
$xml = simplexml_load_file($file);
if ($xml === false) throw new \RuntimeException("Invalid JUnit XML: $file");
XML Schema Inconsistencies:
<custom-tag>) may break merging.dom_read_write or pre-process files with DOMDocument to normalize schemas.Test ID Collisions:
// Pest.php
$this->uniqueTestIds = true;
Performance with Large Files:
dom_read_write recalculates attributes, which can be slow for >500 files.substr if files are format-identical.Shell Access Limitations:
shell_exec.use Sweetchuck\JUnitMerger\Merger;
$merger = new Merger();
$mergedXml = $merger->merge([$file1, $file2]);
Handler-Specific Quirks:
substr: Fails if files have different <testsuites> tag positions.dom_read: Output may have duplicate attributes if not using dom_read_write.vendor/bin/junit-merger merge:files file1.xml file2.xml | tee merged.xml
$mergedXml = simplexml_load_file('merged.xml');
if ($mergedXml->xpath('//testsuite/@tests') === []) {
throw new \RuntimeException("Merged report has no test cases!");
}
1 on failure (e.g., invalid XML). Capture this in Laravel:
$exitCode = shell_exec($command . ' 2>&1', $output);
if ($exitCode !== 0) {
throw new \RuntimeException("Merge failed: " . $output);
}
Custom Handlers:
Pre/Post-Processing:
// app/Providers/AppServiceProvider.php
public function boot() {
Event::listen('junit.merged', function ($mergedXml) {
// Add custom attributes or filter tests
});
}
Queue-Based Merging:
MergeJUnitJob::dispatch($files)->onQueue('junit-merging');
dom_read if no handler is specified. Explicitly set it:
vendor/bin/junit-merger merge:files --handler=dom_read_write file1.xml file2.xml
php -d memory_limit=512M vendor/bin/junit-merger merge:files ...
vendor/bin/junit-merger merge:files "C:/path/to/file1.xml" "C:/path/to/file2.xml"
How can I help you explore Laravel packages today?