diff --git a/keployCli.ts b/keployCli.ts index 93235cb..a3c1e8d 100644 --- a/keployCli.ts +++ b/keployCli.ts @@ -1,381 +1,230 @@ import axios from 'axios'; -import { exec, spawn, ChildProcess } from 'child_process'; -import kill from 'tree-kill'; +import * as fs from 'fs'; +import { spawn } from 'child_process'; const GRAPHQL_ENDPOINT = '/query'; const HOST = 'http://localhost:'; -let serverPort = 6789; +let SERVER_PORT = 6789; + +const setHttpClient = async () => { + const url = `${HOST}${SERVER_PORT}${GRAPHQL_ENDPOINT}`; + return axios.create({ + baseURL: url, + timeout: 10000, + headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } + }); +} export enum TestRunStatus { RUNNING = 'RUNNING', PASSED = 'PASSED', - FAILED = 'FAILED' + FAILED = 'FAILED', + APP_HALTED = 'APP_HALTED', + USER_ABORT = 'USER_ABORT', + APP_FAULT = 'APP_FAULT', + INTERNAL_ERR = 'INTERNAL_ERR' } -interface TestOptions { - maxTimeout: number; - +interface RunOptions { + delay: number; + debug: boolean; + port: number; + path: string; } -let hasTestRunCompleted = false; - -export const setTestRunCompletionStatus = (status: boolean) => { - hasTestRunCompleted = status; +const DEFAULT_RUN_OPTIONS: RunOptions = { + delay: 5, + debug: false, + port: 6789, + path: '.' }; -let userCommandPID: any = 0; +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); -export const Test = async (appCmd: string, options: TestOptions, callback: (err: Error | null, result?: boolean) => void) => { - // set default values - if (appCmd == "") { - appCmd = "npm start" - } - if (options.maxTimeout === 0 || options.maxTimeout === undefined || options.maxTimeout === null) { - options.maxTimeout = 300000; - } +export const Test = async (appCmd: string, runOptions: RunOptions, callback: (err: Error | null, result?: boolean) => void) => { + const options = { ...DEFAULT_RUN_OPTIONS, ...runOptions }; + // Start Keploy + RunKeployServer(appCmd, options.delay, options.debug, SERVER_PORT); + await sleep(5000); let testResult = true; - let startTime = Date.now(); try { const testSets = await FetchTestSets(); - if (testSets === null) { - throw new Error('Test sets are null'); + if (!testSets) { + throw new Error('No test sets found. Are you in the right directory?'); } - console.log("TestSets: ", [...testSets]); - console.log("starting user application"); - for (let testset of testSets) { + for (const testSet of testSets) { let result = true; - StartUserApplication(appCmd) - const testRunId = await RunTestSet(testset); - let testRunStatus; + const { appId, testRunId } = await StartHooks(); + await RunTestSet(testRunId, testSet, appId); + await StartUserApplication(appId); + + const reportPath = `${options.path}/Keploy/reports/${testRunId}/${testSet}-report.yaml`; + + await CheckReportFile(reportPath, 5 + 10); + + let status: TestRunStatus | null = null; + + console.log(`Test set: ${testSet} is running`); + while (true) { - await new Promise(res => setTimeout(res, 2000)); - testRunStatus = await FetchTestSetStatus(testRunId); - // break the loop if the testRunStatus is not running or if it's been more than `maxTimeout` milliseconds - if (testRunStatus !== TestRunStatus.RUNNING) { + status = await FetchTestSetStatus(testRunId, testSet); + if (status === TestRunStatus.RUNNING) { + } else { break; } - if (Date.now() - startTime > options.maxTimeout) { - console.log("Timeout reached, exiting loop. maxTimeout: ", options.maxTimeout); - break; - } - console.log("testRun still in progress"); - // break; } - if (testRunStatus === TestRunStatus.FAILED || testRunStatus === TestRunStatus.RUNNING) { - console.log("testrun failed"); + if (status !== TestRunStatus.PASSED) { result = false; - } else if (testRunStatus === TestRunStatus.PASSED) { - console.log("testrun passed"); + console.error(`Test set: ${testSet} failed with status: ${status}`); + callback(new Error(`Test set: ${testSet} failed with status: ${status}`), false); + break; + } else { result = true; + console.log(`Test set: ${testSet} passed`); } - console.log(`TestResult of [${testset}]: ${result}`); testResult = testResult && result; - StopUserApplication() - await new Promise(res => setTimeout(res, 5000)); // wait for the application to stop + await StopUserApplication(appId); } - // stop the ebpf hooks - stopTest(); - callback(null, testResult); // Callback with no error and the test result } catch (error) { - callback(error as Error); // Callback with the error cast to an Error object + callback(error as Error, false); + } finally { + await StopKeployServer(); + await sleep(2000) + callback(null, testResult); } -} - -export const StartUserApplication = (userCmd: string) => { - const [cmd, ...args] = userCmd.split(' '); - const npmStartProcess = spawn(cmd, args, { - stdio: [process.stdin, 'pipe', process.stderr], - }); - userCommandPID = npmStartProcess.pid -} - -export const StopUserApplication = () => { - kill(userCommandPID) -} -let childProcesses: ChildProcess[] = []; -const processWrap = (command: string): Promise => { - return new Promise((resolve, reject) => { - let isPromiseSettled = false; - const [cmd, ...args] = command.split(' '); - - const childProcess = spawn(cmd, args, { shell: true }); - - const cleanup = () => { - if (!isPromiseSettled) { - isPromiseSettled = true; - childProcesses = childProcesses.filter(cp => cp !== childProcess); - if (!childProcess.killed) { - childProcess.kill(); - } - resolve(); // or reject based on your requirement - } - }; - - if (!isPromiseSettled) { - childProcess.stdout.on('data', (data) => { - console.log(`stdout: ${data}`); - }); - - childProcess.stderr.on('data', (data) => { - console.log(`stderr: ${data}`); - }); +}; - childProcess.on('error', (error) => { - console.error(`Failed to start process: ${error.message}`); - cleanup(); - }); - } - childProcess.on('exit', (code, signal) => { - if (code !== 0 && signal !== "SIGTERM") { - reject(new Error(`Process exited with code: ${code}, signal: ${signal}`)); - } else { - resolve(); - } - cleanup(); - }); - childProcess.on('close', () => { - cleanup(); - }); - - childProcesses.push(childProcess); +const StartUserApplication = async (appId: string): Promise => { + const client = await setHttpClient(); + const response = await client.post('', { + query: `mutation StartApp { startApp(appId: ${appId}) }` }); -}; -export const cleanupProcesses = () => { - childProcesses.forEach(cp => { - try { - if (!cp.killed) { - if (cp.stdout) { - cp.stdout.destroy(); - } - if (cp.stderr) { - cp.stderr.destroy(); - } - if (cp.stdin) { - cp.stdin.destroy(); - } - cp.kill(); - } - } catch (error) { - //console.error(`Failed to kill process: ${error}`); - } - }); - childProcesses.length = 0; // A way to clear the array without reassigning + if (!(response.status >= 200 && response.status < 300 && response.data.data.startApp)) { + throw new Error(`Failed to start user application. Status code: ${response.status}`); + } }; -process.on('exit', cleanupProcesses); - -export const RunKeployServer = (pid: number, delay: number, testPath: string, port: number) => { - return new Promise(async (resolve, reject) => { - let kprocess: ChildProcess | null = null; - const cleanup = () => { - process.off('exit', cleanup); - process.off('SIGINT', cleanup); - process.off('SIGTERM', cleanup); +const StartHooks = async (): Promise<{ appId: string, testRunId: string }> => { + const client = await setHttpClient(); + const response = await client.post('', { + query: `mutation StartHooks { startHooks { appId testRunId } }` + }); - if (kprocess) { - kprocess.kill(); - } - cleanupProcesses(); + if (response.status >= 200 && response.status < 300 && response.data.data.startHooks) { + return { + appId: response.data.data.startHooks.appId, + testRunId: response.data.data.startHooks.testRunId }; - process.on('exit', cleanup); - process.on('SIGINT', cleanup); - process.on('SIGTERM', cleanup); - const command = [ - 'sudo', - '-S', - 'keploybin', - 'serve', - `--pid=${pid}`, - `-p=${testPath}`, - `-d=${delay}`, - `--port=${port}`, - `--language="js"` - ]; - if (port !== 0) { - serverPort = port; - } - if (!hasTestRunCompleted) { - try { - await processWrap(command.join(' ')) - .then(() => { - if (hasTestRunCompleted) { - resolve(); - } - }) - .catch(error => { - reject(error); - }); - } catch (error) { - reject(error); - } - } else { - resolve(); - } - + } else { + throw new Error(`Failed to start hooks. Status code: ${response.status}`); + } +}; +const RunTestSet = async (testRunId: string, testSet: string, appId: string): Promise => { + const client = await setHttpClient(); + const response = await client.post('', { + query: `mutation RunTestSet { runTestSet(testSetId: "${testSet}", testRunId: "${testRunId}", appId: ${appId}) }` }); -} -export const setHttpClient = async () => { - try { - const url = `${HOST}${serverPort}${GRAPHQL_ENDPOINT}`; - return axios.create({ - baseURL: url, - timeout: 10000, - headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } - }); - } catch (error) { - throw error; // Re-throw the error after logging it + if (!(response.status >= 200 && response.status < 300 && response.data.data.runTestSet)) { + throw new Error(`Failed to run test set. Status code: ${response.status}`); } -} - -export const FetchTestSets = async (): Promise => { - try { - const client = await setHttpClient(); - if (!client) throw new Error("Could not initialize HTTP client."); - - const response = await client.post('', { - query: "{ testSets }" - }); +}; - if (response.status >= 200 && response.status < 300) { - return response.data.data.testSets; - } else { - //////console.error('Error: Unexpected response status', response.status); - return null; - } - } catch (error) { - if (error instanceof Error) { - console.error('Error fetching test sets', error.message, error.stack); - } else { - console.error('An unknown error occurred while fetching test sets', error); +const CheckReportFile = async (reportPath: string, timeout: number): Promise => { + const startTime = Date.now(); + while (Date.now() - startTime < timeout * 1000) { + if (fs.existsSync(reportPath)) { + return; } + await new Promise(res => setTimeout(res, 1000)); } - return null; + throw new Error(`Report file not created within ${timeout} seconds`); }; -const stopTest = async (): Promise => { - try { - const client = await setHttpClient(); - if (!client) throw new Error("Could not initialize HTTP client."); - const response = await client.post('', { - query: `{ stopTest }` - }); - if (response.status >= 200 && response.status < 300) { - if (response.data && response.data.data) { - return response.data.data.stopTest; - } else { - console.error('Unexpected response structure', response.data); - return false; - } - } - } catch (error) { - console.error('Error stopping the test', error); +const FetchTestSetStatus = async (testRunId: string, testSet: string): Promise => { + const client = await setHttpClient(); + const response = await client.post('', { + query: `query GetTestSetStatus { testSetStatus(testRunId: "${testRunId}", testSetId: "${testSet}") { status } }` + }); + + if (response.status >= 200 && response.status < 300 && response.data.data.testSetStatus) { + return response.data.data.testSetStatus.status as TestRunStatus; + } else { + throw new Error(`Failed to fetch test set status. Status code: ${response.status}`); } - return false; }; -export const FetchTestSetStatus = async (testRunId: string): Promise => { - try { - const client = await setHttpClient(); - if (!client) throw new Error("Could not initialize HTTP client."); - const response = await client.post('', { - query: `{ testSetStatus(testRunId: "${testRunId}") { status } }` - }); - if (response.status >= 200 && response.status < 300) { - if (response.data && response.data.data && response.data.data.testSetStatus) { - const testStatus = response.data.data.testSetStatus.status as keyof typeof TestRunStatus; - return TestRunStatus[testStatus]; - } else { - console.error('Unexpected response structure', response.data); - return null; - } - } - } catch (error) { - console.error('Error fetching test set status', error); +const StopUserApplication = async (appId: string): Promise => { + const client = await setHttpClient(); + const response = await client.post('', { + query: `mutation StopApp { stopApp(appId: ${appId}) }` + }); + + if (!(response.status >= 200 && response.status < 300 && response.data.data.stopApp)) { + throw new Error(`Failed to stop user application. Status code: ${response.status}`); } - return null; }; -export const RunTestSet = async (testSetName: string): Promise => { - try { - const client = await setHttpClient(); - if (!client) throw new Error("Could not initialize HTTP client."); +const StopKeployServer = async (): Promise => { + const client = await setHttpClient(); + const response = await client.post('', { + query: `mutation { stopHooks }` + }); - const response = await client.post('', { - query: `mutation { runTestSet(testSet: "${testSetName}") { success testRunId message } }` - }); - if (response.data && response.data.data && response.data.data.runTestSet) { - return response.data.data.runTestSet.testRunId; - } else { - console.error('Unexpected response format:', response.data); - } - } catch (error) { - console.error('Error running test set', error); + if (!(response.status >= 200 && response.status < 300 && response.data.data.stopHooks)) { + throw new Error(`Failed to stop Keploy server. Status code: ${response.status}`); } - return " "; }; -export const StopKeployServer = () => { - return killProcessOnPort(serverPort); -}; +const RunKeployServer = (appCmd: string, delay: number, debug: boolean, port: number): void => { -export const killProcessOnPort = async (port: number): Promise => { - return new Promise((resolve, reject) => { - //console.debug(`Trying to kill process running on port: ${port}`); - const command = `lsof -t -i:${port}`; + const command = `sudo -E env "PATH=$PATH" /usr/local/bin/keploy test -c "${appCmd}" --coverage --delay ${delay} --port ${port} ${debug ? '--debug' : ''}`; - exec(command, async (error, stdout, stderr) => { - if (error) { - console.error(`Error executing command: ${stderr}`, error); - return reject(error); - } + const keployProcess = spawn(command, { shell: true }); - const pids = stdout.split('\n').filter(pid => pid); - console.log(`PIDs found: ${pids}`); // Logging the PIDs found + // Log stdout + keployProcess.stdout.on('data', (data) => { + const log = data.toString().trim(); + console.log(log); + }); - if (pids.length === 0) { - console.error(`No process found running on port: ${port}`); - return resolve(); - } + // Log stderr + keployProcess.stderr.on('data', (data) => { + const log = data.toString().trim(); // Convert Buffer to string and trim whitespace + console.error(log); + }); - try { - const jestPid = process.pid.toString(); // Get the PID of the Jest process - const filteredPids = pids.filter(pid => pid !== jestPid); // Filter out the Jest PID from the list of PIDs + keployProcess.on('error', (error) => { + console.error(`Error starting Keploy server: ${error}`); + }); - for (let pid of filteredPids) { - try { - await forceKillProcessByPID(parseInt(pid.trim(), 10)); - } catch (error) { - console.error(`Failed to kill process ${pid}:`, error); - } - } - resolve(); - } catch (error) { - console.error(`Error killing processes:`, error); - reject(error); - } - }); + keployProcess.on('close', (code) => { + if (code !== 0) { + console.error(`Keploy server exited with code ${code}`); + } }); }; -export const forceKillProcessByPID = (pid: number): Promise => { - return new Promise((resolve, reject) => { - try { - if (process?.getuid) { - process.kill(pid, 'SIGKILL'); - resolve(); - } else { - //console.error(`Script is not run as root, cannot kill process with pid ${pid}`); - reject(new Error(`EPERM: Not running as root`)); - } - } catch (error) { - console.error(`Error killing process with pid ${pid}`, error); - reject(error); +const FetchTestSets = async (): Promise => { + try { + const client = await setHttpClient(); + const response = await client.post('', { + query: "{ testSets }" + }); + + if (response.status >= 200 && response.status < 300) { + return response.data.data.testSets; + } else { + console.error(`Error fetching test sets: Status code ${response.status}`); + return null; } - }); + } catch (error) { + console.error(`Error fetching test sets: ${error}`); + return null; + } }; diff --git a/package-lock.json b/package-lock.json index 985e791..1dae05e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,19 @@ "axios": "^1.6.7", "tree-kill": "^1.2.2", "typescript": "^5.3.3" + }, + "devDependencies": { + "@types/axios": "^0.14.0" + } + }, + "node_modules/@types/axios": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", + "integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==", + "deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!", + "dev": true, + "dependencies": { + "axios": "*" } }, "node_modules/@types/body-parser": { diff --git a/package.json b/package.json index 16f031e..3223485 100644 --- a/package.json +++ b/package.json @@ -25,5 +25,8 @@ "axios": "^1.6.7", "tree-kill": "^1.2.2", "typescript": "^5.3.3" + }, + "devDependencies": { + "@types/axios": "^0.14.0" } }