@casoon/auditmysite
Version:
A comprehensive command-line tool for automated accessibility, security, performance, and SEO testing using Playwright and pa11y, based on sitemap URLs
309 lines (251 loc) • 9.22 kB
text/typescript
import { EventDrivenQueue, EventDrivenQueueOptions, QueueStats } from './event-driven-queue';
import { AccessibilityChecker } from './accessibility-checker';
import { TestOptions, AccessibilityResult } from '../types';
export interface ParallelTestManagerOptions extends EventDrivenQueueOptions {
// Queue-spezifische Optionen
maxConcurrent?: number;
maxRetries?: number;
retryDelay?: number;
// Test-spezifische Optionen
testOptions?: TestOptions;
// Progress-Reporting
enableProgressBar?: boolean;
progressUpdateInterval?: number;
// Resource Management
enableResourceMonitoring?: boolean;
maxMemoryUsage?: number; // MB
maxCpuUsage?: number; // Prozent
// Event Callbacks
onTestStart?: (url: string) => void;
onTestComplete?: (url: string, result: AccessibilityResult) => void;
onTestError?: (url: string, error: string) => void;
onProgressUpdate?: (stats: QueueStats) => void;
onQueueEmpty?: () => void;
}
export interface ParallelTestResult {
results: AccessibilityResult[];
stats: QueueStats;
duration: number;
errors: Array<{ url: string; error: string; attempts: number }>;
}
export class ParallelTestManager {
private queue: EventDrivenQueue;
private accessibilityChecker: AccessibilityChecker;
private options: ParallelTestManagerOptions;
private isRunning = false;
private startTime: Date | null = null;
private activeTests: Map<string, Promise<AccessibilityResult>> = new Map();
constructor(options: ParallelTestManagerOptions = {}) {
this.options = {
maxConcurrent: 3,
maxRetries: 3,
retryDelay: 1000,
enableProgressBar: true,
progressUpdateInterval: 1000,
enableResourceMonitoring: true,
maxMemoryUsage: 512, // 512 MB
maxCpuUsage: 80, // 80%
...options
};
// Event-Driven Queue initialisieren
this.queue = new EventDrivenQueue({
maxRetries: this.options.maxRetries,
maxConcurrent: this.options.maxConcurrent,
retryDelay: this.options.retryDelay,
enableEvents: true,
eventCallbacks: {
onUrlAdded: this.handleUrlAdded.bind(this),
onUrlStarted: this.handleUrlStarted.bind(this),
onUrlCompleted: this.handleUrlCompleted.bind(this),
onUrlFailed: this.handleUrlFailed.bind(this),
onUrlRetrying: this.handleUrlRetrying.bind(this),
onQueueEmpty: this.handleQueueEmpty.bind(this),
onProgressUpdate: this.handleProgressUpdate.bind(this),
onError: this.handleError.bind(this)
}
});
// Accessibility Checker initialisieren
this.accessibilityChecker = new AccessibilityChecker();
}
async initialize(): Promise<void> {
await this.accessibilityChecker.initialize();
console.log(`🚀 Parallel Test Manager initialized with ${this.options.maxConcurrent} concurrent workers`);
}
async runTests(urls: string[]): Promise<ParallelTestResult> {
if (this.isRunning) {
throw new Error('Test manager is already running');
}
this.isRunning = true;
this.startTime = new Date();
this.activeTests.clear();
console.log(`🧪 Starting parallel tests for ${urls.length} URLs with ${this.options.maxConcurrent} workers`);
// URLs zur Queue hinzufügen
this.queue.addUrls(urls);
// Event-Listener für Queue-Events
this.setupEventListeners();
// Parallele Test-Ausführung starten
await this.processQueue();
const duration = this.startTime ? Date.now() - this.startTime.getTime() : 0;
const stats = this.queue.getStats();
const results = this.queue.getCompletedResults();
const errors = this.queue.getFailedResults();
this.isRunning = false;
return {
results,
stats,
duration,
errors
};
}
private async processQueue(): Promise<void> {
return new Promise((resolve, reject) => {
// Event-Listener für Queue-Ende
this.queue.onQueueEmpty(() => {
resolve();
});
// Event-Listener für Fehler
this.queue.onError((event) => {
reject(new Error(event.data.error));
});
// Starte Worker-Prozesse
this.startWorkers();
});
}
private startWorkers(): void {
// Starte initiale Worker bis zur maxConcurrent-Grenze
for (let i = 0; i < this.options.maxConcurrent!; i++) {
this.processNextUrl();
}
}
private async processNextUrl(): Promise<void> {
if (!this.isRunning) return;
const queuedUrl = this.queue.getNextUrl();
if (!queuedUrl) return;
try {
// Resource-Monitoring
if (this.options.enableResourceMonitoring) {
this.checkResourceLimits();
}
// Test ausführen
const testPromise = this.accessibilityChecker.testPage(queuedUrl.url, this.options.testOptions);
this.activeTests.set(queuedUrl.url, testPromise);
const result = await testPromise;
this.queue.markCompleted(queuedUrl.url, result);
} catch (error) {
this.queue.markFailed(queuedUrl.url, String(error));
} finally {
this.activeTests.delete(queuedUrl.url);
// Starte nächsten Worker
this.processNextUrl();
}
}
private checkResourceLimits(): void {
const memoryUsage = process.memoryUsage().heapUsed / 1024 / 1024; // MB
const cpuUsage = process.cpuUsage().user / 1000000; // Sekunden
if (memoryUsage > this.options.maxMemoryUsage!) {
console.warn(`⚠️ High memory usage: ${memoryUsage.toFixed(2)} MB`);
// Optional: Queue pausieren oder Worker reduzieren
}
if (cpuUsage > this.options.maxCpuUsage!) {
console.warn(`⚠️ High CPU usage: ${cpuUsage.toFixed(2)}s`);
// Optional: Queue pausieren oder Worker reduzieren
}
}
// Event Handler
private handleUrlAdded(url: string, priority: number): void {
console.log(`📋 Added URL to queue: ${url} (priority: ${priority})`);
}
private handleUrlStarted(url: string): void {
console.log(`🚀 Started testing: ${url}`);
this.options.onTestStart?.(url);
}
private handleUrlCompleted(url: string, result: AccessibilityResult, duration: number): void {
const status = result.passed ? '✅ PASSED' : '❌ FAILED';
console.log(`${status} ${url} (${duration}ms) - ${result.errors.length} errors, ${result.warnings.length} warnings`);
this.options.onTestComplete?.(url, result);
}
private handleUrlFailed(url: string, error: string, attempts: number): void {
console.log(`💥 Failed ${url} (attempt ${attempts}): ${error}`);
this.options.onTestError?.(url, error);
}
private handleUrlRetrying(url: string, attempts: number): void {
console.log(`🔄 Retrying ${url} (attempt ${attempts + 1}/${this.options.maxRetries})`);
}
private handleQueueEmpty(): void {
console.log('🎉 All tests completed!');
this.options.onQueueEmpty?.();
}
private handleProgressUpdate(stats: QueueStats): void {
if (this.options.enableProgressBar) {
this.displayProgressBar(stats);
}
this.options.onProgressUpdate?.(stats);
}
private handleError(error: string): void {
console.error(`❌ Queue error: ${error}`);
}
private setupEventListeners(): void {
// Progress-Update-Interval
if (this.options.enableProgressBar) {
setInterval(() => {
const stats = this.queue.getStats();
this.handleProgressUpdate(stats);
}, this.options.progressUpdateInterval);
}
}
private displayProgressBar(stats: QueueStats): void {
const progress = Math.round(stats.progress);
const barLength = 30;
const filledLength = Math.round((progress / 100) * barLength);
const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
const eta = stats.estimatedTimeRemaining > 0
? `ETA: ${Math.round(stats.estimatedTimeRemaining / 1000)}s`
: '';
const memory = `Memory: ${stats.memoryUsage}MB`;
const workers = `Workers: ${stats.activeWorkers}/${this.options.maxConcurrent}`;
process.stdout.write(`\r🧪 Progress: [${bar}] ${progress}% | ${stats.completed}/${stats.total} | ${workers} | ${memory} | ${eta}`);
if (stats.progress >= 100) {
process.stdout.write('\n');
}
}
// Public API
pause(): void {
this.queue.pause();
console.log('⏸️ Tests paused');
}
resume(): void {
this.queue.resume();
console.log('▶️ Tests resumed');
this.startWorkers();
}
stop(): void {
this.isRunning = false;
this.queue.clear();
console.log('⏹️ Tests stopped');
}
getStats(): QueueStats {
return this.queue.getStats();
}
getActiveTests(): number {
return this.activeTests.size;
}
getQueueSize(): number {
return this.queue.getQueueSize();
}
setMaxConcurrent(max: number): void {
this.queue.setMaxConcurrent(max);
console.log(`🔧 Max concurrent workers set to ${max}`);
}
// Resource Management
getMemoryUsage(): number {
return process.memoryUsage().heapUsed / 1024 / 1024;
}
getCpuUsage(): number {
return process.cpuUsage().user / 1000000;
}
// Cleanup
async cleanup(): Promise<void> {
this.stop();
await this.accessibilityChecker.cleanup();
}
}