feat: complete Phase 3.8 - test infrastructure and CI/CD enhancements
- 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.
This commit is contained in:
167
scripts/generate-test-summary.js
Normal file
167
scripts/generate-test-summary.js
Normal file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env node
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
/**
|
||||
* Generate a markdown summary of test results for PR comments
|
||||
*/
|
||||
function generateTestSummary() {
|
||||
const results = {
|
||||
tests: null,
|
||||
coverage: null,
|
||||
benchmarks: null,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Read test results
|
||||
const testResultPath = resolve(process.cwd(), 'test-results/results.json');
|
||||
if (existsSync(testResultPath)) {
|
||||
try {
|
||||
const testData = JSON.parse(readFileSync(testResultPath, 'utf-8'));
|
||||
const totalTests = testData.numTotalTests || 0;
|
||||
const passedTests = testData.numPassedTests || 0;
|
||||
const failedTests = testData.numFailedTests || 0;
|
||||
const skippedTests = testData.numSkippedTests || 0;
|
||||
const duration = testData.duration || 0;
|
||||
|
||||
results.tests = {
|
||||
total: totalTests,
|
||||
passed: passedTests,
|
||||
failed: failedTests,
|
||||
skipped: skippedTests,
|
||||
duration: duration,
|
||||
success: failedTests === 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error reading test results:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Read coverage results
|
||||
const coveragePath = resolve(process.cwd(), 'coverage/coverage-summary.json');
|
||||
if (existsSync(coveragePath)) {
|
||||
try {
|
||||
const coverageData = JSON.parse(readFileSync(coveragePath, 'utf-8'));
|
||||
const total = coverageData.total;
|
||||
|
||||
results.coverage = {
|
||||
lines: total.lines.pct,
|
||||
statements: total.statements.pct,
|
||||
functions: total.functions.pct,
|
||||
branches: total.branches.pct
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error reading coverage results:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Read benchmark results
|
||||
const benchmarkPath = resolve(process.cwd(), 'benchmark-results.json');
|
||||
if (existsSync(benchmarkPath)) {
|
||||
try {
|
||||
const benchmarkData = JSON.parse(readFileSync(benchmarkPath, 'utf-8'));
|
||||
const benchmarks = [];
|
||||
|
||||
for (const file of benchmarkData.files || []) {
|
||||
for (const group of file.groups || []) {
|
||||
for (const benchmark of group.benchmarks || []) {
|
||||
benchmarks.push({
|
||||
name: `${group.name} - ${benchmark.name}`,
|
||||
mean: benchmark.result.mean,
|
||||
ops: benchmark.result.hz
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.benchmarks = benchmarks;
|
||||
} catch (error) {
|
||||
console.error('Error reading benchmark results:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate markdown summary
|
||||
let summary = '## Test Results Summary\n\n';
|
||||
|
||||
// Test results
|
||||
if (results.tests) {
|
||||
const { total, passed, failed, skipped, duration, success } = results.tests;
|
||||
const emoji = success ? '✅' : '❌';
|
||||
const status = success ? 'PASSED' : 'FAILED';
|
||||
|
||||
summary += `### ${emoji} Tests ${status}\n\n`;
|
||||
summary += `| Metric | Value |\n`;
|
||||
summary += `|--------|-------|\n`;
|
||||
summary += `| Total Tests | ${total} |\n`;
|
||||
summary += `| Passed | ${passed} |\n`;
|
||||
summary += `| Failed | ${failed} |\n`;
|
||||
summary += `| Skipped | ${skipped} |\n`;
|
||||
summary += `| Duration | ${(duration / 1000).toFixed(2)}s |\n\n`;
|
||||
}
|
||||
|
||||
// Coverage results
|
||||
if (results.coverage) {
|
||||
const { lines, statements, functions, branches } = results.coverage;
|
||||
const avgCoverage = (lines + statements + functions + branches) / 4;
|
||||
const emoji = avgCoverage >= 80 ? '✅' : avgCoverage >= 60 ? '⚠️' : '❌';
|
||||
|
||||
summary += `### ${emoji} Coverage Report\n\n`;
|
||||
summary += `| Type | Coverage |\n`;
|
||||
summary += `|------|----------|\n`;
|
||||
summary += `| Lines | ${lines.toFixed(2)}% |\n`;
|
||||
summary += `| Statements | ${statements.toFixed(2)}% |\n`;
|
||||
summary += `| Functions | ${functions.toFixed(2)}% |\n`;
|
||||
summary += `| Branches | ${branches.toFixed(2)}% |\n`;
|
||||
summary += `| **Average** | **${avgCoverage.toFixed(2)}%** |\n\n`;
|
||||
}
|
||||
|
||||
// Benchmark results
|
||||
if (results.benchmarks && results.benchmarks.length > 0) {
|
||||
summary += `### ⚡ Benchmark Results\n\n`;
|
||||
summary += `| Benchmark | Ops/sec | Mean (ms) |\n`;
|
||||
summary += `|-----------|---------|------------|\n`;
|
||||
|
||||
for (const bench of results.benchmarks.slice(0, 10)) { // Show top 10
|
||||
const opsFormatted = bench.ops.toLocaleString('en-US', { maximumFractionDigits: 0 });
|
||||
const meanFormatted = (bench.mean * 1000).toFixed(3);
|
||||
summary += `| ${bench.name} | ${opsFormatted} | ${meanFormatted} |\n`;
|
||||
}
|
||||
|
||||
if (results.benchmarks.length > 10) {
|
||||
summary += `\n*...and ${results.benchmarks.length - 10} more benchmarks*\n`;
|
||||
}
|
||||
summary += '\n';
|
||||
}
|
||||
|
||||
// Links to artifacts
|
||||
const runId = process.env.GITHUB_RUN_ID;
|
||||
const runNumber = process.env.GITHUB_RUN_NUMBER;
|
||||
const sha = process.env.GITHUB_SHA;
|
||||
|
||||
if (runId) {
|
||||
summary += `### 📊 Artifacts\n\n`;
|
||||
summary += `- 📄 [Test Results](https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${runId})\n`;
|
||||
summary += `- 📊 [Coverage Report](https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${runId})\n`;
|
||||
summary += `- ⚡ [Benchmark Results](https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${runId})\n\n`;
|
||||
}
|
||||
|
||||
// Metadata
|
||||
summary += `---\n`;
|
||||
summary += `*Generated at ${new Date().toUTCString()}*\n`;
|
||||
if (sha) {
|
||||
summary += `*Commit: ${sha.substring(0, 7)}*\n`;
|
||||
}
|
||||
if (runNumber) {
|
||||
summary += `*Run: #${runNumber}*\n`;
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
// Generate and output summary
|
||||
const summary = generateTestSummary();
|
||||
console.log(summary);
|
||||
|
||||
// Also write to file for artifact
|
||||
import { writeFileSync } from 'fs';
|
||||
writeFileSync('test-summary.md', summary);
|
||||
Reference in New Issue
Block a user