UNPKG

testeranto

Version:

the AI powered BDD test framework for typescript projects

1,099 lines (1,097 loc) 73.6 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 { spawn } from "node:child_process"; import ansiColors from "ansi-colors"; import net from "net"; import fs, { watch } from "fs"; import path from "path"; import puppeteer 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 { getRunnables } from "../utils"; import { Queue } from "../utils/queue.js"; import { PM_WithEslintAndTsc } from "./PM_WithEslintAndTsc.js"; const changes = {}; const fileHashes = {}; const files = {}; const screenshots = {}; function runtimeLogs(runtime, reportDest) { 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, runtime) { // 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 Object.assign(Object.assign({}, streams), { closeAll: () => { Object.values(streams).forEach((stream) => !stream.closed && stream.close()); }, writeExitCode: (code, 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((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, test, runtime) => { 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, algorithm = "md5") => { return new Promise((resolve, reject) => { resolve(files.reduce(async (mm, 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 { constructor(configs, name, mode) { super(configs, name, mode); this.logStreams = {}; this.sidecars = {}; this.clients = new Set(); this.runningProcesses = new Map(); this.allProcesses = new Map(); this.processLogs = new Map(); this.getRunnables = (tests, testName, payload = { nodeEntryPoints: {}, nodeEntryPointSidecars: {}, webEntryPoints: {}, webEntryPointSidecars: {}, pureEntryPoints: {}, pureEntryPointSidecars: {}, }) => { return getRunnables(tests, testName, payload); }; this.launchPure = async (src, dest) => { 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 = []; 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 = Object.assign({}, 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) => { // 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 } } }; this.launchNode = async (src, dest) => { 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 = []; if (testConfigResource.ports === 0) { const t = { 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 = 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 = new Buffer(""); const server = net.createServer((socket) => { const queue = new Queue(); 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, () => { var _a, _b; // Only handle stdout/stderr for node runtime (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on("data", (data) => { var _a; (_a = logs.stdout) === null || _a === void 0 ? void 0 : _a.write(data); // Add null check }); (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on("data", (data) => { var _a; (_a = logs.stderr) === null || _a === void 0 ? void 0 : _a.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"); }); }); }; this.launchWebSideCar = async (testConfig) => { 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) => { 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) => { var _a, _b, _c, _d; const msg = `${log.text()}\n${JSON.stringify(log.location())}\n${JSON.stringify(log.stackTrace())}\n`; switch (log.type()) { case "info": (_a = logs.info) === null || _a === void 0 ? void 0 : _a.write(msg); break; case "warn": (_b = logs.warn) === null || _b === void 0 ? void 0 : _b.write(msg); break; case "error": (_c = logs.error) === null || _c === void 0 ? void 0 : _c.write(msg); break; case "debug": (_d = logs.debug) === null || _d === void 0 ? void 0 : _d.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 }) => { // 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]); }); }); }; this.launchNodeSideCar = async (sidecar) => { 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 = { name: sidecar[0], ports: [], fs: destFolder, browserWSEndpoint: this.browser.wsEndpoint(), }; const testReq = sidecar[2]; const logs = createLogStreams(dest, "node"); const portsToUse = []; 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 = new Buffer(""); const server = net.createServer((socket) => { socket.on("data", (data) => { buffer = Buffer.concat([buffer, data]); const messages = []; 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) => { var _a; 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`))); (_a = logs.error) === null || _a === void 0 ? void 0 : _a.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); } }; this.stopPureSideCar = async (uid) => { console.log(ansiC.green(ansiC.inverse(`stopPureSideCar ${uid}`))); await this.sidecars[uid].shutdown(); return; }; this.launchPureSideCar = async (sidecar) => { 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; const z = sidecar[2]; const testConfigResource = sidecar[2]; const src = sidecar[0]; const portsToUse = []; 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 // } // } }; this.launchWeb = async (src, dest) => { 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) => { var _a, _b, _c, _d; const msg = `${log.text()}\n`; switch (log.type()) { case "info": (_a = logs.info) === null || _a === void 0 ? void 0 : _a.write(msg); break; case "warn": (_b = logs.warn) === null || _b === void 0 ? void 0 : _b.write(msg); break; case "error": (_c = logs.error) === null || _c === void 0 ? void 0 : _c.write(msg); break; case "debug": (_d = logs.debug) === null || _d === void 0 ? void 0 : _d.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 { return func(page.mainFrame()._id); } }); } else { return page.exposeFunction(command, func); } }); return page; }) .then(async (page) => { const close = () => { if (!files[src]) { files[src] = new Set(); } // files[t].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(); }); return; }; page.on("pageerror", (err) => { logs.writeExitCode(-1, err); console.log(ansiColors.red(`web ! ${src} failed to execute No "tests.json" file was generated. Check ${reportDest}/error.log for more info`)); this.bddTestIsNowDone(src, -1); close(); }); // page.on("console", (log: ConsoleMessage) => {}); await page.goto(`file://${`${destFolder}.html`}`, {}); await page .evaluate(` import('${d}').then(async (x) => { try { return await (await x.default).receiveTestResourceConfig(${webArgz}) } catch (e) { console.log("web run failure", e.toString()) } }) `) .then(async ({ fails, failed, features }) => { statusMessagePretty(fails, src, "web"); this.bddTestIsNowDone(src, fails); // close(); }) .catch((e) => { console.log(ansiC.red(ansiC.inverse(e.stack))); console.log(ansiC.red(ansiC.inverse(`web ! ${src} failed to execute. No "tests.json" file was generated. Check logs for more info`))); this.bddTestIsNowDone(src, -1); }) .finally(() => { // process.exit(-1); close(); }); return page; }); }; this.receiveFeaturesV2 = (reportDest, srcTest, platform) => { const featureDestination = path.resolve(process.cwd(), "reports", "features", "strings", srcTest.split(".").slice(0, -1).join(".") + ".features.txt"); // Read and parse the test report const testReportPath = `${reportDest}/tests.json`; if (!fs.existsSync(testReportPath)) { console.error(`tests.json not found at: ${testReportPath}`); return; } const testReport = JSON.parse(fs.readFileSync(testReportPath, "utf8")); // Add full path information to each test if (testReport.tests) { testReport.tests.forEach((test) => { // Add the full path to each test test.fullPath = path.resolve(process.cwd(), srcTest); }); } // Add full path to the report itself testReport.fullPath = path.resolve(process.cwd(), srcTest); // Write the modified report back fs.writeFileSync(testReportPath, JSON.stringify(testReport, null, 2)); testReport.features .reduce(async (mm, featureStringKey) => { const accum = await mm; const isUrl = isValidUrl(featureStringKey); if (isUrl) { const u = new URL(featureStringKey); if (u.protocol === "file:") { const newPath = `${process.cwd()}/testeranto/features/internal/${path.relative(process.cwd(), u.pathname)}`; // await fs.promises.mkdir(path.dirname(newPath), { recursive: true }); // try { // await fs.unlinkSync(newPath); // // console.log(`Removed existing link at ${newPath}`); // } catch (error) { // if (error.code !== "ENOENT") { // // throw error; // } // } // fs.symlink(u.pathname, newPath, (err) => { // if (err) { // // console.error("Error creating symlink:", err); // } else { // // console.log("Symlink created successfully"); // } // }); accum.files.push(u.pathname); } else if (u.protocol === "http:" || u.protocol === "https:") { const newPath = `${process.cwd()}/testeranto/features/external/${u.hostname}${u.pathname}`; const body = await this.configs.featureIngestor(featureStringKey); writeFileAndCreateDir(newPath, body); accum.files.push(newPath); } } else { await fs.promises.mkdir(path.dirname(featureDestination), { recursive: true, }); accum.strings.push(featureStringKey); } return accum; }, Promise.resolve({ files: [], strings: [] })) .then(({ files, strings }) => { // Markdown files must be referenced in the prompt but string style features are already present in the tests.json file fs.writeFileSync(`testeranto/reports/${this.name}/${srcTest .split(".") .slice(0, -1) .join(".")}/${platform}/featurePrompt.txt`, files .map((f) => { return `/read ${f}`; }) .join("\n")); }); // const f: Record<string, string> = {}; testReport.givens.forEach((g) => { if (g.failed === true) { this.summary[srcTest].failingFeatures[g.key] = g.features; } }); // this.summary[srcTest].failingFeatures = f; this.writeBigBoard(); }; this.checkForShutdown = () => { // console.log(ansiC.inverse(JSON.stringify(this.summary, null, 2))); this.checkQueue(); console.log(ansiC.inverse(`The following jobs are awaiting resources: ${JSON.stringify(this.queue)}`)); console.log(ansiC.inverse(`The status of ports: ${JSON.stringify(this.ports)}`)); this.writeBigBoard(); if (this.mode === "dev") return; let inflight = false; Object.keys(this.summary).forEach((k) => { if (this.summary[k].prompt === "?") { console.log(ansiC.blue(ansiC.inverse(`🕕 prompt ${k}`))); inflight = true; } }); Object.keys(this.summary).forEach((k) => { if (this.summary[k].runTimeErrors === "?") { console.log(ansiC.blue(ansiC.inverse(`🕕 runTimeError ${k}`))); inflight = true; } }); Object.keys(this.summary).forEach((k) => { if (this.summary[k].staticErrors === "?") { console.log(ansiC.blue(ansiC.inverse(`🕕 staticErrors ${k}`))); inflight = true; } }); Object.keys(this.summary).forEach((k) => { if (this.summary[k].typeErrors === "?") { console.log(ansiC.blue(ansiC.inverse(`🕕 typeErrors ${k}`))); inflight = true; } }); this.writeBigBoard(); if (!inflight) { if (this.browser) { if (this.browser) { this.browser.disconnect().then(() => { console.log(ansiC.inverse(`${this.name} has been tested. Goodbye.`)); process.exit(); }); } } } }; 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) => { var _a, _b; 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 (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.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(), }); }); (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.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(),