amazon-seller-mcp
Version:
Model Context Protocol (MCP) client for Amazon Selling Partner API
328 lines • 13.4 kB
JavaScript
/**
* Test maintenance utilities for monitoring test health and performance
* Provides tools for identifying problematic tests and tracking test metrics
*/
// Node.js built-ins
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';
// Internal imports
import { getLogger } from './utils/logger.js';
/**
* Default performance thresholds for test monitoring
*/
export const DEFAULT_THRESHOLDS = {
slowTestThreshold: 5000, // 5 seconds
memoryLeakThreshold: 100, // 100 MB
flakyTestRetryThreshold: 2, // 2 retries
};
/**
* Test maintenance utility class for monitoring test health
*/
export class TestMaintenanceUtility {
metricsFile;
thresholds;
constructor(metricsFile = 'test-metrics.json', thresholds = DEFAULT_THRESHOLDS) {
this.metricsFile = join(process.cwd(), 'test-results', metricsFile);
this.thresholds = thresholds;
}
/**
* Record test execution metrics
*/
recordTestMetrics(metrics) {
const existingMetrics = this.loadMetrics();
existingMetrics.push(metrics);
// Keep only last 1000 test runs to prevent file from growing too large
const recentMetrics = existingMetrics.slice(-1000);
this.saveMetrics(recentMetrics);
}
/**
* Load existing test metrics from file
*/
loadMetrics() {
if (!existsSync(this.metricsFile)) {
return [];
}
try {
const data = readFileSync(this.metricsFile, 'utf-8');
return JSON.parse(data);
}
catch (error) {
getLogger().warn(`Failed to load test metrics: ${error}`);
return [];
}
}
/**
* Save test metrics to file
*/
saveMetrics(metrics) {
try {
const dir = join(process.cwd(), 'test-results');
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(this.metricsFile, JSON.stringify(metrics, null, 2));
}
catch (error) {
getLogger().warn(`Failed to save test metrics: ${error}`);
}
}
/**
* Generate comprehensive test health report
*/
generateHealthReport(daysSince = 7) {
const metrics = this.loadMetrics();
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysSince);
// Filter metrics to recent period
const recentMetrics = metrics.filter((m) => new Date(m.timestamp) >= cutoffDate);
const slowTests = this.identifySlowTests(recentMetrics);
const failingTests = this.identifyFailingTests(recentMetrics);
const flakyTests = this.identifyFlakyTests(recentMetrics);
const memoryLeaks = this.identifyMemoryLeaks(recentMetrics);
const totalDuration = recentMetrics.reduce((sum, m) => sum + m.duration, 0);
const averageExecutionTime = recentMetrics.length > 0 ? totalDuration / recentMetrics.length : 0;
const recommendations = this.generateRecommendations({
slowTests,
failingTests,
flakyTests,
memoryLeaks,
averageExecutionTime,
});
return {
totalTests: recentMetrics.length,
slowTests,
failingTests,
flakyTests,
averageExecutionTime,
memoryLeaks,
recommendations,
};
}
/**
* Identify tests that consistently run slower than threshold
*/
identifySlowTests(metrics) {
const testGroups = this.groupByTest(metrics);
const slowTests = [];
for (const [, testMetrics] of testGroups) {
const averageDuration = testMetrics.reduce((sum, m) => sum + m.duration, 0) / testMetrics.length;
if (averageDuration > this.thresholds.slowTestThreshold) {
// Find the slowest execution for this test
const slowestExecution = testMetrics.reduce((slowest, current) => current.duration > slowest.duration ? current : slowest);
slowTests.push(slowestExecution);
}
}
return slowTests.sort((a, b) => b.duration - a.duration);
}
/**
* Identify tests that have failed recently
*/
identifyFailingTests(metrics) {
return metrics
.filter((m) => m.status === 'failed')
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
}
/**
* Identify flaky tests (tests that sometimes pass, sometimes fail)
*/
identifyFlakyTests(metrics) {
const testGroups = this.groupByTest(metrics);
const flakyTests = [];
for (const [, testMetrics] of testGroups) {
if (testMetrics.length < 3)
continue; // Need at least 3 runs to determine flakiness
const passedCount = testMetrics.filter((m) => m.status === 'passed').length;
const failedCount = testMetrics.filter((m) => m.status === 'failed').length;
const totalRuns = testMetrics.length;
// Consider flaky if both passes and failures exist and failure rate is between 10-90%
const failureRate = failedCount / totalRuns;
if (failedCount > 0 && passedCount > 0 && failureRate >= 0.1 && failureRate <= 0.9) {
// Get the most recent execution
const mostRecent = testMetrics.reduce((latest, current) => new Date(current.timestamp) > new Date(latest.timestamp) ? current : latest);
flakyTests.push(mostRecent);
}
}
return flakyTests;
}
/**
* Identify tests with potential memory leaks
*/
identifyMemoryLeaks(metrics) {
return metrics
.filter((m) => m.memoryUsage && m.memoryUsage > this.thresholds.memoryLeakThreshold)
.sort((a, b) => (b.memoryUsage || 0) - (a.memoryUsage || 0));
}
/**
* Group metrics by test identifier
*/
groupByTest(metrics) {
const groups = new Map();
for (const metric of metrics) {
const key = `${metric.filePath}:${metric.testName}`;
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key).push(metric);
}
return groups;
}
/**
* Generate actionable recommendations based on test health analysis
*/
generateRecommendations(analysis) {
const recommendations = [];
if (analysis.slowTests.length > 0) {
recommendations.push(`🐌 ${analysis.slowTests.length} slow tests detected. Consider optimizing or splitting these tests.`);
if (analysis.slowTests.length > 5) {
recommendations.push('⚡ Consider running slow tests in parallel or using test.concurrent() for independent tests.');
}
}
if (analysis.failingTests.length > 0) {
recommendations.push(`❌ ${analysis.failingTests.length} failing tests detected. Prioritize fixing these tests.`);
}
if (analysis.flakyTests.length > 0) {
recommendations.push(`🎲 ${analysis.flakyTests.length} flaky tests detected. These tests need stabilization.`);
recommendations.push('🔧 Review flaky tests for race conditions, timing issues, or external dependencies.');
}
if (analysis.memoryLeaks.length > 0) {
recommendations.push(`🧠 ${analysis.memoryLeaks.length} tests with high memory usage detected. Check for memory leaks.`);
}
if (analysis.averageExecutionTime > 2000) {
recommendations.push('⏱️ Average test execution time is high. Consider optimizing test setup and teardown.');
}
if (recommendations.length === 0) {
recommendations.push('✅ All tests are performing well! Keep up the good work.');
}
return recommendations;
}
/**
* Get test execution time trends over time
*/
getExecutionTimeTrends(testName, filePath) {
const metrics = this.loadMetrics();
let filteredMetrics = metrics;
if (testName && filePath) {
filteredMetrics = metrics.filter((m) => m.testName === testName && m.filePath === filePath);
}
// Group by date and calculate average duration per day
const dailyAverages = new Map();
for (const metric of filteredMetrics) {
const date = new Date(metric.timestamp).toISOString().split('T')[0];
if (!dailyAverages.has(date)) {
dailyAverages.set(date, { total: 0, count: 0 });
}
const day = dailyAverages.get(date);
day.total += metric.duration;
day.count += 1;
}
const dates = Array.from(dailyAverages.keys()).sort();
const durations = dates.map((date) => {
const day = dailyAverages.get(date);
return day.total / day.count;
});
// Calculate trend
let trend = 'stable';
if (durations.length >= 2) {
const firstHalf = durations.slice(0, Math.floor(durations.length / 2));
const secondHalf = durations.slice(Math.floor(durations.length / 2));
const firstAvg = firstHalf.reduce((sum, d) => sum + d, 0) / firstHalf.length;
const secondAvg = secondHalf.reduce((sum, d) => sum + d, 0) / secondHalf.length;
const changePercent = ((secondAvg - firstAvg) / firstAvg) * 100;
if (changePercent > 10) {
trend = 'degrading';
}
else if (changePercent < -10) {
trend = 'improving';
}
}
return { dates, durations, trend };
}
/**
* Export health report to various formats
*/
exportHealthReport(report, format = 'json') {
switch (format) {
case 'markdown':
return this.exportToMarkdown(report);
case 'csv':
return this.exportToCsv(report);
default:
return JSON.stringify(report, null, 2);
}
}
exportToMarkdown(report) {
let markdown = '# Test Health Report\n\n';
markdown += `## Summary\n`;
markdown += `- **Total Tests**: ${report.totalTests}\n`;
markdown += `- **Average Execution Time**: ${report.averageExecutionTime.toFixed(2)}ms\n`;
markdown += `- **Slow Tests**: ${report.slowTests.length}\n`;
markdown += `- **Failing Tests**: ${report.failingTests.length}\n`;
markdown += `- **Flaky Tests**: ${report.flakyTests.length}\n`;
markdown += `- **Memory Issues**: ${report.memoryLeaks.length}\n\n`;
if (report.recommendations.length > 0) {
markdown += `## Recommendations\n\n`;
for (const rec of report.recommendations) {
markdown += `- ${rec}\n`;
}
markdown += '\n';
}
if (report.slowTests.length > 0) {
markdown += `## Slow Tests\n\n`;
markdown += `| Test | File | Duration | Status |\n`;
markdown += `|------|------|----------|--------|\n`;
for (const test of report.slowTests.slice(0, 10)) {
markdown += `| ${test.testName} | ${test.filePath} | ${test.duration}ms | ${test.status} |\n`;
}
markdown += '\n';
}
return markdown;
}
exportToCsv(report) {
let csv = 'Type,TestName,FilePath,Duration,Status,Timestamp,MemoryUsage\n';
const addTests = (tests, type) => {
for (const test of tests) {
csv += `${type},"${test.testName}","${test.filePath}",${test.duration},${test.status},${test.timestamp},${test.memoryUsage || ''}\n`;
}
};
addTests(report.slowTests, 'Slow');
addTests(report.failingTests, 'Failing');
addTests(report.flakyTests, 'Flaky');
addTests(report.memoryLeaks, 'MemoryLeak');
return csv;
}
}
/**
* Utility functions for test maintenance
*/
/**
* Create a test maintenance utility instance
*/
export function createTestMaintenanceUtility(metricsFile, thresholds) {
const finalThresholds = { ...DEFAULT_THRESHOLDS, ...thresholds };
return new TestMaintenanceUtility(metricsFile, finalThresholds);
}
/**
* Quick health check function for CI/CD integration
*/
export function performQuickHealthCheck() {
const utility = createTestMaintenanceUtility();
const report = utility.generateHealthReport(1); // Last 24 hours
const issues = [];
let healthy = true;
if (report.failingTests.length > 0) {
issues.push(`${report.failingTests.length} tests are currently failing`);
healthy = false;
}
if (report.flakyTests.length > 5) {
issues.push(`${report.flakyTests.length} flaky tests detected (threshold: 5)`);
healthy = false;
}
if (report.slowTests.length > 10) {
issues.push(`${report.slowTests.length} slow tests detected (threshold: 10)`);
}
if (report.averageExecutionTime > 3000) {
issues.push(`Average execution time is ${report.averageExecutionTime.toFixed(2)}ms (threshold: 3000ms)`);
}
return { healthy, issues, metrics: report };
}
//# sourceMappingURL=test-maintenance.js.map