@ply-ct/ply
Version:
REST API Automated Testing
507 lines • 21.3 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PlyFlow = exports.FlowStepResults = void 0;
const minimatch_1 = require("minimatch");
const values_1 = require("./values");
const flowbee = __importStar(require("./flowbee"));
const event_1 = require("./event");
const log_1 = require("./log");
const step_1 = require("./step");
const names_1 = require("./names");
const util = __importStar(require("./util"));
const replace_1 = require("./replace");
const names_2 = require("./names");
class FlowStepResults {
constructor(flow) {
this.flow = flow;
this.results = [];
}
add(stepId, result) {
this.results.push({ ...result, stepId });
}
get latest() {
if (this.results.length > 0) {
return this.results[this.results.length - 1];
}
else {
return { name: this.flow, status: 'Pending', message: '' };
}
}
latestBad() {
return this.latest.status === 'Failed' || this.latest.status === 'Errored';
}
get overall() {
let anySubmitted = false;
for (const result of this.results) {
if (result.status === 'Failed' || result.status === 'Errored') {
return result;
}
else if (result.status === 'Submitted') {
anySubmitted = true;
}
}
return {
name: this.flow,
status: anySubmitted ? 'Submitted' : 'Passed',
message: ''
};
}
/**
* Step result values
*/
get values() {
return this.results.reduce((values, result) => {
if (result.data) {
let data = result.data;
if (typeof data === 'object') {
data = { ...data }; // clone array or object
}
let plyResults = values[names_2.RESULTS];
if (!plyResults) {
plyResults = {};
values[names_2.RESULTS] = plyResults;
}
let stepResult = plyResults[result.stepId];
if (!stepResult) {
stepResult = {};
plyResults[result.stepId] = stepResult;
}
if (typeof data === 'object' && !Array.isArray(data)) {
if (data.request) {
stepResult.request = data.request;
if (typeof stepResult.request.body === 'string' &&
util.isJson(stepResult.request.body)) {
stepResult.request.body = JSON.parse(stepResult.request.body);
}
}
if (data.response) {
stepResult.response = data.response;
if (typeof stepResult.response.body === 'string' &&
util.isJson(stepResult.response.body)) {
stepResult.response.body = JSON.parse(stepResult.response.body);
}
}
}
else {
stepResult.data = data;
}
}
return values;
}, {});
}
}
exports.FlowStepResults = FlowStepResults;
class PlyFlow {
constructor(flow, requestSuite, logger) {
this.flow = flow;
this.requestSuite = requestSuite;
this.logger = logger;
this.type = 'flow';
this.start = 0;
this.maxLoops = 0;
this._onFlow = new event_1.TypedEvent();
this.name = flowbee.getFlowName(flow);
this.instance = this.newInstance();
this.results = new FlowStepResults(this.name);
}
onFlow(listener) {
this._onFlow.on(listener);
}
clone() {
return new PlyFlow(this.flow, this.requestSuite, this.logger);
}
newInstance() {
const id = util.genId();
this.instance = {
id,
runId: id,
flowPath: this.flow.path,
status: 'Pending'
};
return this.instance;
}
/**
* Flow input values
*/
getFlowValues(values, runOptions) {
var _a;
const flowValues = {};
if ((_a = this.flow.attributes) === null || _a === void 0 ? void 0 : _a.values) {
const rows = JSON.parse(this.flow.attributes.values);
for (const row of rows) {
let rowVal = row[1];
if (rowVal === null || rowVal === void 0 ? void 0 : rowVal.trim().length) {
if ((0, values_1.isExpression)(rowVal)) {
rowVal = (0, replace_1.replaceLine)(rowVal, values, {
trusted: runOptions === null || runOptions === void 0 ? void 0 : runOptions.trusted,
logger: this.logger
});
}
const numVal = Number(row[1]);
if (!isNaN(numVal))
rowVal = numVal;
else if (row[1] === 'true' || row[1] === 'false')
rowVal = row[1] === 'true';
else if (util.isJson(row[1]))
rowVal = JSON.parse(row[1]);
flowValues[row[0]] = rowVal;
}
// run values override even flow-configured vals
if (runOptions === null || runOptions === void 0 ? void 0 : runOptions.values) {
const runValue = runOptions.values[row[0]] || runOptions.values[`\${${row[0]}}`];
if (runValue)
flowValues[row[0]] = runValue;
}
}
}
return flowValues;
}
/**
* returns missing value names
*/
validateValues(values) {
var _a;
const missingRequired = [];
if ((_a = this.flow.attributes) === null || _a === void 0 ? void 0 : _a.values) {
const rows = JSON.parse(this.flow.attributes.values);
for (const row of rows) {
if (row[2] === 'true' &&
(values[row[0]] === undefined || values[rows[0]] === null)) {
missingRequired.push(row[0]);
}
}
}
return missingRequired;
}
/**
* Flow output values
*/
getReturnValues(values, trusted = false) {
var _a;
if ((_a = this.flow.attributes) === null || _a === void 0 ? void 0 : _a.return) {
const rows = JSON.parse(this.flow.attributes.return);
const returnVals = {};
for (const row of rows) {
const expr = row[1];
const val = (0, replace_1.replaceLine)(expr, values, { trusted, logger: this.logger });
if (val.trim().length) {
returnVals[row[0]] = val;
}
}
return returnVals;
}
}
/**
* Run a ply flow.
*/
async run(runtime, values, runOptions, runNum) {
var _a, _b, _c;
this.newInstance();
this.results = new FlowStepResults(this.name);
values[names_1.RUN_ID] = this.instance.runId || util.genId();
// flow values supersede file-based
const flowValues = this.getFlowValues(values, runOptions);
for (const flowValKey of Object.keys(flowValues)) {
values[flowValKey] = flowValues[flowValKey];
}
this.maxLoops = runtime.options.maxLoops || 10;
if ((_a = this.flow.attributes) === null || _a === void 0 ? void 0 : _a.maxLoops) {
this.maxLoops = parseInt(this.flow.attributes.maxLoops);
}
if (((_b = this.flow.attributes) === null || _b === void 0 ? void 0 : _b.bail) === 'true') {
runtime.options.bail = true;
}
runtime.results.actual.write('');
const expectedExists = await this.requestSuite.runtime.results.expectedExists();
if (!expectedExists && (runOptions === null || runOptions === void 0 ? void 0 : runOptions.submitIfExpectedMissing)) {
runOptions.submit = true;
}
if ((runOptions === null || runOptions === void 0 ? void 0 : runOptions.createExpected) ||
(!expectedExists && (runOptions === null || runOptions === void 0 ? void 0 : runOptions.createExpectedIfMissing))) {
this.logger.info(`Creating expected result: ${runtime.results.expected}`);
runtime.results.expected.write('');
runOptions.createExpected = true;
}
const startStep = (_c = this.flow.steps) === null || _c === void 0 ? void 0 : _c.find((s) => s.path === 'start');
if (!startStep) {
throw new Error(`Cannot find start step in flow: ${this.flow.path}`);
}
const runId = this.logger.level === log_1.LogLevel.debug ? ` (${this.instance.runId})` : '';
this.logger.info(`Running flow '${this.name}'${runId}`);
this.instance.status = 'In Progress';
this.instance.values = values;
this.instance.start = new Date();
this.emit('start', 'flow', this.instance);
await this.runSubflows(this.getSubflows('Before'), runtime, values, runOptions, runNum);
if (this.results.latestBad() && runtime.options.bail) {
return this.endFlow();
}
await this.exec(startStep, runtime, values, undefined, runOptions, runNum);
if (this.results.latestBad() && runtime.options.bail) {
return this.endFlow();
}
await this.runSubflows(this.getSubflows('After'), runtime, values, runOptions, runNum);
return this.endFlow();
}
endFlow() {
this.instance.end = new Date();
if (this.instance.status === 'Errored' || this.instance.status === 'Failed') {
this.emit('error', 'flow', this.instance);
}
else {
this.instance.status = 'Completed';
this.emit('finish', 'flow', this.instance);
}
return this.results.overall;
}
/**
* Executes a step within a flow and recursively executes the following step(s).
*/
async exec(step, runtime, values, incomingLinkId, runOptions, runNum, subflow) {
var _a, _b, _c, _d, _e, _f, _g;
await this.runSubflows(this.getSubflows('Before', step), runtime, values, runOptions, runNum);
if (this.results.latestBad() && runtime.options.bail) {
return;
}
let insts = 0;
if (this.instance.stepInstances && !runtime.options.parallel) {
insts = (_a = this.instance.stepInstances.filter((si) => si.stepId === step.id)) === null || _a === void 0 ? void 0 : _a.length;
}
const plyStep = new step_1.PlyStep(step, this.requestSuite, this.logger, this.flow, this.instance, subflow === null || subflow === void 0 ? void 0 : subflow.subflow);
if (insts) {
let maxLoops = this.maxLoops;
if (step.path === 'start')
maxLoops = 1;
if ((_b = step.attributes) === null || _b === void 0 ? void 0 : _b.maxLoops) {
maxLoops = parseInt(step.attributes.maxLoops);
}
if (isNaN(maxLoops))
this.stepError(plyStep, `Invalid maxLoops: ${maxLoops}`);
else if (insts + 1 > maxLoops) {
this.stepError(plyStep, `Max loops (${maxLoops} reached for step: ${step.id}`);
}
}
if (subflow) {
if (!subflow.instance.stepInstances) {
subflow.instance.stepInstances = [];
}
subflow.instance.stepInstances.push(plyStep.instance);
}
else {
if (!this.instance.stepInstances) {
this.instance.stepInstances = [];
}
this.instance.stepInstances.push(plyStep.instance);
}
this.emit('start', 'step', plyStep.instance);
this.logger.info('Executing step', plyStep.name);
this.emit('exec', 'step', plyStep.instance);
let result;
if (runtime.options.validate) {
// perform any preflight validations
if (plyStep.step.path === 'start') {
const missing = this.validateValues(values);
if (missing.length) {
plyStep.instance.status = 'Errored';
result = {
name: this.name,
status: 'Errored',
message: `Missing required value(s): ${missing.join(', ')}`
};
}
}
}
if (!result) {
result = await plyStep.run(runtime, values, runOptions, runNum, insts);
}
result.start = (_c = plyStep.instance.start) === null || _c === void 0 ? void 0 : _c.getTime();
result.end = (_d = plyStep.instance.end) === null || _d === void 0 ? void 0 : _d.getTime();
result.data = plyStep.instance.data;
this.results.add(plyStep.name, result);
if (runtime.options.verbose) {
this.requestSuite.logOutcome(plyStep, result, runNum, plyStep.stepName, values);
}
else if ((plyStep.step.path === 'start' || plyStep.step.path === 'stop') && !subflow) {
// non-verbose only start/stop step values are logged
const { [names_2.RESULTS]: _results, [names_1.RUN_ID]: _runId, ...loggedValues } = values;
this.requestSuite.logOutcome(plyStep, result, runNum, plyStep.stepName, loggedValues);
}
else {
this.requestSuite.logOutcome(plyStep, result, runNum, plyStep.stepName);
}
if (result.status === 'Waiting') {
return;
}
if (this.results.latestBad()) {
this.instance.status = plyStep.instance.status;
if (subflow)
subflow.instance.status = plyStep.instance.status;
this.emit('error', 'step', plyStep.instance);
if (result.status === 'Errored' || runtime.options.bail) {
return;
}
}
else {
this.emit('finish', 'step', plyStep.instance);
}
await this.runSubflows(this.getSubflows('After', step), runtime, values, runOptions, runNum);
if (this.results.latestBad() && runtime.options.bail) {
return;
}
const outSteps = {};
if (step.links) {
for (const link of step.links) {
const result = (_e = plyStep.instance.result) === null || _e === void 0 ? void 0 : _e.trim();
if ((!result && !link.result) || result === link.result) {
let outStep;
if (subflow) {
outStep = (_f = subflow.subflow.steps) === null || _f === void 0 ? void 0 : _f.find((s) => s.id === link.to);
}
else {
outStep = (_g = this.flow.steps) === null || _g === void 0 ? void 0 : _g.find((s) => s.id === link.to);
}
if (outStep) {
outSteps[link.id] = outStep;
}
else {
this.stepError(plyStep, `No such step: ${link.to} (linked from ${link.id})`);
}
}
}
}
if (Object.keys(outSteps).length === 0 && step.path !== 'stop') {
this.stepError(plyStep, `No outbound link from step ${step.id} matches result: ${plyStep.instance.result}`);
}
// steps can execute in parallel
await Promise.all(Object.keys(outSteps).map((linkId) => this.exec(outSteps[linkId], runtime, values, linkId, runOptions, runNum, subflow)));
}
getSubflows(type, step) {
var _a;
const subflows = ((_a = this.flow.subflows) === null || _a === void 0 ? void 0 : _a.filter((sub) => {
var _a, _b;
if (((_a = sub.attributes) === null || _a === void 0 ? void 0 : _a.when) === type) {
const steps = (_b = sub.attributes) === null || _b === void 0 ? void 0 : _b.steps;
if (step) {
return steps ? (0, minimatch_1.minimatch)(step.name, steps) : false;
}
else {
return !steps;
}
}
})) || [];
const flowInstanceId = this.instance.id;
return subflows.map((subflow) => {
return {
subflow,
instance: {
id: Date.now().toString(16),
flowInstanceId,
subflowId: subflow.id,
runId: Date.now().toString(16),
flowPath: this.flow.path,
status: 'Pending'
}
};
});
}
async runSubflows(subflows, runtime, values, runOptions, runNum) {
var _a;
for (const subflow of subflows) {
const startStep = (_a = subflow.subflow.steps) === null || _a === void 0 ? void 0 : _a.find((s) => s.path === 'start');
if (!startStep) {
throw new Error(`Cannot find start step in subflow: ${subflow.subflow.id}`);
}
subflow.instance.status = 'In Progress';
subflow.instance.start = new Date();
if (!this.instance.subflowInstances) {
this.instance.subflowInstances = [];
}
this.instance.subflowInstances.push(subflow.instance);
this.emit('start', 'subflow', subflow.instance);
this.logger.info('Executing subflow', subflow.subflow.name);
const resOpts = {
level: 0,
withExpected: runOptions === null || runOptions === void 0 ? void 0 : runOptions.createExpected,
subflow: subflow.subflow.name
};
runtime.appendResult(`${subflow.subflow.name}:`, {
...resOpts,
level: 0,
comment: util.timestamp(subflow.instance.start)
});
runtime.appendResult(`id: ${subflow.subflow.id}`, {
...resOpts,
level: 1
});
await this.exec(startStep, runtime, values, undefined, runOptions, runNum, subflow);
subflow.instance.end = new Date();
const elapsed = subflow.instance.end.getTime() - subflow.instance.start.getTime();
if (this.results.latestBad()) {
subflow.instance.status =
this.results.latest.status === 'Errored' ? 'Errored' : 'Failed';
runtime.updateResult(subflow.subflow.name, `status: ${subflow.instance.status}`, {
...resOpts,
level: 1,
comment: `${elapsed} ms`
});
this.emit('error', 'subflow', subflow.instance);
if (runtime.options.bail) {
return;
}
}
else {
subflow.instance.status = 'Completed';
runtime.updateResult(subflow.subflow.name, `status: ${subflow.instance.status}`, {
...resOpts,
level: 1,
comment: `${elapsed} ms`
});
this.emit('finish', 'subflow', subflow.instance);
}
}
}
stepError(plyStep, message) {
plyStep.instance.status = 'Errored';
plyStep.instance.message = message;
this.emit('error', 'step', plyStep.instance);
this.emit('error', 'flow', this.instance);
throw new Error(message);
}
flowEvent(eventType, elementType, instance) {
return {
eventType,
elementType,
flowPath: this.flow.path,
flowInstanceId: this.instance.id,
instance
};
}
emit(eventType, elementType, instance) {
this._onFlow.emit(this.flowEvent(eventType, elementType, instance));
}
}
exports.PlyFlow = PlyFlow;
//# sourceMappingURL=flow.js.map