#!/usr/bin/env node import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; import { resolve, dirname } from 'path'; /** * Generate detailed test reports in multiple formats */ class TestReportGenerator { constructor() { this.results = { tests: null, coverage: null, benchmarks: null, metadata: { timestamp: new Date().toISOString(), repository: process.env.GITHUB_REPOSITORY || 'n8n-mcp', sha: process.env.GITHUB_SHA || 'unknown', branch: process.env.GITHUB_REF || 'unknown', runId: process.env.GITHUB_RUN_ID || 'local', runNumber: process.env.GITHUB_RUN_NUMBER || '0', } }; } loadTestResults() { const testResultPath = resolve(process.cwd(), 'test-results/results.json'); if (existsSync(testResultPath)) { try { const data = JSON.parse(readFileSync(testResultPath, 'utf-8')); this.results.tests = this.processTestResults(data); } catch (error) { console.error('Error loading test results:', error); } } } processTestResults(data) { const processedResults = { summary: { total: data.numTotalTests || 0, passed: data.numPassedTests || 0, failed: data.numFailedTests || 0, skipped: data.numSkippedTests || 0, duration: data.duration || 0, success: (data.numFailedTests || 0) === 0 }, testSuites: [], failedTests: [] }; // Process test suites if (data.testResults) { for (const suite of data.testResults) { const suiteInfo = { name: suite.name, duration: suite.duration || 0, tests: { total: suite.numPassingTests + suite.numFailingTests + suite.numPendingTests, passed: suite.numPassingTests || 0, failed: suite.numFailingTests || 0, skipped: suite.numPendingTests || 0 }, status: suite.numFailingTests === 0 ? 'passed' : 'failed' }; processedResults.testSuites.push(suiteInfo); // Collect failed tests if (suite.testResults) { for (const test of suite.testResults) { if (test.status === 'failed') { processedResults.failedTests.push({ suite: suite.name, test: test.title, duration: test.duration || 0, error: test.failureMessages ? test.failureMessages.join('\n') : 'Unknown error' }); } } } } } return processedResults; } loadCoverageResults() { const coveragePath = resolve(process.cwd(), 'coverage/coverage-summary.json'); if (existsSync(coveragePath)) { try { const data = JSON.parse(readFileSync(coveragePath, 'utf-8')); this.results.coverage = this.processCoverageResults(data); } catch (error) { console.error('Error loading coverage results:', error); } } } processCoverageResults(data) { const coverage = { summary: { lines: data.total.lines.pct, statements: data.total.statements.pct, functions: data.total.functions.pct, branches: data.total.branches.pct, average: 0 }, files: [] }; // Calculate average coverage.summary.average = ( coverage.summary.lines + coverage.summary.statements + coverage.summary.functions + coverage.summary.branches ) / 4; // Process file coverage for (const [filePath, fileData] of Object.entries(data)) { if (filePath !== 'total') { coverage.files.push({ path: filePath, lines: fileData.lines.pct, statements: fileData.statements.pct, functions: fileData.functions.pct, branches: fileData.branches.pct, uncoveredLines: fileData.lines.total - fileData.lines.covered }); } } // Sort files by coverage (lowest first) coverage.files.sort((a, b) => a.lines - b.lines); return coverage; } loadBenchmarkResults() { const benchmarkPath = resolve(process.cwd(), 'benchmark-results.json'); if (existsSync(benchmarkPath)) { try { const data = JSON.parse(readFileSync(benchmarkPath, 'utf-8')); this.results.benchmarks = this.processBenchmarkResults(data); } catch (error) { console.error('Error loading benchmark results:', error); } } } processBenchmarkResults(data) { const benchmarks = { timestamp: data.timestamp, results: [] }; for (const file of data.files || []) { for (const group of file.groups || []) { for (const benchmark of group.benchmarks || []) { benchmarks.results.push({ file: file.filepath, group: group.name, name: benchmark.name, ops: benchmark.result.hz, mean: benchmark.result.mean, min: benchmark.result.min, max: benchmark.result.max, p75: benchmark.result.p75, p99: benchmark.result.p99, samples: benchmark.result.samples }); } } } // Sort by ops/sec (highest first) benchmarks.results.sort((a, b) => b.ops - a.ops); return benchmarks; } generateMarkdownReport() { let report = '# n8n-mcp Test Report\n\n'; report += `Generated: ${this.results.metadata.timestamp}\n\n`; // Metadata report += '## Build Information\n\n'; report += `- **Repository**: ${this.results.metadata.repository}\n`; report += `- **Commit**: ${this.results.metadata.sha.substring(0, 7)}\n`; report += `- **Branch**: ${this.results.metadata.branch}\n`; report += `- **Run**: #${this.results.metadata.runNumber}\n\n`; // Test Results if (this.results.tests) { const { summary, testSuites, failedTests } = this.results.tests; const emoji = summary.success ? '✅' : '❌'; report += `## ${emoji} Test Results\n\n`; report += `### Summary\n\n`; report += `- **Total Tests**: ${summary.total}\n`; report += `- **Passed**: ${summary.passed} (${((summary.passed / summary.total) * 100).toFixed(1)}%)\n`; report += `- **Failed**: ${summary.failed}\n`; report += `- **Skipped**: ${summary.skipped}\n`; report += `- **Duration**: ${(summary.duration / 1000).toFixed(2)}s\n\n`; // Test Suites if (testSuites.length > 0) { report += '### Test Suites\n\n'; report += '| Suite | Status | Tests | Duration |\n'; report += '|-------|--------|-------|----------|\n'; for (const suite of testSuites) { const status = suite.status === 'passed' ? '✅' : '❌'; const tests = `${suite.tests.passed}/${suite.tests.total}`; const duration = `${(suite.duration / 1000).toFixed(2)}s`; report += `| ${suite.name} | ${status} | ${tests} | ${duration} |\n`; } report += '\n'; } // Failed Tests if (failedTests.length > 0) { report += '### Failed Tests\n\n'; for (const failed of failedTests) { report += `#### ${failed.suite} > ${failed.test}\n\n`; report += '```\n'; report += failed.error; report += '\n```\n\n'; } } } // Coverage Results if (this.results.coverage) { const { summary, files } = this.results.coverage; const emoji = summary.average >= 80 ? '✅' : summary.average >= 60 ? '⚠️' : '❌'; report += `## ${emoji} Coverage Report\n\n`; report += '### Summary\n\n'; report += `- **Lines**: ${summary.lines.toFixed(2)}%\n`; report += `- **Statements**: ${summary.statements.toFixed(2)}%\n`; report += `- **Functions**: ${summary.functions.toFixed(2)}%\n`; report += `- **Branches**: ${summary.branches.toFixed(2)}%\n`; report += `- **Average**: ${summary.average.toFixed(2)}%\n\n`; // Files with low coverage const lowCoverageFiles = files.filter(f => f.lines < 80).slice(0, 10); if (lowCoverageFiles.length > 0) { report += '### Files with Low Coverage\n\n'; report += '| File | Lines | Uncovered Lines |\n'; report += '|------|-------|----------------|\n'; for (const file of lowCoverageFiles) { const fileName = file.path.split('/').pop(); report += `| ${fileName} | ${file.lines.toFixed(1)}% | ${file.uncoveredLines} |\n`; } report += '\n'; } } // Benchmark Results if (this.results.benchmarks && this.results.benchmarks.results.length > 0) { report += '## ⚡ Benchmark Results\n\n'; report += '### Top Performers\n\n'; report += '| Benchmark | Ops/sec | Mean (ms) | Samples |\n'; report += '|-----------|---------|-----------|----------|\n'; for (const bench of this.results.benchmarks.results.slice(0, 10)) { const opsFormatted = bench.ops.toLocaleString('en-US', { maximumFractionDigits: 0 }); const meanFormatted = (bench.mean * 1000).toFixed(3); report += `| ${bench.name} | ${opsFormatted} | ${meanFormatted} | ${bench.samples} |\n`; } report += '\n'; } return report; } generateJsonReport() { return JSON.stringify(this.results, null, 2); } generateHtmlReport() { const htmlTemplate = ` n8n-mcp Test Report

