@ply-ct/ply
Version:
REST API Automated Testing
369 lines • 17.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.FlowLoader = exports.FlowSuite = void 0;
const flow_1 = require("./flow");
const log_1 = require("./log");
const logger_1 = require("./logger");
const retrieval_1 = require("./retrieval");
const runtime_1 = require("./runtime");
const result_1 = require("./result");
const suite_1 = require("./suite");
const step_1 = require("./step");
const skip_1 = require("./skip");
const util = __importStar(require("./util"));
const yaml = __importStar(require("./yaml"));
const ply_1 = require("./ply");
/**
* Suite representing a ply flow.
*/
class FlowSuite extends suite_1.Suite {
/**
* @param plyFlow PlyFlow
* @param path relative path from tests location (forward slashes)
* @param runtime info
* @param logger
* @param start zero-based start line
* @param end zero-based end line
*/
constructor(plyFlow, path, runtime, logger, start = 0, end) {
super(plyFlow.name, 'flow', path, runtime, logger, start, end);
this.plyFlow = plyFlow;
this.path = path;
this.runtime = runtime;
this.logger = logger;
this.start = start;
this.end = end;
}
/**
* Override to execute flow itself if all steps are specified
* @param steps
*/
async runTests(steps, values, runOptions, runNum) {
if (runOptions && Object.keys(runOptions).length > 0) {
this.log.debug('RunOptions', runOptions);
}
if (runOptions === null || runOptions === void 0 ? void 0 : runOptions.requireTsNode) {
require('ts-node/register');
}
this.emitSuiteStarted();
// runtime values are a deep copy of passed values
const runValues = JSON.parse(JSON.stringify(values));
this.runtime.responseMassagers = undefined;
let results;
if (this.isFlowSpec(steps)) {
results = [await this.runFlow(runValues, runOptions, runNum)];
}
else {
results = await this.runSteps(steps, runValues, runOptions);
}
this.emitSuiteFinished();
return results;
}
getStep(stepId) {
const step = this.all().find((step) => step.step.id === stepId);
if (!step)
throw new Error(`Step not found: ${stepId}`);
return step;
}
async runFlow(values, runOptions, runNum) {
var _a, _b;
if ((_a = this.runtime.options) === null || _a === void 0 ? void 0 : _a.parallel) {
this.plyFlow = this.plyFlow.clone();
}
this.plyFlow.onFlow((flowEvent) => {
var _a, _b, _c;
if (flowEvent.eventType === 'exec') {
// emit test event (not for request -- emitted in requestSuite)
const stepInstance = flowEvent.instance;
const step = this.getStep(stepInstance.stepId);
if (step.step.path !== 'request') {
this.emitTest(step);
}
}
else {
// exec not applicable for ply subscribers
(_a = this.emitter) === null || _a === void 0 ? void 0 : _a.emit('flow', flowEvent);
if (flowEvent.elementType === 'step' &&
(flowEvent.eventType === 'finish' || flowEvent.eventType === 'error')) {
const stepInstance = flowEvent.instance;
const step = this.getStep(stepInstance.stepId);
if (step.step.path !== 'request') {
if (flowEvent.eventType === 'error') {
(_b = this.emitter) === null || _b === void 0 ? void 0 : _b.emit('outcome', {
plyee: new ply_1.Plyee(this.runtime.options.testsLocation + '/' + this.path, step).path,
outcome: { status: 'Errored', message: stepInstance.message || '' }
});
}
else if (flowEvent.eventType === 'finish') {
(_c = this.emitter) === null || _c === void 0 ? void 0 : _c.emit('outcome', {
plyee: new ply_1.Plyee(this.runtime.options.testsLocation + '/' + this.path, step).path,
outcome: {
status: (runOptions === null || runOptions === void 0 ? void 0 : runOptions.submit) ? 'Submitted' : 'Passed',
message: ''
}
});
}
}
}
}
});
this.plyFlow.requestSuite.emitter = this.emitter;
const start = Date.now();
const res = await this.plyFlow.run(this.runtime, values, runOptions, runNum);
const flowResult = { flow: this.path, ...res, start, end: Date.now() };
if ((_b = this.plyFlow.flow.attributes) === null || _b === void 0 ? void 0 : _b.return) {
// don't evaluate result values if not used
const retVals = this.plyFlow.getReturnValues({ ...values, ...this.plyFlow.results.values }, runOptions === null || runOptions === void 0 ? void 0 : runOptions.trusted);
if (retVals) {
this.log.debug(`${this.name} return values`, retVals);
flowResult.return = retVals;
}
}
return flowResult;
}
/**
* Run steps independently
*/
async runSteps(steps, values, runOptions) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
// flow values supersede file-based
const flowValues = this.plyFlow.getFlowValues(values, runOptions);
for (const flowValKey of Object.keys(flowValues)) {
values[flowValKey] = flowValues[flowValKey];
}
const requestSuite = new suite_1.Suite(this.plyFlow.name, 'request', this.path, this.runtime, this.logger, 0, 0);
requestSuite.callingFlowPath = this.plyFlow.flow.path;
requestSuite.emitter = this.emitter;
this.runtime.results.actual.clear();
const expectedExists = await this.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: ${this.runtime.results.expected}`);
this.runtime.results.expected.write('');
runOptions.createExpected = true;
}
const results = [];
// emit start event for synthetic flow
(_a = this.emitter) === null || _a === void 0 ? void 0 : _a.emit('flow', this.plyFlow.flowEvent('start', 'flow', this.plyFlow.instance));
this.plyFlow.instance.stepInstances = [];
for (const step of steps) {
this.emitTest(step);
let subflow;
const dot = step.name.indexOf('.');
if (dot > 0) {
const subflowId = step.name.substring(0, dot);
subflow = (_b = this.plyFlow.flow.subflows) === null || _b === void 0 ? void 0 : _b.find((sub) => sub.id === subflowId);
}
const insts = (_c = this.plyFlow.instance.stepInstances) === null || _c === void 0 ? void 0 : _c.filter((stepInst) => {
return stepInst.stepId === step.step.id;
}).length;
const plyStep = new step_1.PlyStep(step.step, requestSuite, this.logger, this.plyFlow.flow, this.plyFlow.instance, subflow);
if (insts) {
let maxLoops = this.runtime.options.maxLoops || 10;
if ((_d = this.plyFlow.flow.attributes) === null || _d === void 0 ? void 0 : _d.maxLoops) {
maxLoops = parseInt(this.plyFlow.flow.attributes.maxLoops);
}
if ((_e = step.step.attributes) === null || _e === void 0 ? void 0 : _e.maxLoops) {
maxLoops = parseInt(step.step.attributes.maxLoops);
}
if (isNaN(maxLoops)) {
this.plyFlow.stepError(plyStep, `Invalid maxLoops: ${maxLoops}`);
}
else if (insts + 1 > maxLoops) {
this.plyFlow.stepError(plyStep, `Max loops (${maxLoops} reached for step: ${step.step.id}`);
}
}
(_f = this.emitter) === null || _f === void 0 ? void 0 : _f.emit('flow', this.plyFlow.flowEvent('start', 'step', plyStep.instance));
const result = await plyStep.run(this.runtime, values, runOptions, 0, insts);
if (result.status === 'Failed' || result.status === 'Errored') {
(_g = this.emitter) === null || _g === void 0 ? void 0 : _g.emit('flow', this.plyFlow.flowEvent('error', 'step', plyStep.instance));
}
else {
(_h = this.emitter) === null || _h === void 0 ? void 0 : _h.emit('flow', this.plyFlow.flowEvent('finish', 'step', plyStep.instance));
}
if (step.step.path !== 'request') {
super.logOutcome(step, {
status: result.status,
message: result.message,
start: (_j = plyStep.instance.start) === null || _j === void 0 ? void 0 : _j.getTime(),
diffs: result.diffs
}, 0, 'Step');
}
results.push(result);
if (plyStep.instance)
this.plyFlow.instance.stepInstances.push(plyStep.instance);
}
// stop event for synthetic flow
const evt = results.reduce((overall, res) => res.status === 'Failed' || res.status === 'Errored' ? 'error' : overall, 'finish');
(_k = this.emitter) === null || _k === void 0 ? void 0 : _k.emit('flow', this.plyFlow.flowEvent(evt, 'flow', this.plyFlow.instance));
return results;
}
/**
* True if steps array is identical to flow steps.
*/
isFlowSpec(steps) {
if (steps.length !== this.size()) {
return false;
}
const flowStepNames = Object.keys(this.tests);
for (let i = 0; i < steps.length; i++) {
if (steps[i].name !== flowStepNames[i]) {
return false;
}
}
return true;
}
/**
* Returns all reachable unique steps in this.plyFlow.
* These are the tests in this FlowSuite.
*/
getSteps() {
var _a, _b, _c, _d, _e;
const steps = [];
const addSteps = (startStep, subflow) => {
var _a, _b;
const already = steps.find((step) => step.step.id === startStep.id);
if (!already) {
steps.push({ step: startStep, subflow });
if (startStep.links) {
for (const link of startStep.links) {
let outStep;
if (subflow) {
outStep = (_a = subflow.steps) === null || _a === void 0 ? void 0 : _a.find((s) => s.id === link.to);
}
else {
outStep = (_b = this.plyFlow.flow.steps) === null || _b === void 0 ? void 0 : _b.find((s) => s.id === link.to);
}
if (outStep) {
addSteps(outStep, subflow);
}
}
}
}
};
(_b = (_a = this.plyFlow.flow.subflows) === null || _a === void 0 ? void 0 : _a.filter((sub) => { var _a; return ((_a = sub.attributes) === null || _a === void 0 ? void 0 : _a.when) === 'Before'; })) === null || _b === void 0 ? void 0 : _b.forEach((before) => {
var _a;
(_a = before.steps) === null || _a === void 0 ? void 0 : _a.forEach((step) => addSteps(step, before));
});
(_c = this.plyFlow.flow.steps) === null || _c === void 0 ? void 0 : _c.forEach((step) => addSteps(step));
(_e = (_d = this.plyFlow.flow.subflows) === null || _d === void 0 ? void 0 : _d.filter((sub) => { var _a; return ((_a = sub.attributes) === null || _a === void 0 ? void 0 : _a.when) === 'After'; })) === null || _e === void 0 ? void 0 : _e.forEach((after) => {
var _a;
(_a = after.steps) === null || _a === void 0 ? void 0 : _a.forEach((step) => addSteps(step, after));
});
return steps.map((step) => {
return {
name: step.subflow ? `${step.subflow.id}-${step.step.id}` : step.step.id,
type: 'flow',
step: step.step,
...(step.subflow && { subflow: step.subflow })
};
});
}
}
exports.FlowSuite = FlowSuite;
class FlowLoader {
constructor(locations, options, logger) {
this.locations = locations;
this.options = options;
this.logger = logger;
if (options.skip) {
this.skip = new skip_1.Skip(options.skip);
}
}
async load() {
const retrievals = this.locations.map((loc) => new retrieval_1.Retrieval(loc));
// load flow files in parallel
const promises = retrievals.map((retr) => this.loadSuite(retr));
const suites = await Promise.all(promises);
suites.sort((s1, s2) => s1.name.localeCompare(s2.name));
return suites;
}
async loadSuite(retrieval) {
const contents = await retrieval.read();
if (typeof contents === 'undefined') {
throw new Error('Cannot retrieve: ' + retrieval.location.absolute);
}
const resultPaths = await result_1.ResultPaths.create(this.options, retrieval);
resultPaths.isFlowResult = true;
return this.buildSuite(retrieval, contents, resultPaths);
}
buildSuite(retrieval, contents, resultPaths) {
var _a;
const runtime = new runtime_1.Runtime(this.options, retrieval, resultPaths);
const logger = this.logger ||
new logger_1.Logger({
level: this.options.verbose
? log_1.LogLevel.debug
: this.options.quiet
? log_1.LogLevel.error
: log_1.LogLevel.info,
prettyIndent: this.options.prettyIndent
}, runtime.results.log);
// request suite comprising all requests configured in steps
const requestSuite = new suite_1.Suite(retrieval.location.base, 'request', retrieval.location.relativeTo(this.options.testsLocation), runtime, logger, 0, 0);
const flowbeeFlow = FlowLoader.parse(contents, retrieval.location.path);
requestSuite.callingFlowPath = flowbeeFlow.path;
const plyFlow = new flow_1.PlyFlow(flowbeeFlow, requestSuite, logger);
const suite = new FlowSuite(plyFlow, retrieval.location.relativeTo(this.options.testsLocation), runtime, logger, 0, util.lines(contents).length - 1);
for (const step of suite.getSteps()) {
suite.add(step);
}
// mark if skipped
if ((_a = this.skip) === null || _a === void 0 ? void 0 : _a.isSkipped(suite.path)) {
suite.skip = true;
}
return suite;
}
/**
* Parse a flowbee flow from text (reproduced from flowbee.FlowDiagram)
* @param text json or yaml
* @param file file name
*/
static parse(text, file) {
let flow;
if (text.startsWith('{')) {
try {
flow = JSON.parse(text);
}
catch (err) {
throw new Error(`Failed to parse ${file}: ${err}`);
}
}
else {
flow = yaml.load(file, text);
}
if (!flow)
throw new Error(`Unable to load from empty: ${file}`);
flow.type = 'flow';
flow.path = file.replace(/\\/g, '/');
return flow;
}
}
exports.FlowLoader = FlowLoader;
//# sourceMappingURL=flows.js.map