@arghajit/dummy
Version:
A Playwright reporter and dashboard for visualizing test results.
227 lines (194 loc) โข 7.22 kB
JavaScript
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;
}