testeranto
Version:
the AI powered BDD test framework for typescript projects
1,672 lines (1,453 loc) • 62.1 kB
text/typescript
/* 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 {