- Add test result artifacts storage with multiple formats (JUnit, JSON, HTML) - Configure GitHub Actions to upload and preserve test outputs - Add PR comment integration with test summaries - Create benchmark comparison workflow for PR performance tracking - Add detailed test report generation scripts - Configure artifact retention policies (30 days for tests, 90 for combined) - Set up test metadata collection for better debugging This completes all remaining test infrastructure tasks and provides comprehensive visibility into test results across CI/CD pipeline.
260 lines
7.9 KiB
JavaScript
260 lines
7.9 KiB
JavaScript
#!/usr/bin/env node
|
|
import { readFileSync, existsSync, writeFileSync } from 'fs';
|
|
import { resolve } from 'path';
|
|
|
|
/**
|
|
* Compare benchmark results between runs
|
|
*/
|
|
class BenchmarkComparator {
|
|
constructor() {
|
|
this.threshold = 0.1; // 10% threshold for significant changes
|
|
}
|
|
|
|
loadBenchmarkResults(path) {
|
|
if (!existsSync(path)) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
} catch (error) {
|
|
console.error(`Error loading benchmark results from ${path}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
compareBenchmarks(current, baseline) {
|
|
const comparison = {
|
|
timestamp: new Date().toISOString(),
|
|
summary: {
|
|
improved: 0,
|
|
regressed: 0,
|
|
unchanged: 0,
|
|
added: 0,
|
|
removed: 0
|
|
},
|
|
benchmarks: []
|
|
};
|
|
|
|
// Create maps for easy lookup
|
|
const currentMap = new Map();
|
|
const baselineMap = new Map();
|
|
|
|
// Process current benchmarks
|
|
if (current && current.files) {
|
|
for (const file of current.files) {
|
|
for (const group of file.groups || []) {
|
|
for (const bench of group.benchmarks || []) {
|
|
const key = `${group.name}::${bench.name}`;
|
|
currentMap.set(key, {
|
|
ops: bench.result.hz,
|
|
mean: bench.result.mean,
|
|
file: file.filepath
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process baseline benchmarks
|
|
if (baseline && baseline.files) {
|
|
for (const file of baseline.files) {
|
|
for (const group of file.groups || []) {
|
|
for (const bench of group.benchmarks || []) {
|
|
const key = `${group.name}::${bench.name}`;
|
|
baselineMap.set(key, {
|
|
ops: bench.result.hz,
|
|
mean: bench.result.mean,
|
|
file: file.filepath
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compare benchmarks
|
|
for (const [key, current] of currentMap) {
|
|
const baseline = baselineMap.get(key);
|
|
|
|
if (!baseline) {
|
|
// New benchmark
|
|
comparison.summary.added++;
|
|
comparison.benchmarks.push({
|
|
name: key,
|
|
status: 'added',
|
|
current: current.ops,
|
|
baseline: null,
|
|
change: null,
|
|
file: current.file
|
|
});
|
|
} else {
|
|
// Compare performance
|
|
const change = ((current.ops - baseline.ops) / baseline.ops) * 100;
|
|
let status = 'unchanged';
|
|
|
|
if (Math.abs(change) >= this.threshold * 100) {
|
|
if (change > 0) {
|
|
status = 'improved';
|
|
comparison.summary.improved++;
|
|
} else {
|
|
status = 'regressed';
|
|
comparison.summary.regressed++;
|
|
}
|
|
} else {
|
|
comparison.summary.unchanged++;
|
|
}
|
|
|
|
comparison.benchmarks.push({
|
|
name: key,
|
|
status,
|
|
current: current.ops,
|
|
baseline: baseline.ops,
|
|
change,
|
|
meanCurrent: current.mean,
|
|
meanBaseline: baseline.mean,
|
|
file: current.file
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for removed benchmarks
|
|
for (const [key, baseline] of baselineMap) {
|
|
if (!currentMap.has(key)) {
|
|
comparison.summary.removed++;
|
|
comparison.benchmarks.push({
|
|
name: key,
|
|
status: 'removed',
|
|
current: null,
|
|
baseline: baseline.ops,
|
|
change: null,
|
|
file: baseline.file
|
|
});
|
|
}
|
|
}
|
|
|
|
// Sort by change percentage (regressions first)
|
|
comparison.benchmarks.sort((a, b) => {
|
|
if (a.status === 'regressed' && b.status !== 'regressed') return -1;
|
|
if (b.status === 'regressed' && a.status !== 'regressed') return 1;
|
|
if (a.change !== null && b.change !== null) {
|
|
return a.change - b.change;
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
return comparison;
|
|
}
|
|
|
|
generateMarkdownReport(comparison) {
|
|
let report = '## Benchmark Comparison Report\n\n';
|
|
|
|
const { summary } = comparison;
|
|
report += '### Summary\n\n';
|
|
report += `- **Improved**: ${summary.improved} benchmarks\n`;
|
|
report += `- **Regressed**: ${summary.regressed} benchmarks\n`;
|
|
report += `- **Unchanged**: ${summary.unchanged} benchmarks\n`;
|
|
report += `- **Added**: ${summary.added} benchmarks\n`;
|
|
report += `- **Removed**: ${summary.removed} benchmarks\n\n`;
|
|
|
|
// Regressions
|
|
const regressions = comparison.benchmarks.filter(b => b.status === 'regressed');
|
|
if (regressions.length > 0) {
|
|
report += '### ⚠️ Performance Regressions\n\n';
|
|
report += '| Benchmark | Current | Baseline | Change |\n';
|
|
report += '|-----------|---------|----------|--------|\n';
|
|
|
|
for (const bench of regressions) {
|
|
const currentOps = bench.current.toLocaleString('en-US', { maximumFractionDigits: 0 });
|
|
const baselineOps = bench.baseline.toLocaleString('en-US', { maximumFractionDigits: 0 });
|
|
const changeStr = bench.change.toFixed(2);
|
|
report += `| ${bench.name} | ${currentOps} ops/s | ${baselineOps} ops/s | **${changeStr}%** |\n`;
|
|
}
|
|
report += '\n';
|
|
}
|
|
|
|
// Improvements
|
|
const improvements = comparison.benchmarks.filter(b => b.status === 'improved');
|
|
if (improvements.length > 0) {
|
|
report += '### ✅ Performance Improvements\n\n';
|
|
report += '| Benchmark | Current | Baseline | Change |\n';
|
|
report += '|-----------|---------|----------|--------|\n';
|
|
|
|
for (const bench of improvements) {
|
|
const currentOps = bench.current.toLocaleString('en-US', { maximumFractionDigits: 0 });
|
|
const baselineOps = bench.baseline.toLocaleString('en-US', { maximumFractionDigits: 0 });
|
|
const changeStr = bench.change.toFixed(2);
|
|
report += `| ${bench.name} | ${currentOps} ops/s | ${baselineOps} ops/s | **+${changeStr}%** |\n`;
|
|
}
|
|
report += '\n';
|
|
}
|
|
|
|
// New benchmarks
|
|
const added = comparison.benchmarks.filter(b => b.status === 'added');
|
|
if (added.length > 0) {
|
|
report += '### 🆕 New Benchmarks\n\n';
|
|
report += '| Benchmark | Performance |\n';
|
|
report += '|-----------|-------------|\n';
|
|
|
|
for (const bench of added) {
|
|
const ops = bench.current.toLocaleString('en-US', { maximumFractionDigits: 0 });
|
|
report += `| ${bench.name} | ${ops} ops/s |\n`;
|
|
}
|
|
report += '\n';
|
|
}
|
|
|
|
return report;
|
|
}
|
|
|
|
generateJsonReport(comparison) {
|
|
return JSON.stringify(comparison, null, 2);
|
|
}
|
|
|
|
async compare(currentPath, baselinePath) {
|
|
// Load results
|
|
const current = this.loadBenchmarkResults(currentPath);
|
|
const baseline = this.loadBenchmarkResults(baselinePath);
|
|
|
|
if (!current && !baseline) {
|
|
console.error('No benchmark results found');
|
|
return;
|
|
}
|
|
|
|
// Generate comparison
|
|
const comparison = this.compareBenchmarks(current, baseline);
|
|
|
|
// Generate reports
|
|
const markdownReport = this.generateMarkdownReport(comparison);
|
|
const jsonReport = this.generateJsonReport(comparison);
|
|
|
|
// Write reports
|
|
writeFileSync('benchmark-comparison.md', markdownReport);
|
|
writeFileSync('benchmark-comparison.json', jsonReport);
|
|
|
|
// Output summary to console
|
|
console.log(markdownReport);
|
|
|
|
// Return exit code based on regressions
|
|
if (comparison.summary.regressed > 0) {
|
|
console.error(`\n❌ Found ${comparison.summary.regressed} performance regressions`);
|
|
process.exit(1);
|
|
} else {
|
|
console.log(`\n✅ No performance regressions found`);
|
|
process.exit(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse command line arguments
|
|
const args = process.argv.slice(2);
|
|
if (args.length < 1) {
|
|
console.error('Usage: node compare-benchmarks.js <current-results> [baseline-results]');
|
|
console.error('If baseline-results is not provided, it will look for benchmark-baseline.json');
|
|
process.exit(1);
|
|
}
|
|
|
|
const currentPath = args[0];
|
|
const baselinePath = args[1] || 'benchmark-baseline.json';
|
|
|
|
// Run comparison
|
|
const comparator = new BenchmarkComparator();
|
|
comparator.compare(currentPath, baselinePath).catch(console.error); |