import * as p from '@clack/prompts'; import color from 'picocolors'; import { cloneDeep } from "lodash"; import { Execution, Flow, Scenario } from "../types/execution.types"; import YAML from 'yaml'; import { log, processedSchedule } from "./general"; import { Feature, InputSchedule, RunnningExecution } from "../types/types"; import { getFeatures } from "../api/crud"; import { asyncAction } from './asyncActions'; import { haltExecution, HaltExecutionError, runExecution } from '../api/execute'; import { handleGetAll } from '../eventHandlers/handlers'; import { cache } from '..'; export async function createExecutionObjectFromFile(fileContent: string): Promise { const parsed: Partial = YAML.parse(fileContent); if (typeof parsed !== 'object' || parsed === null) { throw new Error('Invalid YAML file: root is not an object.'); } if (!parsed.name || !parsed.env || !parsed.branch /* or other required fields */) { throw new Error('Missing required fields in execution YAML (name, env, branch, etc).'); } parsed.schedule = processedSchedule(parsed.schedule as any); if (parsed.schedule) { // @ts-ignore delete parsed.schedule.timezoneOffset; // @ts-ignore delete parsed.schedule.repeat; } parsed.projectId = process.env.projectId ?? ""; parsed.userId = process.env.user_id ?? ""; parsed.enabled = false; parsed.isSingleThreaded = false; parsed.flows = await processFlows(parsed.flows! as Flow[], parsed.env!); return parsed as Execution; } /** * 1. assign threads to each sg and scenarios[] to each sg * 2. create commands * 3. add indexes of features and scenarios * @param flows */ async function processFlows(flows: Flow[], env: string): Promise { const featuresResponse = await getFeatures(); const features: Feature[] = featuresResponse.features; for (const flow of flows) { for (const sg of flow.scenarioGroups) { const scenarios: Scenario[] = []; features.forEach((feature, fIdx) => { feature.scenarios.forEach((scenario, sIdx) => { if (sg.tags.some((tag) => scenario.tags.includes(tag))) { const command = getScenarioRunCommand(sIdx, fIdx, env, features, process.env.ACCESS_TOKEN!); scenarios.push({ featureIndex: fIdx, scenarioIndex: sIdx, command, }); } }); }); sg.scenarios = scenarios; } } return flows; } export const getScenarioRunCommand = ( scenarioIndex: number, featureIndex: number, env_name: string, features: Feature[], accessToken: string ) => { // const cmd = `npx cross-env NODE_ENV_BLINQ=stage BLINQ_ENV="environments/${env}.json" TOKEN=${process.env.TOKEN1} cucumber-js --format bvt --name "buy item" "features/shop.feature"`; const envs = { prod: { tag: "", additionalVars: "", }, stage: { tag: "@stage", additionalVars: " NODE_ENV_BLINQ=stage", }, dev: { tag: "@next", additionalVars: " NODE_ENV_BLINQ=dev", }, }; const NODE_ENV_BLINQ = process.env.NODE_ENV_BLINQ!; let additionalVars = envs[NODE_ENV_BLINQ as keyof typeof envs]?.additionalVars ?? ""; const env: string = NODE_ENV_BLINQ; // if its a customer env like amdocs: https://amdocs.api.blinq.io or something similar if (env.startsWith("http") && additionalVars === "") additionalVars = " NODE_ENV_BLINQ=" + env; const featureName = features[featureIndex]?.name; const scenarioName = features[featureIndex]?.scenarios[scenarioIndex]?.name; if (!featureName || !scenarioName) { console.error("Feature or Scenario name not found"); return 'echo "Failed to generate command"'; } // const cmd = `npx cross-env${additionalVars} BLINQ_ENV='environments/${env_name}.json' TOKEN=${accessToken} HEADLESS='true' RUN_ID='' cucumber-js --format bvt --name '${scenarioName}' 'features/${featureName}'`; const cmd = `npx cross-env${additionalVars} BLINQ_ENV='environments/${env_name}.json' TOKEN=${accessToken} HEADLESS='true' cucumber-js --format bvt --name '${scenarioName}' 'features/${featureName}'`; return cmd; }; export function allocateMaxThreads(execution: Execution, count: number): Execution { // Allocate max threads to each scenario group in the execution const updatedExecution = cloneDeep(execution); updatedExecution.flows.forEach((flow) => { flow.scenarioGroups.forEach((sg) => { sg.threadCount = Math.min(sg.scenarios.length, count); }); }); return updatedExecution; } export async function initiateRunByName( args: Record ) { const { executionName, maxThreads: threadLimit, uploadFailedVideos, retryCount } = args; const executions: Execution[] = await handleGetAll(false); const execution = executions.find((exec) => exec.name === executionName.trim()); if (!execution) { throw new Error(`Execution with name "${executionName}" not found.`); } await initiateRun({ executionId: execution._id, maxThreads: threadLimit, uploadFailedVideos, retryCount }); } interface SuccessfulInitiation { wasRunInitiated: true; message: string; instanceId: string; reportLink: string; } interface FailedInitiation { wasRunInitiated: false; message: string; error: string; } interface FailedNetworkRequest { error: string; } type InitiateRunResult = SuccessfulInitiation | FailedInitiation | FailedNetworkRequest; export async function initiateRun(args: Record) { const { executionId, uploadFailedVideos, retryCount, maxThreads: threadLimit } = args; const executions: Execution[] = await handleGetAll(false); const execution = executions.find((exec) => exec._id === executionId); if (!execution) { throw new Error(`Execution with ID ${executionId} not found.`); } const testDataOverwriteObject = cache.getInnerObject('testData'); try { await asyncAction(async () => { const data: InitiateRunResult = await runExecution( executionId!, process.env.ACCESS_TOKEN!, testDataOverwriteObject, { uploadFailedVideos, retryCount, threadLimit } ); if (!('error' in data)) { console.log('\n', JSON.stringify({ executionInstanceId: data.instanceId, reportURL: data.reportLink }, null, 2)); } else { throw new Error(data.error); } }, 10000, '...'); p.outro(color.green('Execution run successfully started' + (testDataOverwriteObject ? ' with test data overwrite: ' + JSON.stringify(testDataOverwriteObject) : '.'))); } catch (err) { p.outro(color.red('Failed to start execution run: ' + (err as Error).message)); } } export async function terminateRun(executionId: string) { try { await asyncAction(async () => { await haltExecution(executionId); }, 10000, 'Stopping execution...'); p.outro(color.green('✅ Execution run successfully stopped.')); } catch (err) { const error = err as Error; if (error instanceof HaltExecutionError) { const msg = error.message.toLowerCase(); if (msg.includes('already halted') || msg.includes('not currently running')) { p.outro(color.yellow('⚠️ Execution is already stopped or not running.')); return; } if (error.status === 404) { p.outro(color.red('❌ Execution not found.')); return; } } // fallback p.outro(color.red(`❌ Failed to stop execution run: ${error.message}`)); } }