UNPKG

testeranto

Version:

the AI powered BDD test framework for typescript projects

1,672 lines (1,453 loc) 62.1 kB
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable no-async-promise-executor */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unused-vars */ import { ChildProcess, spawn } from "node:child_process"; import ansiColors from "ansi-colors"; import net from "net"; import { Page } from "puppeteer-core/lib/esm/puppeteer"; import fs, { watch } from "fs"; import path from "path"; import puppeteer, { ConsoleMessage } from "puppeteer-core"; import ansiC from "ansi-colors"; import crypto from "node:crypto"; import { WebSocketServer } from "ws"; import http from "http"; import url from "url"; import mime from "mime-types"; import { IFinalResults, IRunnables, ITTestResourceConfiguration, } from "../lib/index.js"; import { getRunnables } from "../utils"; import { IBuiltConfig, IRunTime, ITestTypes } from "../Types.js"; import { Sidecar } from "../lib/Sidecar.js"; import { Queue } from "../utils/queue.js"; import { PM_WithEslintAndTsc } from "./PM_WithEslintAndTsc.js"; type IOutputs = Record< string, { entryPoint: string; inputs: Record<string, string>; } >; const changes: Record<string, string> = {}; const fileHashes = {}; const files: Record<string, Set<string>> = {}; const screenshots: Record<string, Promise<Uint8Array>[]> = {}; type LogStreams = { closeAll: () => void; writeExitCode: (code: number, error?: Error) => void; stdout?: fs.WriteStream; stderr?: fs.WriteStream; info?: fs.WriteStream; warn?: fs.WriteStream; error?: fs.WriteStream; debug?: fs.WriteStream; exit: fs.WriteStream; }; function runtimeLogs( runtime: IRunTime, reportDest: string ): Record<string, fs.WriteStream> { const safeDest = reportDest || `testeranto/reports/default_${Date.now()}`; try { if (!fs.existsSync(safeDest)) { fs.mkdirSync(safeDest, { recursive: true }); } if (runtime === "node") { return { stdout: fs.createWriteStream(`${safeDest}/stdout.log`), stderr: fs.createWriteStream(`${safeDest}/stderr.log`), exit: fs.createWriteStream(`${safeDest}/exit.log`), }; } else if (runtime === "web") { return { info: fs.createWriteStream(`${safeDest}/info.log`), warn: fs.createWriteStream(`${safeDest}/warn.log`), error: fs.createWriteStream(`${safeDest}/error.log`), debug: fs.createWriteStream(`${safeDest}/debug.log`), exit: fs.createWriteStream(`${safeDest}/exit.log`), }; } else if (runtime === "pure") { return { exit: fs.createWriteStream(`${safeDest}/exit.log`), }; } else { throw `unknown runtime: ${runtime}`; } } catch (e) { console.error(`Failed to create log streams in ${safeDest}:`, e); throw e; } } function createLogStreams(reportDest: string, runtime: IRunTime): LogStreams { // Create directory if it doesn't exist if (!fs.existsSync(reportDest)) { fs.mkdirSync(reportDest, { recursive: true }); } const streams = runtimeLogs(runtime, reportDest); // const streams = { // exit: fs.createWriteStream(`${reportDest}/exit.log`), const safeDest = reportDest || `testeranto/reports/default_${Date.now()}`; try { if (!fs.existsSync(safeDest)) { fs.mkdirSync(safeDest, { recursive: true }); } const streams = runtimeLogs(runtime, safeDest); // const streams = { // exit: fs.createWriteStream(`${safeDest}/exit.log`), // ...(runtime === "node" || runtime === "pure" // ? { // stdout: fs.createWriteStream(`${safeDest}/stdout.log`), // stderr: fs.createWriteStream(`${safeDest}/stderr.log`), // } // : { // info: fs.createWriteStream(`${safeDest}/info.log`), // warn: fs.createWriteStream(`${safeDest}/warn.log`), // error: fs.createWriteStream(`${safeDest}/error.log`), // debug: fs.createWriteStream(`${safeDest}/debug.log`), // }), // }; return { ...streams, closeAll: () => { Object.values(streams).forEach( (stream) => !stream.closed && stream.close() ); }, writeExitCode: (code: number, error?: Error) => { if (error) { streams.exit.write(`Error: ${error.message}\n`); if (error.stack) { streams.exit.write(`Stack Trace:\n${error.stack}\n`); } } streams.exit.write(`${code}\n`); }, exit: streams.exit, }; } catch (e) { console.error(`Failed to create log streams in ${safeDest}:`, e); throw e; } } async function fileHash(filePath, algorithm = "md5") { return new Promise<string>((resolve, reject) => { const hash = crypto.createHash(algorithm); const fileStream = fs.createReadStream(filePath); fileStream.on("data", (data) => { hash.update(data); }); fileStream.on("end", () => { const fileHash = hash.digest("hex"); resolve(fileHash); }); fileStream.on("error", (error) => { reject(`Error reading file: ${error.message}`); }); }); } const statusMessagePretty = ( failures: number, test: string, runtime: IRunTime ) => { if (failures === 0) { console.log(ansiC.green(ansiC.inverse(`${runtime} > ${test}`))); } else if (failures > 0) { console.log( ansiC.red( ansiC.inverse( `${runtime} > ${test} failed ${failures} times (exit code: ${failures})` ) ) ); } else { console.log( ansiC.red(ansiC.inverse(`${runtime} > ${test} crashed (exit code: -1)`)) ); } }; async function writeFileAndCreateDir(filePath, data) { const dirPath = path.dirname(filePath); try { await fs.promises.mkdir(dirPath, { recursive: true }); await fs.writeFileSync(filePath, data); } catch (error) { console.error(`Error writing file: ${error}`); } } const filesHash = async (files: string[], algorithm = "md5") => { return new Promise<string>((resolve, reject) => { resolve( files.reduce(async (mm: Promise<string>, f) => { return (await mm) + (await fileHash(f)); }, Promise.resolve("")) ); }); }; function isValidUrl(string) { try { new URL(string); return true; } catch (err) { return false; } } // Wait for file to exist, checks every 2 seconds by default async function pollForFile(path, timeout = 2000) { const intervalObj = setInterval(function () { const file = path; const fileExists = fs.existsSync(file); if (fileExists) { clearInterval(intervalObj); } }, timeout); } export class PM_Main extends PM_WithEslintAndTsc { ports: Record<number, string>; queue: string[]; logStreams: Record<string, ReturnType<typeof createLogStreams>> = {}; webMetafileWatcher: fs.FSWatcher; nodeMetafileWatcher: fs.FSWatcher; importMetafileWatcher: fs.FSWatcher; pureSidecars: Record<number, Sidecar>; nodeSidecars: Record<number, ChildProcess>; webSidecars: Record<number, Page>; sidecars: Record<number, any> = {}; launchers: Record<string, () => void>; wss: WebSocketServer; clients: Set<any> = new Set(); httpServer: http.Server; runningProcesses: Map<string, ChildProcess> = new Map(); allProcesses: Map< string, { child?: ChildProcess; status: "running" | "exited" | "error"; exitCode?: number; error?: string; command: string; pid?: number; timestamp: string; } > = new Map(); processLogs: Map<string, string[]> = new Map(); constructor(configs: IBuiltConfig, name: string, mode: "once" | "dev") { super(configs, name, mode); this.launchers = {}; this.ports = {}; this.queue = []; this.nodeSidecars = {}; this.webSidecars = {}; this.pureSidecars = {}; this.configs.ports.forEach((element) => { this.ports[element] = ""; // set ports as open }); // Create HTTP server this.httpServer = http.createServer(this.requestHandler.bind(this)); // Start WebSocket server attached to the HTTP server this.wss = new WebSocketServer({ server: this.httpServer }); this.wss.on("connection", (ws) => { this.clients.add(ws); console.log("Client connected"); ws.on("message", (data) => { try { const message = JSON.parse(data.toString()); if (message.type === "executeCommand") { // Validate the command starts with 'aider' if (message.command && message.command.trim().startsWith("aider")) { console.log(`Executing command: ${message.command}`); // Execute the command const processId = Date.now().toString(); const child = spawn(message.command, { shell: true, cwd: process.cwd(), }); // Track the process in both maps this.runningProcesses.set(processId, child); this.allProcesses.set(processId, { child, status: "running", command: message.command, pid: child.pid, timestamp: new Date().toISOString(), }); // Initialize logs for this process this.processLogs.set(processId, []); // Broadcast process started this.broadcast({ type: "processStarted", processId, command: message.command, timestamp: new Date().toISOString(), logs: [], }); // Capture stdout and stderr child.stdout?.on("data", (data) => { const logData = data.toString(); // Add to stored logs const logs = this.processLogs.get(processId) || []; logs.push(logData); this.processLogs.set(processId, logs); this.broadcast({ type: "processStdout", processId, data: logData, timestamp: new Date().toISOString(), }); }); child.stderr?.on("data", (data) => { const logData = data.toString(); // Add to stored logs const logs = this.processLogs.get(processId) || []; logs.push(logData); this.processLogs.set(processId, logs); this.broadcast({ type: "processStderr", processId, data: logData, timestamp: new Date().toISOString(), }); }); child.on("error", (error) => { console.error(`Failed to execute command: ${error}`); this.runningProcesses.delete(processId); // Update the process status to error const processInfo = this.allProcesses.get(processId); if (processInfo) { this.allProcesses.set(processId, { ...processInfo, status: "error", error: error.message, }); } this.broadcast({ type: "processError", processId, error: error.message, timestamp: new Date().toISOString(), }); }); child.on("exit", (code) => { console.log(`Command exited with code ${code}`); // Remove from running processes but keep in allProcesses this.runningProcesses.delete(processId); // Update the process status to exited const processInfo = this.allProcesses.get(processId); if (processInfo) { this.allProcesses.set(processId, { ...processInfo, status: "exited", exitCode: code, }); } this.broadcast({ type: "processExited", processId, exitCode: code, timestamp: new Date().toISOString(), }); }); } else { console.error('Invalid command: must start with "aider"'); } } else if (message.type === "getRunningProcesses") { // Send list of all processes (both running and completed) with their full logs const processes = Array.from(this.allProcesses.entries()).map( ([id, procInfo]) => ({ processId: id, command: procInfo.command, pid: procInfo.pid, status: procInfo.status, exitCode: procInfo.exitCode, error: procInfo.error, timestamp: procInfo.timestamp, logs: this.processLogs.get(id) || [], }) ); ws.send( JSON.stringify({ type: "runningProcesses", processes, }) ); } else if (message.type === "getProcess") { // Send specific process with full logs const processId = message.processId; const procInfo = this.allProcesses.get(processId); if (procInfo) { ws.send( JSON.stringify({ type: "processData", processId, command: procInfo.command, pid: procInfo.pid, status: procInfo.status, exitCode: procInfo.exitCode, error: procInfo.error, timestamp: procInfo.timestamp, logs: this.processLogs.get(processId) || [], }) ); } } else if (message.type === "stdin") { // Handle stdin input for a process const processId = message.processId; const data = message.data; console.log("Received stdin for process", processId, ":", data); const childProcess = this.runningProcesses.get(processId); if (childProcess && childProcess.stdin) { console.log("Writing to process stdin"); childProcess.stdin.write(data); } else { console.log( "Cannot write to stdin - process not found or no stdin:", { processExists: !!childProcess, stdinExists: childProcess?.stdin ? true : false, } ); } } else if (message.type === "killProcess") { // Handle killing a process const processId = message.processId; console.log("Received killProcess for process", processId); const childProcess = this.runningProcesses.get(processId); if (childProcess) { console.log("Killing process"); childProcess.kill("SIGTERM"); // The process exit handler will update the status and broadcast the change } else { console.log("Cannot kill process - process not found:", { processExists: !!childProcess, }); } } } catch (error) { console.error("Error handling WebSocket message:", error); } }); ws.on("close", () => { this.clients.delete(ws); console.log("Client disconnected"); }); ws.on("error", (error) => { console.error("WebSocket error:", error); this.clients.delete(ws); }); }); // Start HTTP server const httpPort = Number(process.env.HTTP_PORT) || 3000; this.httpServer.listen(httpPort, () => { console.log(`HTTP server running on http://localhost:${httpPort}`); }); } async stopSideCar(uid: number): Promise<any> { console.log(ansiC.green(ansiC.inverse(`stopSideCar ${uid}`))); Object.entries(this.pureSidecars).forEach(async ([k, v]) => { if (Number(k) === uid) { await this.pureSidecars[Number(k)].stop(); delete this.pureSidecars[Number(k)]; } }); Object.entries(this.nodeSidecars).forEach(async ([k, v]) => { if (Number(k) === uid) { await this.nodeSidecars[Number(k)].send("stop"); delete this.nodeSidecars[Number(k)]; } }); Object.entries(this.webSidecars).forEach(async ([k, v]) => { if (Number(k) === uid) { (await this.browser.pages()).forEach(async (p) => { if (p.mainFrame()._id === k) { await this.webSidecars[Number(k)].close(); delete this.webSidecars[Number(k)]; } }); } }); return; } async launchSideCar( n: number, name: string ): Promise<[number, ITTestResourceConfiguration]> { const c = this.configs.tests.find(([v, r]) => { return v === name; }) as ITestTypes; const s = c[3][n]; const r = s[1]; if (r === "node") { return this.launchNodeSideCar(s); } else if (r === "web") { return this.launchWebSideCar(s); } else if (r === "pure") { return this.launchPureSideCar(s); } else { throw `unknown runtime ${r}`; } } mapping(): [string, (...a) => any][] { return [ ["$", this.$], ["click", this.click], ["closePage", this.closePage], ["createWriteStream", this.createWriteStream], ["customclose", this.customclose], ["customScreenShot", this.customScreenShot.bind(this)], ["end", this.end], ["existsSync", this.existsSync], ["focusOn", this.focusOn], ["getAttribute", this.getAttribute], ["getInnerHtml", this.getInnerHtml], // ["setValue", this.setValue], ["goto", this.goto.bind(this)], ["isDisabled", this.isDisabled], ["launchSideCar", this.launchSideCar.bind(this)], ["mkdirSync", this.mkdirSync], ["newPage", this.newPage], ["page", this.page], ["pages", this.pages], ["screencast", this.screencast], ["screencastStop", this.screencastStop], ["stopSideCar", this.stopSideCar.bind(this)], ["typeInto", this.typeInto], ["waitForSelector", this.waitForSelector], ["write", this.write], ["writeFileSync", this.writeFileSync], ]; } async start() { // set up the "pure" listeners this.mapping().forEach(async ([command, func]) => { globalThis[command] = func; }); if (!fs.existsSync(`testeranto/reports/${this.name}`)) { fs.mkdirSync(`testeranto/reports/${this.name}`); } const executablePath = "/opt/homebrew/bin/chromium"; try { this.browser = await puppeteer.launch({ slowMo: 1, waitForInitialPage: false, executablePath, headless: true, defaultViewport: null, // Disable default 800x600 viewport dumpio: false, devtools: false, args: [ "--allow-file-access-from-files", "--allow-insecure-localhost", "--allow-running-insecure-content", "--auto-open-devtools-for-tabs", "--disable-dev-shm-usage", "--disable-extensions", "--disable-features=site-per-process", "--disable-gpu", "--disable-setuid-sandbox", "--disable-site-isolation-trials", "--disable-web-security", "--no-first-run", "--no-sandbox", "--no-startup-window", "--reduce-security-for-testing", "--remote-allow-origins=*", "--start-maximized", "--unsafely-treat-insecure-origin-as-secure=*", `--remote-debugging-port=3234`, // "--disable-features=IsolateOrigins,site-per-process", // "--disable-features=IsolateOrigins", // "--disk-cache-dir=/dev/null", // "--disk-cache-size=1", // "--no-zygote", // "--remote-allow-origins=ws://localhost:3234", // "--single-process", // "--start-maximized", // "--unsafely-treat-insecure-origin-as-secure", // "--unsafely-treat-insecure-origin-as-secure=ws://192.168.0.101:3234", ], }); } catch (e) { console.error(e); console.error( "could not start chrome via puppeter. Check this path: ", executablePath ); } const { nodeEntryPoints, webEntryPoints, pureEntryPoints } = this.getRunnables(this.configs.tests, this.name); [ [ nodeEntryPoints, this.launchNode, "node", (w) => { this.nodeMetafileWatcher = w; }, ], [ webEntryPoints, this.launchWeb, "web", (w) => { this.webMetafileWatcher = w; }, ], [ pureEntryPoints, this.launchPure, "pure", (w) => { this.importMetafileWatcher = w; }, ], ].forEach( async ([eps, launcher, runtime, watcher]: [ Record<string, string>, (src: string, dest: string) => Promise<void>, IRunTime, (f: fs.FSWatcher) => void ]) => { const metafile = `./testeranto/metafiles/${runtime}/${this.name}.json`; await pollForFile(metafile); Object.entries(eps).forEach( async ([inputFile, outputFile]: [string, string]) => { // await pollForFile(outputFile);\ this.launchers[inputFile] = () => launcher(inputFile, outputFile); this.launchers[inputFile](); try { watch(outputFile, async (e, filename) => { const hash = await fileHash(outputFile); if (fileHashes[inputFile] !== hash) { fileHashes[inputFile] = hash; console.log( ansiC.yellow(ansiC.inverse(`< ${e} ${filename}`)) ); // launcher(inputFile, outputFile); this.launchers[inputFile](); } }); } catch (e) { console.error(e); } } ); this.metafileOutputs(runtime); watcher( watch(metafile, async (e, filename) => { console.log( ansiC.yellow(ansiC.inverse(`< ${e} ${filename} (${runtime})`)) ); this.metafileOutputs(runtime); }) ); } ); // Object.keys(this.configs.externalTests).forEach((et) => { // this.launchExternalTest(et, this.configs.externalTests[et]); // }); } // async launchExternalTest( // externalTestName: string, // externalTest: { // watch: string[]; // exec: string; // } // ) { // // fs.mkdirSync(`testeranto/externalTests/${externalTestName}`); // // exec(externalTest.exec, (error, stdout, stderr) => { // // if (error) { // // fs.writeFileSync( // // `testeranto/externalTests/${externalTestName}/exitcode.txt`, // // `${error.name}\n${error.message}\n${error.code}\n` // // ); // // } else { // // fs.writeFileSync( // // `testeranto/externalTests/${externalTestName}/exitcode.txt`, // // `0` // // ); // // } // // fs.writeFileSync( // // `testeranto/externalTests/${externalTestName}/stdout.txt`, // // stdout // // ); // // fs.writeFileSync( // // `testeranto/externalTests/${externalTestName}/stderr.txt`, // // stderr // // ); // // }); // } async stop() { console.log(ansiC.inverse("Testeranto-Run is shutting down gracefully...")); this.mode = "once"; this.nodeMetafileWatcher.close(); this.webMetafileWatcher.close(); this.importMetafileWatcher.close(); // Close any remaining log streams Object.values(this.logStreams || {}).forEach((logs) => logs.closeAll()); // Close WebSocket server this.wss.close(() => { console.log("WebSocket server closed"); }); // Close all client connections this.clients.forEach((client) => { client.terminate(); }); this.clients.clear(); // Close HTTP server this.httpServer.close(() => { console.log("HTTP server closed"); }); this.checkForShutdown(); } getRunnables = ( tests: ITestTypes[], testName: string, payload = { nodeEntryPoints: {}, nodeEntryPointSidecars: {}, webEntryPoints: {}, webEntryPointSidecars: {}, pureEntryPoints: {}, pureEntryPointSidecars: {}, } ): IRunnables => { return getRunnables(tests, testName, payload); }; async metafileOutputs(platform: IRunTime) { const metafile = JSON.parse( fs .readFileSync(`./testeranto/metafiles/${platform}/${this.name}.json`) .toString() ).metafile; if (!metafile) return; const outputs: IOutputs = metafile.outputs; Object.keys(outputs).forEach(async (k) => { const pattern = `testeranto/bundles/${platform}/${this.name}/${this.configs.src}`; if (!k.startsWith(pattern)) { return false; } const addableFiles = Object.keys(outputs[k].inputs).filter((i) => { if (!fs.existsSync(i)) return false; if (i.startsWith("node_modules")) return false; if (i.startsWith("./node_modules")) return false; return true; }); const f = `${k.split(".").slice(0, -1).join(".")}/`; if (!fs.existsSync(f)) { fs.mkdirSync(f); } const entrypoint = outputs[k].entryPoint; if (entrypoint) { const changeDigest = await filesHash(addableFiles); if (changeDigest === changes[entrypoint]) { // skip } else { changes[entrypoint] = changeDigest; this.tscCheck({ platform, addableFiles, entrypoint: entrypoint, }); this.eslintCheck(entrypoint, platform, addableFiles); this.makePrompt(entrypoint, addableFiles, platform); } } }); } launchPure = async (src: string, dest: string) => { console.log(ansiC.green(ansiC.inverse(`pure < ${src}`))); this.bddTestIsRunning(src); const reportDest = `testeranto/reports/${this.name}/${src .split(".") .slice(0, -1) .join(".")}/pure`; if (!fs.existsSync(reportDest)) { fs.mkdirSync(reportDest, { recursive: true }); } const destFolder = dest.replace(".mjs", ""); let argz = ""; const testConfig = this.configs.tests.find((t) => { return t[0] === src; }); if (!testConfig) { console.log(ansiC.inverse("missing test config! Exiting ungracefully!")); process.exit(-1); } const testConfigResource = testConfig[2]; const portsToUse: string[] = []; if (testConfigResource.ports === 0) { argz = JSON.stringify({ scheduled: true, name: src, ports: portsToUse, fs: reportDest, browserWSEndpoint: this.browser.wsEndpoint(), }); } else if (testConfigResource.ports > 0) { const openPorts = Object.entries(this.ports).filter( ([portnumber, status]) => status === "" ); if (openPorts.length >= testConfigResource.ports) { for (let i = 0; i < testConfigResource.ports; i++) { portsToUse.push(openPorts[i][0]); this.ports[openPorts[i][0]] = src; // port is now claimed } argz = JSON.stringify({ scheduled: true, name: src, ports: portsToUse, fs: destFolder, browserWSEndpoint: this.browser.wsEndpoint(), }); } else { this.queue.push(src); return [Math.random(), argz]; } } else { console.error("negative port makes no sense", src); process.exit(-1); } const builtfile = dest; // const webSideCares: Page[] = []; // fs.writeFileSync( // `${reportDest}/stdlog.txt`, // "THIS FILE IS AUTO GENERATED. IT IS PURPOSEFULLY LEFT BLANK." // ); // await Promise.all( // testConfig[3].map(async (sidecar) => { // if (sidecar[1] === "web") { // const s = await this.launchWebSideCar( // sidecar[0], // destinationOfRuntime(sidecar[0], "web", this.configs), // sidecar // ); // webSideCares.push(s); // return s; // } // if (sidecar[1] === "node") { // return this.launchNodeSideCar( // sidecar[0], // destinationOfRuntime(sidecar[0], "node", this.configs), // sidecar // ); // } // }) // ); const logs = createLogStreams(reportDest, "pure"); try { await import(`${builtfile}?cacheBust=${Date.now()}`).then((module) => { // Override console methods to redirect logs // Only override stdout/stderr methods for pure runtime const originalConsole = { ...console }; // console.log = (...args) => { // logs.stdout.write(args.join(" ") + "\n"); // originalConsole.log(...args); // }; // console.error = (...args) => { // logs.stderr.write(args.join(" ") + "\n"); // originalConsole.error(...args); // }; return module.default .then((defaultModule) => { defaultModule .receiveTestResourceConfig(argz) .then(async (results: IFinalResults) => { // this.receiveFeatures(results.features, destFolder, src, "pure"); // this.receiveFeaturesV2(reportDest, src, "pure"); statusMessagePretty(results.fails, src, "pure"); this.bddTestIsNowDone(src, results.fails); }) .catch((e1) => { console.log( ansiC.red(`launchPure - ${src} errored with: ${e1.stack}`) ); this.bddTestIsNowDone(src, -1); statusMessagePretty(-1, src, "pure"); }); // .finally(() => { // // webSideCares.forEach((webSideCar) => webSideCar.close()); // }); }) .catch((e2) => { console.log( ansiColors.red( `pure ! ${src} failed to execute. No "tests.json" file was generated. Check the logs for more info` ) ); logs.exit.write(e2.stack); logs.exit.write(-1); this.bddTestIsNowDone(src, -1); statusMessagePretty(-1, src, "pure"); // console.error(e); }) .finally((x) => { // const fileSet = files[src] || new Set(); // fs.writeFileSync( // reportDest + "/manifest.json", // JSON.stringify(Array.from(fileSet)) // ); }); }); } catch (e3) { logs.writeExitCode(-1, e3); console.log( ansiC.red( ansiC.inverse( `${src} 1 errored with: ${e3}. Check logs for more info` ) ) ); logs.exit.write(e3.stack); logs.exit.write(-1); this.bddTestIsNowDone(src, -1); statusMessagePretty(-1, src, "pure"); } for (let i = 0; i <= portsToUse.length; i++) { if (portsToUse[i]) { this.ports[portsToUse[i]] = ""; //port is open again } } }; launchNode = async (src: string, dest: string) => { console.log(ansiC.green(ansiC.inverse(`node < ${src}`))); this.bddTestIsRunning(src); const reportDest = `testeranto/reports/${this.name}/${src .split(".") .slice(0, -1) .join(".")}/node`; if (!fs.existsSync(reportDest)) { fs.mkdirSync(reportDest, { recursive: true }); } // const destFolder = dest.replace(".mjs", ""); let testResources = ""; const testConfig = this.configs.tests.find((t) => { return t[0] === src; }); if (!testConfig) { console.log( ansiC.inverse(`missing test config! Exiting ungracefully for '${src}'`) ); process.exit(-1); } const testConfigResource = testConfig[2]; const portsToUse: string[] = []; if (testConfigResource.ports === 0) { const t: ITTestResourceConfiguration = { name: src, // ports: portsToUse.map((v) => Number(v)), ports: [], fs: reportDest, browserWSEndpoint: this.browser.wsEndpoint(), }; testResources = JSON.stringify(t); } else if (testConfigResource.ports > 0) { const openPorts: [string, string][] = Object.entries(this.ports).filter( ([portnumber, portopen]) => portopen === "" ); if (openPorts.length >= testConfigResource.ports) { for (let i = 0; i < testConfigResource.ports; i++) { portsToUse.push(openPorts[i][0]); // Convert string port to number this.ports[openPorts[i][0]] = src; // port is now claimed } testResources = JSON.stringify({ scheduled: true, name: src, ports: portsToUse, fs: reportDest, browserWSEndpoint: this.browser.wsEndpoint(), }); } else { console.log( ansiC.red( `node: cannot run ${src} because there are no open ports ATM. This job will be enqueued and run again run a port is available` ) ); this.queue.push(src); return [Math.random(), argz]; // Add this return } } else { console.error("negative port makes no sense", src); process.exit(-1); } const builtfile = dest; let haltReturns = false; const ipcfile = "/tmp/tpipe_" + Math.random(); const child = spawn( "node", // "node", [ // "--inspect-brk", builtfile, testResources, ipcfile, ], { stdio: ["pipe", "pipe", "pipe", "ipc"], } ); let buffer: Buffer<ArrayBufferLike> = new Buffer(""); const server = net.createServer((socket) => { const queue = new Queue<string[]>(); socket.on("data", (data) => { buffer = Buffer.concat([buffer, data]); for (let b = 0; b < buffer.length + 1; b++) { const c = buffer.slice(0, b); let d; try { d = JSON.parse(c.toString()); queue.enqueue(d); buffer = buffer.slice(b, buffer.length + 1); b = 0; } catch (e) { // b++; } } while (queue.size() > 0) { const message = queue.dequeue(); if (message) { // set up the "node" listeners this.mapping().forEach(async ([command, func]) => { if (message[0] === command) { const x = message.slice(1, -1); const r = await this[command](...x); if (!haltReturns) { child.send( JSON.stringify({ payload: r, key: message[message.length - 1], }) ); } } }); } } }); }); const logs = createLogStreams(reportDest, "node"); server.listen(ipcfile, () => { // Only handle stdout/stderr for node runtime child.stdout?.on("data", (data) => { logs.stdout?.write(data); // Add null check }); child.stderr?.on("data", (data) => { logs.stderr?.write(data); // Add null check }); child.on("error", (err) => {}); child.on("close", (code) => { const exitCode = code === null ? -1 : code; if (exitCode < 0) { logs.writeExitCode( exitCode, new Error("Process crashed or was terminated") ); } else { logs.writeExitCode(exitCode); } logs.closeAll(); server.close(); if (!files[src]) { files[src] = new Set(); } if (exitCode === 255) { console.log( ansiColors.red( `node ! ${src} failed to execute. No "tests.json" file was generated. Check ${reportDest}/stderr.log for more info` ) ); this.bddTestIsNowDone(src, -1); statusMessagePretty(-1, src, "node"); return; } else if (exitCode === 0) { this.bddTestIsNowDone(src, 0); statusMessagePretty(0, src, "node"); } else { this.bddTestIsNowDone(src, exitCode); statusMessagePretty(exitCode, src, "node"); } haltReturns = true; }); child.on("exit", (code) => { haltReturns = true; for (let i = 0; i <= portsToUse.length; i++) { if (portsToUse[i]) { this.ports[portsToUse[i]] = ""; //port is open again } } }); child.on("error", (e) => { console.log("error"); haltReturns = true; console.log( ansiC.red( ansiC.inverse( `${src} errored with: ${e.name}. Check error logs for more info` ) ) ); this.bddTestIsNowDone(src, -1); statusMessagePretty(-1, src, "node"); }); }); }; launchWebSideCar = async ( testConfig: ITestTypes ): Promise<[number, Page]> => { const src = testConfig[0]; const dest = src.split(".").slice(0, -1).join("."); // const d = dest + ".mjs"; const destFolder = dest.replace(".mjs", ""); console.log(ansiC.green(ansiC.inverse(`launchWebSideCar ${src}`))); // const fileStreams2: fs.WriteStream[] = []; // const doneFileStream2: Promise<any>[] = []; const logs = createLogStreams(dest, "web"); return new Promise((res, rej) => { this.browser .newPage() .then(async (page) => { this.mapping().forEach(async ([command, func]) => { page.exposeFunction(command, func); }); const close = () => { if (!files[src]) { files[src] = new Set(); } // files[src].add(filepath); // fs.writeFileSync( // destFolder + "/manifest.json", // JSON.stringify(Array.from(files[src])) // ); delete files[src]; Promise.all(screenshots[src] || []).then(() => { delete screenshots[src]; page.close(); }); }; page.on("pageerror", (err: Error) => { console.debug(`Error from ${src}: [${err.name}] `); console.debug(`Error from ${src}: [${err.name}] `); if (err.cause) { console.debug(`Error from ${src} cause: [${err.cause}] `); } if (err.stack) { console.debug(`Error from stack ${src}: [${err.stack}] `); } console.debug(`Error from message ${src}: [${err.message}] `); this.bddTestIsNowDone(src, -1); close(); }); page.on("console", (log: ConsoleMessage) => { const msg = `${log.text()}\n${JSON.stringify( log.location() )}\n${JSON.stringify(log.stackTrace())}\n`; switch (log.type()) { case "info": logs.info?.write(msg); break; case "warn": logs.warn?.write(msg); break; case "error": logs.error?.write(msg); break; case "debug": logs.debug?.write(msg); break; default: break; } }); await page.goto(`file://${`${destFolder}.html`}`, {}); const webArgz = JSON.stringify({ name: dest, ports: [].toString(), fs: dest, browserWSEndpoint: this.browser.wsEndpoint(), }); const d = `${dest}?cacheBust=${Date.now()}`; const evaluation = ` import('${d}').then(async (x) => { try { return await (await x.default).receiveTestResourceConfig(${webArgz}) } catch (e) { console.log("fail", e.toString()) } })`; await page .evaluate(evaluation) .then(async ({ fails, failed, features }: IFinalResults) => { // this.receiveFeatures(features, destFolder, src, "web"); // this.receiveFeaturesV2(reportDest, src, "web"); statusMessagePretty(fails, src, "web"); this.bddTestIsNowDone(src, fails); }) .catch((e) => { console.log( ansiC.red( ansiC.inverse(`launchWebSidecar - ${src} errored with: ${e}`) ) ); }) .finally(() => { this.bddTestIsNowDone(src, -1); close(); }); return page; // return page; }) .then(async (page) => { await page.goto(`file://${`${dest}.html`}`, {}); res([Math.random(), page]); }); }); }; launchNodeSideCar = async ( sidecar: ITestTypes ): Promise<[number, ITTestResourceConfiguration]> => { const src = sidecar[0]; const dest = process.cwd() + `/testeranto/bundles/node/${this.name}/${sidecar[0]}`; const d = dest + ".mjs"; console.log(ansiC.green(ansiC.inverse(`launchNodeSideCar ${sidecar[0]}`))); const destFolder = dest.replace(".ts", ""); const reportDest = `testeranto/reports/${this.name}/${src .split(".") .slice(0, -1) .join(".")}/node`; const argz: ITTestResourceConfiguration = { name: sidecar[0], ports: [], fs: destFolder, browserWSEndpoint: this.browser.wsEndpoint(), }; const testReq: { ports: number } = sidecar[2]; const logs = createLogStreams(dest, "node"); const portsToUse: number[] = []; if (testReq.ports === 0) { // argz = { // name: sidecar[0], // ports: portsToUse, // fs: destFolder, // browserWSEndpoint: this.browser.wsEndpoint(), // }; } else if (testReq.ports > 0) { const openPorts = Object.entries(this.ports).filter( ([portnumber, portopen]) => portopen === "" ); if (openPorts.length >= testReq.ports) { for (let i = 0; i < testReq.ports; i++) { portsToUse.push(Number(openPorts[i][0])); // Convert string port to number this.ports[openPorts[i][0]] = src; // port is now closed } argz.ports = portsToUse; const builtfile = destFolder + ".mjs"; let haltReturns = false; let buffer: Buffer<ArrayBufferLike> = new Buffer(""); const server = net.createServer((socket) => { socket.on("data", (data) => { buffer = Buffer.concat([buffer, data]); const messages: string[][] = []; for (let b = 0; b < buffer.length + 1; b++) { const c = buffer.slice(0, b); let d; try { d = JSON.parse(c.toString()); messages.push(d); buffer = buffer.slice(b, buffer.length + 1); b = 0; } catch (e) { // b++; } } messages.forEach(async (payload) => { this.mapping().forEach(async ([command, func]) => { if (payload[0] === command) { const x = payload.slice(1, -1); const r = await this[command](...x); if (!haltReturns) { child.send( JSON.stringify({ payload: r, key: payload[payload.length - 1], }) ); } } }); }); }); }); const child = spawn("node", [builtfile, JSON.stringify(argz)], { stdio: ["pipe", "pipe", "pipe", "ipc"], // silent: true }); const p = "/tmp/tpipe" + Math.random(); server.listen(p, () => { child.on("close", (code) => { server.close(); haltReturns = true; }); child.on("exit", (code) => { haltReturns = true; for (let i = 0; i <= portsToUse.length; i++) { if (portsToUse[i]) { this.ports[portsToUse[i]] = ""; //port is open again } } }); child.on("error", (e) => { if (fs.existsSync(p)) { fs.rmSync(p); } haltReturns = true; console.log( ansiC.red( ansiC.inverse( `launchNodeSideCar - ${src} errored with: ${e.name}. Check logs for more info` ) ) ); logs.error?.write(e.toString() + "\n"); // this.bddTestIsNowDone(src, -1); // statusMessagePretty(-1, src); }); }); child.send({ path: p }); const r = Math.random(); this.nodeSidecars[r] = child; return [r, argz]; } else { console.log( ansiC.red( `cannot ${src} because there are no open ports. the job will be unqueued` ) ); this.queue.push(sidecar[0]); return [Math.random(), argz]; } } else { console.error("negative port makes no sense", sidecar[0]); process.exit(-1); } }; stopPureSideCar = async (uid: number) => { console.log(ansiC.green(ansiC.inverse(`stopPureSideCar ${uid}`))); await this.sidecars[uid].shutdown(); return; }; launchPureSideCar = async ( sidecar: ITestTypes ): Promise<[number, ITTestResourceConfiguration]> => { console.log(ansiC.green(ansiC.inverse(`launchPureSideCar ${sidecar[0]}`))); const r = Math.random(); const dest = process.cwd() + `/testeranto/bundles/pure/${this.name}/${sidecar[0]}`; const builtfile = dest.split(".").slice(0, -1).concat("mjs").join("."); const destFolder = dest.replace(".mjs", ""); let argz: ITTestResourceConfiguration; const z = sidecar[2]; const testConfigResource: { ports: number } = sidecar[2]; const src = sidecar[0]; const portsToUse: number[] = []; if (testConfigResource.ports === 0) { argz = { // scheduled: true, name: src, ports: portsToUse, fs: destFolder, browserWSEndpoint: this.browser.wsEndpoint(), }; } else if (testConfigResource.ports > 0) { const openPorts = Object.entries(this.ports).filter( ([portnumber, portopen]) => portopen === "" ); if (openPorts.length >= testConfigResource.ports) { for (let i = 0; i < testConfigResource.ports; i++) { portsToUse.push(Number(openPorts[i][0])); this.ports[openPorts[i][0]] = src; // port is now claimed } argz = { // scheduled: true, name: src, // ports: [3333], ports: portsToUse, fs: ".", browserWSEndpoint: this.browser.wsEndpoint(), }; } else { this.queue.push(src); // return; } } else { console.error("negative port makes no sense", src); process.exit(-1); } // const builtfile = dest + ".mjs"; await import(`${builtfile}?cacheBust=${Date.now()}`).then((module) => { if (!this.pureSidecars) this.pureSidecars = {}; this.pureSidecars[r] = module.default; this.pureSidecars[r].start(argz); }); return [r, argz]; // for (let i = 0; i <= portsToUse.length; i++) { // if (portsToUse[i]) { // this.ports[portsToUse[i]] = "true"; //port is open again // } // } }; launchWeb = async (src: string, dest: string) => { console.log(ansiC.green(ansiC.inverse(`web < ${src}`))); this.bddTestIsRunning(src); const reportDest = `testeranto/reports/${this.name}/${src .split(".") .slice(0, -1) .join(".")}/web`; if (!fs.existsSync(reportDest)) { fs.mkdirSync(reportDest, { recursive: true }); } const destFolder = dest.replace(".mjs", ""); const webArgz = JSON.stringify({ name: src, ports: [].toString(), fs: reportDest, browserWSEndpoint: this.browser.wsEndpoint(), }); const d = `${dest}?cacheBust=${Date.now()}`; const logs = createLogStreams(reportDest, "web"); this.browser .newPage() .then((page) => { page.on("console", (log: ConsoleMessage) => { const msg = `${log.text()}\n`; switch (log.type()) { case "info": logs.info?.write(msg); break; case "warn": logs.warn?.write(msg); break; case "error": logs.error?.write(msg); break; case "debug": logs.debug?.write(msg); break; default: break; } }); page.on("close", () => { logs.writeExitCode(0); // Web tests exit with 0 unless there's an error logs.closeAll(); logs.closeAll(); }); this.mapping().forEach(async ([command, func]) => { if (command === "page") { page.exposeFunction(command, (x?) => { if (x) { return func(x); } else {