n8n-mcp Test Report

Repository: ${this.results.metadata.repository}
Commit: ${this.results.metadata.sha.substring(0, 7)}
Run: #${this.results.metadata.runNumber}
Generated: ${new Date(this.results.metadata.timestamp).toLocaleString()}
${this.generateTestResultsHtml()} ${this.generateCoverageHtml()} ${this.generateBenchmarkHtml()} `; return htmlTemplate; } generateTestResultsHtml() { if (!this.results.tests) return ''; const { summary, testSuites, failedTests } = this.results.tests; const successRate = ((summary.passed / summary.total) * 100).toFixed(1); const statusClass = summary.success ? 'success' : 'danger'; const statusIcon = summary.success ? '✅' : '❌'; let html = `

${statusIcon} Test Results

${summary.total}
Total Tests
${summary.passed}
Passed
${summary.failed}
Failed
${successRate}%
Success Rate
${(summary.duration / 1000).toFixed(1)}s
Duration
`; if (testSuites.length > 0) { html += `

Test Suites

`; for (const suite of testSuites) { const status = suite.status === 'passed' ? '✅' : '❌'; const statusClass = suite.status === 'passed' ? 'success' : 'danger'; html += ` `; } html += `
Suite Status Tests Duration
${suite.name} ${status} ${suite.tests.passed}/${suite.tests.total} ${(suite.duration / 1000).toFixed(2)}s
`; } if (failedTests.length > 0) { html += `

