rwsdk
Version:
Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime
384 lines (383 loc) • 22.6 kB
JavaScript
import { join, basename } from "path";
import { writeFile } from "fs/promises";
import { mkdirp } from "fs-extra";
import { log } from "./constants.mjs";
import { state } from "./state.mjs";
/**
* Maps a test status to a display string with emoji
*/
function formatTestStatus(status) {
switch (status) {
case "PASSED":
return "✅ PASSED";
case "FAILED":
return "❌ FAILED";
case "SKIPPED":
return "⏩ SKIPPED";
case "DID_NOT_RUN":
return "⚠️ DID NOT RUN";
default:
return "❓ UNKNOWN";
}
}
/**
* Generates the final test report without doing any resource cleanup.
*/
export async function generateFinalReport() {
try {
// Helper function to check if a failure matches the specified patterns
function failureMatches(failure, patterns, notPatterns = []) {
// Check if any of the patterns match in either step or error
const matchesPattern = patterns.some((pattern) => failure.step.includes(pattern) ||
(failure.error && failure.error.includes(pattern)));
// Check if any of the not-patterns match in either step or error
const matchesNotPattern = notPatterns.some((pattern) => failure.step.includes(pattern) ||
(failure.error && failure.error.includes(pattern)));
// Return true if it matches a pattern and doesn't match any not-pattern
return matchesPattern && !matchesNotPattern;
}
// Create a report object
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const report = {
timestamp,
success: state.exitCode === 0,
exitCode: state.exitCode,
workerName: (state.resources.workerName || null),
projectDir: state.options.artifactDir
? join(state.options.artifactDir, "project")
: null,
logFiles: state.options.artifactDir
? {
stdout: join(state.options.artifactDir, "logs", `stdout-${timestamp}.log`),
stderr: join(state.options.artifactDir, "logs", `stderr-${timestamp}.log`),
combined: join(state.options.artifactDir, "logs", `combined-${timestamp}.log`),
}
: null,
failures: state.failures,
options: {
customPath: state.options.customPath,
skipDev: state.options.skipDev,
skipRelease: state.options.skipRelease,
skipClient: state.options.skipClient,
},
testStatus: state.testStatus,
};
// Always print the report to console in a pretty format
console.log("\n==================================================");
console.log(" 📊 SMOKE TEST REPORT ");
console.log("==================================================");
console.log("--------------------------------------------------");
console.log(`Timestamp: ${timestamp}`);
console.log(`Status: ${report.success ? "✅ PASSED" : "❌ FAILED"}`);
console.log(`Exit code: ${state.exitCode}`);
if (report.workerName) {
console.log(`Worker name: ${report.workerName}`);
}
console.log(`Test options:`);
console.log(` - Custom path: ${report.options.customPath || "/"}`);
console.log(` - Skip dev: ${report.options.skipDev ? "Yes" : "No"}`);
console.log(` - Skip release: ${report.options.skipRelease ? "Yes" : "No"}`);
console.log(` - Skip client: ${report.options.skipClient ? "Yes" : "No"}`);
// Add info about log files
if (report.logFiles) {
console.log(`Log files:`);
console.log(` - stdout: ${basename(report.logFiles.stdout)}`);
console.log(` - stderr: ${basename(report.logFiles.stderr)}`);
console.log(` - combined: ${basename(report.logFiles.combined)}`);
}
console.log("--------------------------------------------------");
// Add summary of failures count
if (state.failures.length > 0) {
console.log(`\n❌ Failed tests: ${state.failures.length}`);
}
else if (report.success) {
console.log("\n✅ All smoke tests passed successfully!");
}
// Group failures by step to determine which stages had issues
const devFailures = state.failures.filter((f) => failureMatches(f, ["Development", "Development Server", "Development -"]));
const releaseFailures = state.failures.filter((f) => failureMatches(f, ["Production", "Release", "Production -"]));
// Add hierarchical test results overview
console.log("\n==================================================");
console.log(" 🔍 TEST RESULTS SUMMARY ");
console.log("==================================================");
// Dev tests summary using the new testStatus system
console.log(`● Development Tests: ${formatTestStatus(state.testStatus.dev.overall)}`);
// Only show details if the overall test status is not "SKIPPED" or "DID_NOT_RUN"
if (state.testStatus.dev.overall !== "SKIPPED" &&
state.testStatus.dev.overall !== "DID_NOT_RUN") {
console.log(` ├─ Initial Tests:`);
console.log(` │ ├─ Server-side: ${formatTestStatus(state.testStatus.dev.initialServerSide)}`);
console.log(` │ ├─ Client-side: ${formatTestStatus(state.testStatus.dev.initialClientSide)}`);
console.log(` │ ├─ Server Render Check: ${formatTestStatus(state.testStatus.dev.initialServerRenderCheck)}`);
console.log(` │ ├─ URL Styles: ${formatTestStatus(state.testStatus.dev.initialUrlStyles)}`);
console.log(` │ ├─ Client Module Styles: ${formatTestStatus(state.testStatus.dev.initialClientModuleStyles)}`);
console.log(` │ ├─ Server HMR: ${formatTestStatus(state.testStatus.dev.initialServerHmr)}`);
console.log(` │ └─ Client HMR: ${formatTestStatus(state.testStatus.dev.initialClientHmr)}`);
console.log(` └─ Realtime Tests:`);
console.log(` ├─ Upgrade: ${formatTestStatus(state.testStatus.dev.realtimeUpgrade)}`);
console.log(` ├─ Server-side: ${formatTestStatus(state.testStatus.dev.realtimeServerSide)}`);
console.log(` ├─ Client-side: ${formatTestStatus(state.testStatus.dev.realtimeClientSide)}`);
console.log(` ├─ Server Render Check: ${formatTestStatus(state.testStatus.dev.realtimeServerRenderCheck)}`);
console.log(` ├─ URL Styles: ${formatTestStatus(state.testStatus.dev.realtimeUrlStyles)}`);
console.log(` ├─ Client Module Styles: ${formatTestStatus(state.testStatus.dev.realtimeClientModuleStyles)}`);
console.log(` ├─ Server HMR: ${formatTestStatus(state.testStatus.dev.realtimeServerHmr)}`);
console.log(` └─ Client HMR: ${formatTestStatus(state.testStatus.dev.realtimeClientHmr)}`);
}
// Production tests summary using the new testStatus system
console.log(`● Production Tests: ${formatTestStatus(state.testStatus.production.overall)}`);
// Only show details if the overall test status is not "SKIPPED" or "DID_NOT_RUN"
if (state.testStatus.production.overall !== "SKIPPED" &&
state.testStatus.production.overall !== "DID_NOT_RUN") {
console.log(` ├─ Release Command: ${formatTestStatus(state.testStatus.production.releaseCommand)}`);
// Only show these if release command was either not run or passed
if (state.testStatus.production.releaseCommand !== "FAILED") {
console.log(` ├─ Initial Tests:`);
console.log(` │ ├─ Server-side: ${formatTestStatus(state.testStatus.production.initialServerSide)}`);
console.log(` │ ├─ Client-side: ${formatTestStatus(state.testStatus.production.initialClientSide)}`);
console.log(` │ ├─ Server Render Check: ${formatTestStatus(state.testStatus.production.initialServerRenderCheck)}`);
console.log(` │ ├─ URL Styles: ${formatTestStatus(state.testStatus.production.initialUrlStyles)}`);
console.log(` │ └─ Client Module Styles: ${formatTestStatus(state.testStatus.production.initialClientModuleStyles)}`);
console.log(` └─ Realtime Tests:`);
console.log(` ├─ Upgrade: ${formatTestStatus(state.testStatus.production.realtimeUpgrade)}`);
console.log(` ├─ Server-side: ${formatTestStatus(state.testStatus.production.realtimeServerSide)}`);
console.log(` ├─ Client-side: ${formatTestStatus(state.testStatus.production.realtimeClientSide)}`);
console.log(` ├─ Server Render Check: ${formatTestStatus(state.testStatus.production.realtimeServerRenderCheck)}`);
console.log(` ├─ URL Styles: ${formatTestStatus(state.testStatus.production.realtimeUrlStyles)}`);
console.log(` └─ Client Module Styles: ${formatTestStatus(state.testStatus.production.realtimeClientModuleStyles)}`);
}
else {
console.log(` └─ Tests: ⏩ SKIPPED (release command failed)`);
}
}
// Add failures to the report file if we have a valid artifactDir
if (state.options.artifactDir) {
try {
// Ensure the directory exists, even if it was not created earlier
await mkdirp(state.options.artifactDir);
// Use the standardized reports directory
const reportDir = join(state.options.artifactDir, "reports");
// Ensure the directory exists
await mkdirp(reportDir);
const reportPath = join(reportDir, `smoke-test-report-${timestamp}.json`);
await writeFile(reportPath, JSON.stringify(report, null, 2));
console.log(`\n📝 Report saved to ${reportPath}`);
}
catch (reportError) {
console.error(`⚠️ Could not save report to file: ${reportError instanceof Error ? reportError.message : String(reportError)}`);
}
}
else {
console.log("\n⚠️ No artifacts directory specified, report not saved to disk");
}
// Report failures with clear environment context
if (state.failures.length > 0) {
console.log("\n==================================================");
console.log(" 🔍 FAILURE DETAILS ");
console.log("==================================================");
// Group failures by environment (Dev vs Release)
if (devFailures.length > 0) {
console.log("----------------- DEVELOPMENT ENVIRONMENT -----------------");
devFailures.forEach((failure, index) => {
console.log(`Failure #${index + 1}: ${failure.step}`);
// Split error message into lines if it's long
const errorLines = failure.error.split("\n");
console.log(`Error: ${errorLines[0]}`);
for (let i = 1; i < errorLines.length; i++) {
console.log(` ${errorLines[i]}`);
}
console.log(``);
});
console.log(`--------------------------------------------------`);
}
if (releaseFailures.length > 0) {
console.log("----------------- PRODUCTION ENVIRONMENT -----------------");
releaseFailures.forEach((failure, index) => {
console.log(`Failure #${index + 1}: ${failure.step}`);
// Split error message into lines if it's long
const errorLines = failure.error.split("\n");
console.log(`Error: ${errorLines[0]}`);
for (let i = 1; i < errorLines.length; i++) {
console.log(` ${errorLines[i]}`);
}
console.log(``);
});
console.log(`--------------------------------------------------`);
}
// Show other failures that don't fit into the above categories
const otherFailures = state.failures.filter((f) => !devFailures.includes(f) && !releaseFailures.includes(f));
if (otherFailures.length > 0) {
console.log("----------------- OTHER FAILURES -----------------");
otherFailures.forEach((failure, index) => {
console.log(`Failure #${index + 1}: ${failure.step}`);
// Split error message into lines if it's long
const errorLines = failure.error.split("\n");
console.log(`Error: ${errorLines[0]}`);
for (let i = 1; i < errorLines.length; i++) {
console.log(` ${errorLines[i]}`);
}
console.log(``);
});
console.log(`--------------------------------------------------`);
}
}
}
catch (error) {
// Last resort error handling
console.error("❌ Failed to generate report:", error);
}
}
/**
* Updates the test status in the state object and reports the result.
*/
export function reportSmokeTestResult(result, type, phase = "", environment = "Development") {
const phasePrefix = phase ? `(${phase}) ` : "";
log("Reporting %s%s smoke test result: %O", phasePrefix, type, result);
if (result.verificationPassed) {
console.log(`✅ ${phasePrefix}${type} smoke test passed!`);
if (result.serverTimestamp) {
console.log(`✅ Server timestamp: ${result.serverTimestamp}`);
}
if (result.clientTimestamp) {
console.log(`✅ Client timestamp: ${result.clientTimestamp}`);
}
}
else {
log("ERROR: %s%s smoke test failed. Status: %s. Error: %s", phasePrefix, type, result.status, result.error || "unknown");
// The actual state update and error throwing is now handled by the caller functions
// We only need to report the result in the console
console.error(`❌ ${phasePrefix}${type} smoke test failed. Status: ${result.status}${result.error ? `. Error: ${result.error}` : ""}`);
}
}
/**
* Initialize test statuses based on test options
*/
export function initializeTestStatus() {
// Set default status for all tests as "DID_NOT_RUN"
// Dev tests
state.testStatus.dev.overall = "DID_NOT_RUN";
state.testStatus.dev.initialServerSide = "DID_NOT_RUN";
state.testStatus.dev.initialClientSide = "DID_NOT_RUN";
state.testStatus.dev.initialServerRenderCheck = "DID_NOT_RUN";
state.testStatus.dev.realtimeUpgrade = "DID_NOT_RUN";
state.testStatus.dev.realtimeServerSide = "DID_NOT_RUN";
state.testStatus.dev.realtimeClientSide = "DID_NOT_RUN";
state.testStatus.dev.realtimeServerRenderCheck = "DID_NOT_RUN";
state.testStatus.dev.initialServerHmr = "DID_NOT_RUN";
state.testStatus.dev.initialClientHmr = "DID_NOT_RUN";
state.testStatus.dev.realtimeServerHmr = "DID_NOT_RUN";
state.testStatus.dev.realtimeClientHmr = "DID_NOT_RUN";
state.testStatus.dev.initialUrlStyles = "DID_NOT_RUN";
state.testStatus.dev.initialClientModuleStyles = "DID_NOT_RUN";
state.testStatus.dev.realtimeUrlStyles = "DID_NOT_RUN";
state.testStatus.dev.realtimeClientModuleStyles = "DID_NOT_RUN";
// Production tests
state.testStatus.production.overall = "DID_NOT_RUN";
state.testStatus.production.releaseCommand = "DID_NOT_RUN";
state.testStatus.production.initialServerSide = "DID_NOT_RUN";
state.testStatus.production.initialClientSide = "DID_NOT_RUN";
state.testStatus.production.initialServerRenderCheck = "DID_NOT_RUN";
state.testStatus.production.realtimeUpgrade = "DID_NOT_RUN";
state.testStatus.production.realtimeServerSide = "DID_NOT_RUN";
state.testStatus.production.realtimeClientSide = "DID_NOT_RUN";
state.testStatus.production.realtimeServerRenderCheck = "DID_NOT_RUN";
state.testStatus.production.initialServerHmr = "DID_NOT_RUN";
state.testStatus.production.initialClientHmr = "DID_NOT_RUN";
state.testStatus.production.realtimeServerHmr = "DID_NOT_RUN";
state.testStatus.production.realtimeClientHmr = "DID_NOT_RUN";
state.testStatus.production.initialUrlStyles = "DID_NOT_RUN";
state.testStatus.production.initialClientModuleStyles = "DID_NOT_RUN";
state.testStatus.production.realtimeUrlStyles = "DID_NOT_RUN";
state.testStatus.production.realtimeClientModuleStyles = "DID_NOT_RUN";
// Now override with specific statuses based on options
// Mark skipped tests based on options
if (state.options.skipDev) {
state.testStatus.dev.overall = "SKIPPED";
state.testStatus.dev.initialServerSide = "SKIPPED";
state.testStatus.dev.initialClientSide = "SKIPPED";
state.testStatus.dev.initialServerRenderCheck = "SKIPPED";
state.testStatus.dev.realtimeUpgrade = "SKIPPED";
state.testStatus.dev.realtimeServerSide = "SKIPPED";
state.testStatus.dev.realtimeClientSide = "SKIPPED";
state.testStatus.dev.realtimeServerRenderCheck = "SKIPPED";
state.testStatus.dev.initialServerHmr = "SKIPPED";
state.testStatus.dev.initialClientHmr = "SKIPPED";
state.testStatus.dev.realtimeServerHmr = "SKIPPED";
state.testStatus.dev.realtimeClientHmr = "SKIPPED";
state.testStatus.dev.initialUrlStyles = "SKIPPED";
state.testStatus.dev.initialClientModuleStyles = "SKIPPED";
state.testStatus.dev.realtimeUrlStyles = "SKIPPED";
state.testStatus.dev.realtimeClientModuleStyles = "SKIPPED";
}
if (state.options.skipRelease) {
state.testStatus.production.overall = "SKIPPED";
state.testStatus.production.releaseCommand = "SKIPPED";
state.testStatus.production.initialServerSide = "SKIPPED";
state.testStatus.production.initialClientSide = "SKIPPED";
state.testStatus.production.initialServerRenderCheck = "SKIPPED";
state.testStatus.production.realtimeUpgrade = "SKIPPED";
state.testStatus.production.realtimeServerSide = "SKIPPED";
state.testStatus.production.realtimeClientSide = "SKIPPED";
state.testStatus.production.realtimeServerRenderCheck = "SKIPPED";
state.testStatus.production.initialServerHmr = "SKIPPED";
state.testStatus.production.initialClientHmr = "SKIPPED";
state.testStatus.production.realtimeServerHmr = "SKIPPED";
state.testStatus.production.realtimeClientHmr = "SKIPPED";
state.testStatus.production.initialUrlStyles = "SKIPPED";
state.testStatus.production.initialClientModuleStyles = "SKIPPED";
state.testStatus.production.realtimeUrlStyles = "SKIPPED";
state.testStatus.production.realtimeClientModuleStyles = "SKIPPED";
}
if (state.options.skipClient) {
state.testStatus.dev.initialClientSide = "SKIPPED";
state.testStatus.dev.realtimeClientSide = "SKIPPED";
state.testStatus.production.initialClientSide = "SKIPPED";
state.testStatus.production.realtimeClientSide = "SKIPPED";
state.testStatus.dev.initialServerRenderCheck = "SKIPPED";
state.testStatus.dev.realtimeServerRenderCheck = "SKIPPED";
state.testStatus.production.initialServerRenderCheck = "SKIPPED";
state.testStatus.production.realtimeServerRenderCheck = "SKIPPED";
state.testStatus.dev.initialClientHmr = "SKIPPED";
state.testStatus.dev.realtimeClientHmr = "SKIPPED";
state.testStatus.production.initialClientHmr = "SKIPPED";
state.testStatus.production.realtimeClientHmr = "SKIPPED";
}
// Skip HMR tests in production and when requested
state.testStatus.production.initialServerHmr = "SKIPPED";
state.testStatus.production.initialClientHmr = "SKIPPED";
state.testStatus.production.realtimeServerHmr = "SKIPPED";
state.testStatus.production.realtimeClientHmr = "SKIPPED";
if (state.options.skipHmr) {
state.testStatus.dev.initialServerHmr = "SKIPPED";
state.testStatus.dev.initialClientHmr = "SKIPPED";
state.testStatus.dev.realtimeServerHmr = "SKIPPED";
state.testStatus.dev.realtimeClientHmr = "SKIPPED";
}
// Handle realtime option which skips initial tests
if (state.options.realtime) {
// In realtime mode, initial tests are skipped
if (!state.options.skipDev) {
state.testStatus.dev.initialServerSide = "SKIPPED";
state.testStatus.dev.initialClientSide = "SKIPPED";
state.testStatus.dev.initialServerRenderCheck = "SKIPPED";
state.testStatus.dev.initialServerHmr = "SKIPPED";
state.testStatus.dev.initialClientHmr = "SKIPPED";
state.testStatus.dev.initialUrlStyles = "SKIPPED";
state.testStatus.dev.initialClientModuleStyles = "SKIPPED";
// Set the upgrade test to PASSED as it's implicitly run for realtime mode
state.testStatus.dev.realtimeUpgrade = "PASSED";
}
if (!state.options.skipRelease) {
state.testStatus.production.initialServerSide = "SKIPPED";
state.testStatus.production.initialClientSide = "SKIPPED";
state.testStatus.production.initialServerRenderCheck = "SKIPPED";
state.testStatus.production.initialServerHmr = "SKIPPED";
state.testStatus.production.initialClientHmr = "SKIPPED";
state.testStatus.production.initialUrlStyles = "SKIPPED";
state.testStatus.production.initialClientModuleStyles = "SKIPPED";
// Set release command to PASSED since it must have succeeded for realtime tests to run
state.testStatus.production.releaseCommand = "PASSED";
// Set the upgrade test to PASSED as it's implicitly run for realtime mode
state.testStatus.production.realtimeUpgrade = "PASSED";
}
}
}