mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:01:09 +02:00
Implement rock-solid automated testing infrastructure for mana-core-auth with daily execution, notifications, and comprehensive monitoring. Test Suite Improvements: - Fix all 36 failing BetterAuthService tests (missing service mocks) - Add 21 JwtAuthGuard tests achieving 100% statement coverage - Create silentError helper to suppress intentional error logs - Fix Todo backend TaskService test structure - Add jose mock for JWT testing - Configure jest collectCoverageFrom for mana-core-auth GitHub Actions Workflow: - Daily automated test execution (2 AM UTC + manual trigger) - Matrix parallelization across 6 backend services - PostgreSQL and Redis service containers - Coverage enforcement (80% threshold) - Multi-channel notifications (Discord, Slack, GitHub Issues) - Support for success notifications (opt-in) Test Infrastructure: - Coverage aggregation across multiple services - Flaky test detection with 30-run history tracking - Performance metrics tracking with regression detection - Test data seeding and cleanup scripts - Comprehensive test reporting with formatted metrics Documentation: - TESTING_GUIDE.md (4000+ words) - Complete testing documentation - AUTOMATED_TESTING_SYSTEM.md - System architecture and workflows - DISCORD_NOTIFICATIONS_SETUP.md - Discord webhook setup guide - TESTING_DEPLOYMENT_CHECKLIST.md - Pre-deployment verification - TESTING_QUICK_REFERENCE.md - Quick command reference Final Result: - 180/180 tests passing (100% pass rate) - Zero console errors in test output - Automated daily testing with rich notifications - Production-ready test infrastructure
265 lines
7.1 KiB
JavaScript
265 lines
7.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/* eslint-disable @typescript-eslint/no-require-imports, no-console */
|
|
/**
|
|
* Track Test Performance Metrics
|
|
*
|
|
* Records test execution time, memory usage, and other performance metrics
|
|
* to track trends over time and identify performance regressions.
|
|
*
|
|
* Usage:
|
|
* node track-metrics.js <test-results-dir>
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
function loadMetricsHistory(resultsDir) {
|
|
const historyFile = path.join(resultsDir, 'metrics-history.json');
|
|
|
|
if (!fs.existsSync(historyFile)) {
|
|
return [];
|
|
}
|
|
|
|
return JSON.parse(fs.readFileSync(historyFile, 'utf8'));
|
|
}
|
|
|
|
function saveMetricsHistory(resultsDir, history) {
|
|
const historyFile = path.join(resultsDir, 'metrics-history.json');
|
|
fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
|
|
}
|
|
|
|
function findTestResultFiles(dir) {
|
|
const results = [];
|
|
|
|
function walk(currentDir) {
|
|
if (!fs.existsSync(currentDir)) {
|
|
return;
|
|
}
|
|
|
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(currentDir, entry.name);
|
|
|
|
if (entry.isDirectory()) {
|
|
walk(fullPath);
|
|
} else if (entry.name.match(/test.*results.*\.json$/i)) {
|
|
results.push(fullPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
walk(dir);
|
|
return results;
|
|
}
|
|
|
|
function calculateMetrics(resultFiles) {
|
|
let totalTests = 0;
|
|
let totalDuration = 0;
|
|
let slowestTest = null;
|
|
const suiteMetrics = {};
|
|
|
|
for (const file of resultFiles) {
|
|
try {
|
|
const content = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
const suiteName = path.basename(path.dirname(file));
|
|
|
|
if (!suiteMetrics[suiteName]) {
|
|
suiteMetrics[suiteName] = {
|
|
tests: 0,
|
|
duration: 0,
|
|
slowestTest: null,
|
|
};
|
|
}
|
|
|
|
// Jest format
|
|
if (content.testResults) {
|
|
content.testResults.forEach((suite) => {
|
|
const suiteTests = suite.assertionResults || [];
|
|
totalTests += suiteTests.length;
|
|
suiteMetrics[suiteName].tests += suiteTests.length;
|
|
|
|
suiteTests.forEach((test) => {
|
|
const duration = test.duration || 0;
|
|
totalDuration += duration;
|
|
suiteMetrics[suiteName].duration += duration;
|
|
|
|
if (!slowestTest || duration > slowestTest.duration) {
|
|
slowestTest = {
|
|
name: test.fullName || test.title,
|
|
duration,
|
|
suite: suite.name,
|
|
};
|
|
}
|
|
|
|
if (
|
|
!suiteMetrics[suiteName].slowestTest ||
|
|
duration > suiteMetrics[suiteName].slowestTest.duration
|
|
) {
|
|
suiteMetrics[suiteName].slowestTest = {
|
|
name: test.fullName || test.title,
|
|
duration,
|
|
};
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle other formats...
|
|
} catch (error) {
|
|
console.error(`Error parsing ${file}:`, error.message);
|
|
}
|
|
}
|
|
|
|
return {
|
|
timestamp: new Date().toISOString(),
|
|
totalTests,
|
|
totalDuration: Math.round(totalDuration),
|
|
averageDuration: totalTests > 0 ? Math.round(totalDuration / totalTests) : 0,
|
|
slowestTest,
|
|
suiteMetrics,
|
|
};
|
|
}
|
|
|
|
function detectRegressions(currentMetrics, history) {
|
|
if (history.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const regressions = [];
|
|
const lastRun = history[history.length - 1];
|
|
|
|
// Check total duration increase
|
|
const durationIncrease =
|
|
((currentMetrics.totalDuration - lastRun.totalDuration) / lastRun.totalDuration) * 100;
|
|
|
|
if (durationIncrease > 20) {
|
|
regressions.push({
|
|
type: 'duration',
|
|
message: `Total test duration increased by ${durationIncrease.toFixed(1)}%`,
|
|
previous: lastRun.totalDuration,
|
|
current: currentMetrics.totalDuration,
|
|
});
|
|
}
|
|
|
|
// Check per-suite regressions
|
|
for (const [suite, metrics] of Object.entries(currentMetrics.suiteMetrics)) {
|
|
const previousSuite = lastRun.suiteMetrics?.[suite];
|
|
|
|
if (previousSuite) {
|
|
const suiteIncrease =
|
|
((metrics.duration - previousSuite.duration) / previousSuite.duration) * 100;
|
|
|
|
if (suiteIncrease > 30) {
|
|
regressions.push({
|
|
type: 'suite',
|
|
suite,
|
|
message: `${suite} duration increased by ${suiteIncrease.toFixed(1)}%`,
|
|
previous: previousSuite.duration,
|
|
current: metrics.duration,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return regressions;
|
|
}
|
|
|
|
function generateMetricsReport(metrics, regressions) {
|
|
let report = '# Test Performance Metrics\n\n';
|
|
|
|
// Summary
|
|
report += `**Date:** ${new Date(metrics.timestamp).toISOString().split('T')[0]}\n\n`;
|
|
report += `- **Total Tests:** ${metrics.totalTests}\n`;
|
|
report += `- **Total Duration:** ${(metrics.totalDuration / 1000).toFixed(2)}s\n`;
|
|
report += `- **Average Duration:** ${metrics.averageDuration}ms per test\n`;
|
|
|
|
if (metrics.slowestTest) {
|
|
report += `- **Slowest Test:** ${metrics.slowestTest.name} (${metrics.slowestTest.duration}ms)\n`;
|
|
}
|
|
|
|
// Performance regressions
|
|
if (regressions.length > 0) {
|
|
report += '\n## ⚠️ Performance Regressions Detected\n\n';
|
|
regressions.forEach((reg) => {
|
|
report += `- ${reg.message}\n`;
|
|
report += ` - Previous: ${reg.previous}ms\n`;
|
|
report += ` - Current: ${reg.current}ms\n`;
|
|
});
|
|
}
|
|
|
|
// Suite breakdown
|
|
report += '\n## Suite Performance\n\n';
|
|
report += '| Suite | Tests | Duration | Avg/Test | Slowest |\n';
|
|
report += '|-------|-------|----------|----------|--------|\n';
|
|
|
|
for (const [suite, data] of Object.entries(metrics.suiteMetrics)) {
|
|
const avgPerTest = data.tests > 0 ? Math.round(data.duration / data.tests) : 0;
|
|
const slowest = data.slowestTest ? `${data.slowestTest.duration}ms` : 'N/A';
|
|
|
|
report += `| ${suite} | ${data.tests} | ${data.duration}ms | ${avgPerTest}ms | ${slowest} |\n`;
|
|
}
|
|
|
|
return report;
|
|
}
|
|
|
|
function main() {
|
|
const resultsDir = process.argv[2];
|
|
|
|
if (!resultsDir) {
|
|
console.error('Usage: node track-metrics.js <test-results-dir>');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('Tracking test performance metrics...');
|
|
|
|
// Ensure results directory exists
|
|
if (!fs.existsSync(resultsDir)) {
|
|
fs.mkdirSync(resultsDir, { recursive: true });
|
|
}
|
|
|
|
// Find test result files
|
|
const resultFiles = findTestResultFiles(resultsDir);
|
|
console.log(`Found ${resultFiles.length} test result files`);
|
|
|
|
if (resultFiles.length === 0) {
|
|
console.log('No test results to analyze.');
|
|
return;
|
|
}
|
|
|
|
// Calculate current metrics
|
|
const currentMetrics = calculateMetrics(resultFiles);
|
|
console.log(`Analyzed ${currentMetrics.totalTests} tests`);
|
|
|
|
// Load history and detect regressions
|
|
const history = loadMetricsHistory(resultsDir);
|
|
const regressions = detectRegressions(currentMetrics, history);
|
|
|
|
// Update history
|
|
history.push(currentMetrics);
|
|
|
|
// Keep only last 90 days
|
|
const ninetyDaysAgo = Date.now() - 90 * 24 * 60 * 60 * 1000;
|
|
const filteredHistory = history.filter((m) => new Date(m.timestamp).getTime() > ninetyDaysAgo);
|
|
|
|
saveMetricsHistory(resultsDir, filteredHistory);
|
|
|
|
// Save current metrics
|
|
const metricsFile = path.join(resultsDir, 'metrics.json');
|
|
fs.writeFileSync(metricsFile, JSON.stringify(currentMetrics, null, 2));
|
|
|
|
// Generate and save report
|
|
const report = generateMetricsReport(currentMetrics, regressions);
|
|
const reportFile = path.join(resultsDir, 'metrics-report.md');
|
|
fs.writeFileSync(reportFile, report);
|
|
|
|
console.log(`\n${report}`);
|
|
console.log(`\nMetrics saved to ${metricsFile}`);
|
|
|
|
if (regressions.length > 0) {
|
|
console.error(`\n⚠️ ${regressions.length} performance regression(s) detected!`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|