Failed Tests

`; for (const failed of failedTests) { html += `

${failed.suite} > ${failed.test}

${this.escapeHtml(failed.error)}
`; } } html += `
`; return html; } generateCoverageHtml() { if (!this.results.coverage) return ''; const { summary, files } = this.results.coverage; const coverageClass = summary.average >= 80 ? 'success' : summary.average >= 60 ? 'warning' : 'danger'; const progressClass = summary.average >= 80 ? '' : summary.average >= 60 ? 'coverage-medium' : 'coverage-low'; let html = `

📊 Coverage Report

${summary.average.toFixed(1)}%
Average Coverage
${summary.lines.toFixed(1)}%
Lines
${summary.statements.toFixed(1)}%
Statements
${summary.functions.toFixed(1)}%
Functions
${summary.branches.toFixed(1)}%
Branches
`; const lowCoverageFiles = files.filter(f => f.lines < 80).slice(0, 10); if (lowCoverageFiles.length > 0) { html += `

Files with Low Coverage

`; for (const file of lowCoverageFiles) { const fileName = file.path.split('/').pop(); html += ` `; } html += `
File Lines Statements Functions Branches
${fileName} ${file.lines.toFixed(1)}% ${file.statements.toFixed(1)}% ${file.functions.toFixed(1)}% ${file.branches.toFixed(1)}%
`; } html += `
`; return html; } generateBenchmarkHtml() { if (!this.results.benchmarks || this.results.benchmarks.results.length === 0) return ''; let html = `

⚡ Benchmark Results

`; for (const bench of this.results.benchmarks.results.slice(0, 20)) { const opsFormatted = bench.ops.toLocaleString('en-US', { maximumFractionDigits: 0 }); const meanFormatted = (bench.mean * 1000).toFixed(3); const minFormatted = (bench.min * 1000).toFixed(3); const maxFormatted = (bench.max * 1000).toFixed(3); html += ` `; } html += `
Benchmark Operations/sec Mean Time (ms) Min (ms) Max (ms) Samples
${bench.name} ${opsFormatted} ${meanFormatted} ${minFormatted} ${maxFormatted} ${bench.samples}
`; if (this.results.benchmarks.results.length > 20) { html += `

Showing top 20 of ${this.results.benchmarks.results.length} benchmarks

`; } html += `
`; return html; } escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, m => map[m]); } async generate() { // Load all results this.loadTestResults(); this.loadCoverageResults(); this.loadBenchmarkResults(); // Ensure output directory exists const outputDir = resolve(process.cwd(), 'test-reports'); if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true }); } // Generate reports in different formats const markdownReport = this.generateMarkdownReport(); const jsonReport = this.generateJsonReport(); const htmlReport = this.generateHtmlReport(); // Write reports writeFileSync(resolve(outputDir, 'report.md'), markdownReport); writeFileSync(resolve(outputDir, 'report.json'), jsonReport); writeFileSync(resolve(outputDir, 'report.html'), htmlReport); console.log('Test reports generated successfully:'); console.log('- test-reports/report.md'); console.log('- test-reports/report.json'); console.log('- test-reports/report.html'); } } // Run the generator const generator = new TestReportGenerator(); generator.generate().catch(console.error);