UNPKG

@arghajit/dummy

Version:

A Playwright reporter and dashboard for visualizing test results.

227 lines (194 loc) โ€ข 7.22 kB
import * as fs from "fs/promises"; import * as path from "path"; import { getReporterConfig } from "./config-reader.mjs"; /** * Reads all `<outputFile>-*.json` files in the `pulse-results` directory * and merges them into a single `<outputFile>.json`. * It resolves duplicate tests using exactly the same logic as the reporter. * * @param {string} customOutputDir The base report directory override (from CLI). * @param {boolean} forceMerge Try to merge regardless of config.resetOnEachRun (used by sharded merge). */ export async function mergeSequentialReportsIfNeeded(customOutputDir, forceMerge = false) { const config = await getReporterConfig(customOutputDir); // This logic should ONLY run if resetOnEachRun is disabled, UNLESS we are forcing it // (e.g. recovering orphaned shards in merge-pulse-report.mjs). if (config.resetOnEachRun && !forceMerge) { return; } const individualReportsSubDir = config.individualReportsSubDir; const baseOutputFile = config.outputFile; // If customOutputDir is provided, it might be an absolute path to a shard. Use it directly if it is absolute. // Otherwise, fall back to the config's outputDir (which is resolved relative to CWD). const outputDir = customOutputDir && path.isAbsolute(customOutputDir) ? customOutputDir : config.outputDir; const pulseResultsDir = path.join(outputDir, individualReportsSubDir); const finalOutputPath = path.join(outputDir, baseOutputFile); // Use the actual outputFile name as seed for shard files (e.g. "results.json" -> "results-") const shardPrefix = baseOutputFile.replace(".json", "-"); let reportFiles; try { const allFiles = await fs.readdir(pulseResultsDir); reportFiles = allFiles.filter( (file) => file.startsWith(shardPrefix) && file.endsWith(".json"), ); } catch (error) { if (error.code === "ENOENT") { // No individual reports directory found, which is completely fine/normal return; } console.error( `Pulse Reporter: Error reading directory ${pulseResultsDir}:`, error, ); return; } if (reportFiles.length === 0) { // No matching JSON report files found to merge return; } console.log( `\n๐Ÿ”„ Merging ${reportFiles.length} sequential test run(s) from '${individualReportsSubDir}'...`, ); const allResultsFromAllFiles = []; let latestTimestamp = new Date(0); let lastRunEnvironment = undefined; let totalDuration = 0; for (const file of reportFiles) { const filePath = path.join(pulseResultsDir, file); try { const content = await fs.readFile(filePath, "utf-8"); const json = JSON.parse(content); let currentRunId = `run-${Date.now()}`; if (json.run) { if (json.run.id) currentRunId = json.run.id; const runTimestamp = new Date(json.run.timestamp); if (runTimestamp > latestTimestamp) { latestTimestamp = runTimestamp; lastRunEnvironment = json.run.environment || undefined; } } if (json.results) { // Tag each result with its runId to ensure we can sum them up if they have same IDs but different runs const resultsWithRunId = json.results.map((r) => ({ ...r, runId: currentRunId, })); allResultsFromAllFiles.push(...resultsWithRunId); } } catch (err) { console.warn( `Pulse Reporter: Could not parse report file ${filePath}. Skipping. Error: ${err.message}`, ); } } // De-duplicate the results from ALL merged files using the same logic as the reporter const finalMergedResults = getFinalizedResults(allResultsFromAllFiles); totalDuration = finalMergedResults.reduce( (acc, r) => acc + (r.duration || 0), 0, ); const combinedRun = { id: `run-${Date.now()}`, timestamp: latestTimestamp.toISOString(), environment: lastRunEnvironment, totalTests: finalMergedResults.length, passed: finalMergedResults.filter( (r) => (r.final_status || r.status) === "passed", ).length, failed: finalMergedResults.filter( (r) => (r.final_status || r.status) === "failed", ).length, skipped: finalMergedResults.filter( (r) => (r.final_status || r.status) === "skipped", ).length, flaky: finalMergedResults.filter( (r) => (r.final_status || r.status) === "flaky", ).length, duration: totalDuration, }; const finalReport = { run: combinedRun, results: finalMergedResults, metadata: { generatedAt: new Date().toISOString(), }, }; try { await fs.writeFile( finalOutputPath, JSON.stringify( finalReport, (key, value) => { if (value instanceof Date) return value.toISOString(); return value; }, 2, ), ); console.log( `โœ… Merged report with ${finalMergedResults.length} total results saved to ${finalOutputPath}`, ); // Clean up the pulse-results directory after a successful merge try { await fs.rm(pulseResultsDir, { recursive: true, force: true }); console.log( `๐Ÿงน Cleaned up temporary reports directory at ${pulseResultsDir}`, ); } catch (cleanupErr) { console.warn( `Pulse Reporter: Could not clean up individual reports directory. Error: ${cleanupErr.message}`, ); } } catch (err) { console.error( `Pulse Reporter: Failed to write final merged report to ${finalOutputPath}. Error: ${err.message}`, ); } } function getFinalizedResults(allResults) { const resultsMap = new Map(); for (const result of allResults) { // Unique key is runId + testId to support summation of tests across different sequential runs const compositeId = `${result.runId || "unknown"}-${result.id}`; if (!resultsMap.has(compositeId)) { resultsMap.set(compositeId, []); } resultsMap.get(compositeId).push(result); } const finalResults = []; for (const [testId, attempts] of resultsMap.entries()) { // Sort by retry count (ASC) then timestamp (DESC) to ensure stable resolution attempts.sort((a, b) => { if (a.retries !== b.retries) return a.retries - b.retries; return new Date(b.startTime).getTime() - new Date(a.startTime).getTime(); }); const firstAttempt = attempts[0]; const retryAttempts = attempts.slice(1); const hasActualRetries = retryAttempts.length > 0 && retryAttempts.some( (attempt) => attempt.status === "failed" || attempt.status === "flaky" || firstAttempt.status === "failed" || firstAttempt.status === "flaky" ); if (hasActualRetries) { firstAttempt.retryHistory = retryAttempts; const lastAttempt = attempts[attempts.length - 1]; firstAttempt.final_status = lastAttempt.status; if (lastAttempt.outcome === "flaky" || lastAttempt.status === "flaky") { firstAttempt.outcome = "flaky"; firstAttempt.status = "flaky"; } } else { delete firstAttempt.final_status; delete firstAttempt.retryHistory; } finalResults.push(firstAttempt); } return finalResults; }