UNPKG

@redocly/respect-core

Version:
247 lines 9.71 kB
import { blue } from 'colorette'; import { basename, dirname, resolve } from 'node:path'; import { ApiFetcher } from '../../utils/api-fetcher.js'; import { createTestContext } from './context/create-test-context.js'; import { getValueFromContext } from '../context-parser/index.js'; import { getWorkflowsToRun } from './get-workflows-to-run.js'; import { runStep } from './run-step.js'; import { printWorkflowSeparator, printRequiredWorkflowSeparator, } from '../logger-output/helpers.js'; import { bundleArazzo } from './get-test-description-from-file.js'; import { CHECKS } from '../checks/index.js'; import { createRuntimeExpressionCtx, mergeWorkflowInputs } from './context/index.js'; import { evaluateRuntimeExpressionPayload } from '../runtime-expressions/index.js'; import { calculateTotals } from '../logger-output/index.js'; import { resolveRunningWorkflows } from './resolve-running-workflows.js'; export async function runTestFile({ options, collectSpecData, executedStepsCount, }) { const workflowOptions = { ...options, filePath: options.file, metadata: { ...options }, }; const bundledTestDescription = await bundleArazzo({ filePath: options.file, collectSpecData, version: options?.version, logger: options.logger, externalRefResolver: options?.externalRefResolver, skipLint: options?.skipLint, }); return await runWorkflows({ testDescription: bundledTestDescription, options: workflowOptions, executedStepsCount, }); } async function runWorkflows({ testDescription, options, executedStepsCount, }) { const apiClient = new ApiFetcher({ fetch: options.fetch, }); const ctx = await createTestContext(testDescription, options, apiClient); const workflowsToRun = resolveRunningWorkflows(options.workflow); const workflowsToSkip = resolveRunningWorkflows(options.skip); const workflows = getWorkflowsToRun({ workflows: ctx.workflows, workflowsToRun, workflowsToSkip, logger: ctx.options.logger, }); const executedWorkflows = []; for (const workflow of workflows) { ctx.executedSteps = []; // run dependencies workflows first if (workflow.dependsOn?.length) { await handleDependsOn({ workflow, ctx, config: options.config, executedStepsCount }); } const workflowExecutionResult = await runWorkflow({ workflowInput: workflow.workflowId, ctx, executedStepsCount, }); executedWorkflows.push(workflowExecutionResult); } return { ctx, executedWorkflows }; } export async function runWorkflow({ workflowInput, ctx, fromStepId, skipLineSeparator, parentStepId, invocationContext, executedStepsCount, retriesLeft, }) { const { logger } = ctx.options; const workflowStartTime = performance.now(); const fileBaseName = basename(ctx.options.filePath); const workflow = typeof workflowInput === 'string' ? ctx.workflows.find((w) => w.workflowId === workflowInput) : workflowInput; if (!workflow) { throw new Error(`\n ${blue('Workflow')} ${workflowInput} ${blue('not found')} \n`); } const workflowInputSchema = workflow.inputs; if (workflowInputSchema) { const inputs = ctx.$workflows[workflow.workflowId].inputs; ctx.$workflows[workflow.workflowId].inputs = mergeWorkflowInputs({ inputs, workflowInputSchema, env: ctx.options.envVariables, }); } const workflowId = workflow.workflowId; if (!fromStepId) { printWorkflowSeparator({ fileName: fileBaseName, workflowName: workflowId, skipLineSeparator, logger, }); } const fromStepIndex = fromStepId ? workflow.steps.findIndex((step) => step.stepId === fromStepId) : 0; const workflowSteps = workflow.steps.slice(fromStepIndex); // clean $steps ctx before running workflow steps ctx.$steps = {}; for (const step of workflowSteps) { try { const stepResult = await runStep({ step, ctx, workflowId, executedStepsCount, retriesLeft, }); // When `end` action is used, we should not continue with the next steps if (stepResult?.shouldEnd) { break; } } catch (err) { const failedCall = { name: CHECKS.UNEXPECTED_ERROR, message: err.message, passed: false, severity: ctx.severity['UNEXPECTED_ERROR'], }; step.checks.push(failedCall); ctx.executedSteps.push(step); } } const hasFailedTimeoutSteps = workflow.steps.some((step) => step.checks?.some((check) => !check.passed && check.name == CHECKS.GLOBAL_TIMEOUT_ERROR)); // workflow level outputs if (workflow.outputs && workflowId && !hasFailedTimeoutSteps) { if (!ctx.$outputs) { ctx.$outputs = {}; } if (!ctx.$outputs[workflowId]) { ctx.$outputs[workflowId] = {}; } const runtimeExpressionContext = createRuntimeExpressionCtx({ ctx: { ...ctx, $inputs: { ...(ctx.$inputs || {}), ...(ctx.$workflows[workflowId]?.inputs || {}), }, }, workflowId, }); const outputs = {}; for (const outputKey of Object.keys(workflow.outputs)) { try { outputs[outputKey] = evaluateRuntimeExpressionPayload({ payload: workflow.outputs[outputKey], context: runtimeExpressionContext, logger: ctx.options.logger, }); } catch (error) { throw new Error(`Failed to resolve output "${outputKey}" in workflow "${workflowId}": ${error.message}`); } } ctx.$outputs[workflowId] = outputs; ctx.$workflows[workflowId].outputs = outputs; } workflow.time = Math.ceil(performance.now() - workflowStartTime); logger.printNewLine(); const endTime = performance.now(); return { type: 'workflow', invocationContext, workflowId, stepId: parentStepId, startTime: workflowStartTime, endTime, totalTimeMs: calculateWorkflowTotalTimeMs(ctx.executedSteps), executedSteps: ctx.executedSteps, ctx, globalTimeoutError: hasFailedTimeoutSteps, }; } async function handleDependsOn({ workflow, ctx, config, executedStepsCount, }) { if (!workflow.dependsOn?.length) return; const dependenciesWorkflows = await Promise.all(workflow.dependsOn.map(async (workflowId) => { const resolvedWorkflow = getValueFromContext({ value: workflowId, ctx, logger: ctx.options.logger, }); const workflowCtx = await resolveWorkflowContext(workflowId, resolvedWorkflow, ctx, config); printRequiredWorkflowSeparator(workflow.workflowId, ctx.options.logger); return runWorkflow({ workflowInput: resolvedWorkflow, ctx: workflowCtx, skipLineSeparator: true, executedStepsCount, }); })); const totals = calculateTotals(dependenciesWorkflows); const hasProblems = totals.steps.failed > 0; if (hasProblems) { throw new Error('Dependent workflows has failed steps'); } } export async function resolveWorkflowContext(workflowId, resolvedWorkflow, ctx, config) { const sourceDescriptionId = workflowId && workflowId.startsWith('$sourceDescriptions.') && workflowId.split('.')[1]; const testDescription = sourceDescriptionId && ctx.$sourceDescriptions[sourceDescriptionId]; // executing external workflow should not mutate the original context // only outputs are transferred to the parent workflow // creating the new ctx for the external workflow or recreate current ctx for local workflow return testDescription ? await createTestContext(testDescription, { ...ctx.options, filePath: findSourceDescriptionUrl(sourceDescriptionId, ctx.sourceDescriptions, ctx.options), workflow: [resolvedWorkflow.workflowId], skip: undefined, config, }, ctx.apiClient) : { ...ctx, executedSteps: [], }; } function findSourceDescriptionUrl(sourceDescriptionId, sourceDescriptions, options) { const sourceDescription = sourceDescriptions && sourceDescriptions.find(({ name }) => name === sourceDescriptionId); if (!sourceDescription) { return ''; } else if (sourceDescription.type === 'openapi') { return sourceDescription.url; } else if (sourceDescription.type === 'arazzo') { return resolve(dirname(options.filePath), sourceDescription.url); } else { throw new Error(`Unknown source description type ${sourceDescription.type}`); } } function isWorkflowExecutionResult(step) { return 'type' in step && step.type === 'workflow'; } function calculateWorkflowTotalTimeMs(executedSteps) { let totalTime = 0; for (const step of executedSteps) { if (isWorkflowExecutionResult(step)) { totalTime += step.totalTimeMs; } else { totalTime += step.response?.time || 0; } } return totalTime; } //# sourceMappingURL=runner.js.map