UNPKG

panoptical

Version:

A modern testing tool that makes Playwright easy to use with simple YAML syntax

346 lines (288 loc) 10.4 kB
import chalk from 'chalk'; import fs from 'fs'; import path from 'path'; const RUNS_FILE = '.panoptical/runs.json'; const FLAKES_FILE = '.panoptical/flakes.json'; /** * Clean up test reports based on the specified strategy * @param {Object} options - Cleanup options * @param {string} options.strategy - Cleanup strategy: 'orphaned', 'specific', 'all' * @param {string} [options.testName] - Specific test name to remove (for 'specific' strategy) * @param {boolean} [options.force] - Skip confirmation prompts */ export async function cleanupReports(options = {}) { const { strategy, testName, force = false } = options; try { // Ensure .panoptical directory exists if (!fs.existsSync('.panoptical')) { console.log(chalk.yellow('No .panoptical directory found. Nothing to clean up.')); return; } // Load current data const runsData = loadJsonFile(RUNS_FILE, {}); const flakesData = loadJsonFile(FLAKES_FILE, {}); let removedTests = []; switch (strategy) { case 'orphaned': removedTests = await cleanupOrphanedTests(runsData, flakesData, force); break; case 'specific': if (!testName) { throw new Error('Test name is required for specific cleanup'); } removedTests = await cleanupSpecificTest(runsData, flakesData, testName, force); break; case 'all': removedTests = await cleanupAllTests(runsData, flakesData, force); break; default: throw new Error(`Unknown cleanup strategy: ${strategy}`); } if (removedTests.length === 0) { console.log(chalk.cyan('No tests were removed.')); return; } // Save updated data await saveJsonFile(RUNS_FILE, runsData); await saveJsonFile(FLAKES_FILE, flakesData); // Show summary console.log(chalk.green(`\nSuccessfully cleaned up ${removedTests.length} test(s):`)); removedTests.forEach(test => { console.log(chalk.white(` • ${test}`)); }); // Recalculate and show new statistics await showUpdatedStatistics(runsData); } catch (error) { console.error(chalk.red(`\nCleanup failed: ${error.message}`)); process.exit(1); } } /** * Remove tests that no longer exist as YAML files */ async function cleanupOrphanedTests(runsData, flakesData, force = false) { console.log(chalk.blue('\nScanning for orphaned tests...')); const existingTests = await getExistingTestFiles(); const reportedTests = Object.keys(runsData); const orphanedTests = reportedTests.filter(test => !existingTests.includes(test)); if (orphanedTests.length === 0) { console.log(chalk.cyan('No orphaned tests found.')); return []; } console.log(chalk.yellow(`Found ${orphanedTests.length} orphaned test(s):`)); orphanedTests.forEach(test => { console.log(chalk.white(` • ${test}`)); }); // Ask for confirmation unless force flag is set if (!force) { const confirmed = await askForConfirmation( `\nDo you want to remove these ${orphanedTests.length} orphaned test(s)? (y/N)` ); if (!confirmed) { console.log(chalk.cyan('Cleanup cancelled.')); return []; } } // Remove orphaned tests orphanedTests.forEach(test => { delete runsData[test]; delete flakesData[test]; }); return orphanedTests; } /** * Remove a specific test from reports */ async function cleanupSpecificTest(runsData, flakesData, testName, force = false) { console.log(chalk.blue(`\nRemoving specific test: ${testName}`)); if (!runsData[testName] && !flakesData[testName]) { console.log(chalk.yellow(`Test '${testName}' not found in reports.`)); return []; } // Show test details before removal const runCount = runsData[testName]?.length || 0; const flakeData = flakesData[testName]; console.log(chalk.white(` • Test runs: ${runCount}`)); if (flakeData) { console.log(chalk.white(` • Flakiness data: Available`)); } // Ask for confirmation unless force flag is set if (!force) { const confirmed = await askForConfirmation( `\nDo you want to remove '${testName}' and all its ${runCount} run(s)? (y/N)` ); if (!confirmed) { console.log(chalk.cyan('Cleanup cancelled.')); return []; } } // Remove the test delete runsData[testName]; delete flakesData[testName]; return [testName]; } /** * Remove all test data (destructive operation) */ async function cleanupAllTests(runsData, flakesData, force = false) { console.log(chalk.red('\nWARNING: This will remove ALL test data!')); const totalTests = Object.keys(runsData).length; const totalRuns = Object.values(runsData).reduce((sum, runs) => sum + runs.length, 0); console.log(chalk.white(` • Total tests: ${totalTests}`)); console.log(chalk.white(` • Total runs: ${totalRuns}`)); console.log(chalk.white(` • Flakiness data: ${Object.keys(flakesData).length} tests`)); // Ask for confirmation with stronger warning unless force flag is set if (!force) { const confirmed = await askForConfirmation( `\nAre you ABSOLUTELY sure you want to delete ALL test data? This cannot be undone! (yes/NO)`, 'yes' ); if (!confirmed) { console.log(chalk.cyan('Cleanup cancelled.')); return []; } } // Clear all data const removedTests = Object.keys(runsData); Object.keys(runsData).forEach(key => delete runsData[key]); Object.keys(flakesData).forEach(key => delete flakesData[key]); return removedTests; } /** * Get list of existing test YAML files (recursively searches subdirectories) */ async function getExistingTestFiles() { const testsDir = 'tests'; if (!fs.existsSync(testsDir)) { return []; } try { const testFiles = []; function scanDirectory(dir) { const items = fs.readdirSync(dir); for (const item of items) { const fullPath = path.join(dir, item); const stat = fs.statSync(fullPath); if (stat.isDirectory()) { // Recursively scan subdirectories scanDirectory(fullPath); } else if (item.endsWith('.yaml') || item.endsWith('.yml')) { // Add test file (just the filename, not the full path) testFiles.push(item); } } } scanDirectory(testsDir); return testFiles; } catch (error) { console.warn(chalk.yellow(`Warning: Could not read tests directory: ${error.message}`)); return []; } } /** * Load JSON file safely */ function loadJsonFile(filePath, defaultValue = {}) { try { if (fs.existsSync(filePath)) { const content = fs.readFileSync(filePath, 'utf8'); return JSON.parse(content); } } catch (error) { console.warn(chalk.yellow(`Warning: Could not parse ${filePath}: ${error.message}`)); } return defaultValue; } /** * Save JSON file safely */ async function saveJsonFile(filePath, data) { try { // Ensure directory exists const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } // Write file with pretty formatting fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8'); } catch (error) { throw new Error(`Failed to save ${filePath}: ${error.message}`); } } /** * Ask for user confirmation */ async function askForConfirmation(message, expectedAnswer = 'y') { const readline = await import('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { rl.question(message + ' ', (answer) => { rl.close(); resolve(answer.toLowerCase() === expectedAnswer.toLowerCase()); }); }); } /** * Show updated statistics after cleanup */ async function showUpdatedStatistics(runsData) { const totalTests = Object.keys(runsData).length; const totalRuns = Object.values(runsData).reduce((sum, runs) => sum + runs.length, 0); if (totalTests === 0) { console.log(chalk.cyan('\nAll test data has been removed.')); return; } const passedRuns = Object.values(runsData).reduce((sum, runs) => sum + runs.filter(r => r.status === 'pass').length, 0 ); const passRate = totalRuns > 0 ? Math.round((passedRuns / totalRuns) * 100) : 0; console.log(chalk.blue('\nUpdated Statistics:')); console.log(chalk.white(` • Total tests: ${totalTests}`)); console.log(chalk.white(` • Total runs: ${totalRuns}`)); console.log(chalk.white(` • Pass rate: ${passRate}%`)); } /** * Automatically maintain test run limits (called periodically or after test runs) * @param {number} maxRuns - Maximum runs to keep per test (default: 50) * @param {boolean} saveToFile - Whether to save the limited data back to files */ export async function maintainRunLimits(maxRuns = 50, saveToFile = true) { try { if (!fs.existsSync('.panoptical')) { return; } const runsData = loadJsonFile(RUNS_FILE, {}); const flakesData = loadJsonFile(FLAKES_FILE, {}); let totalRunsRemoved = 0; let testsTruncated = 0; for (const [testName, runs] of Object.entries(runsData)) { if (runs.length > maxRuns) { const runsToRemove = runs.length - maxRuns; runsData[testName] = runs.slice(-maxRuns); // Keep newest runs totalRunsRemoved += runsToRemove; testsTruncated++; // Also update flakes data if it exists if (flakesData[testName] && flakesData[testName].runs) { const remainingRunTimestamps = new Set(runsData[testName].map(r => r.ts)); flakesData[testName].runs = flakesData[testName].runs.filter(r => remainingRunTimestamps.has(r.ts) ); } } } if (totalRunsRemoved > 0) { console.log(chalk.blue(`Auto-maintenance: Limited ${testsTruncated} tests to ${maxRuns} runs, removed ${totalRunsRemoved} old runs`)); if (saveToFile) { await saveJsonFile(RUNS_FILE, runsData); await saveJsonFile(FLAKES_FILE, flakesData); } } return { totalRunsRemoved, testsTruncated }; } catch (error) { console.warn(chalk.yellow(`Auto-maintenance failed: ${error.message}`)); return { totalRunsRemoved: 0, testsTruncated: 0 }; } }