UNPKG

11.2 kBPlain TextView Raw
1import * as pad from 'pad';
2import { stringify } from 'querystring';
3import { loadAllActions } from './actionLoading';
4import { generateSequenceDiagram, initDiagramCreation } from './diagramDrawing';
5import { getLogger } from './logging';
6import { Action } from './model/Action';
7import { ActionType } from './model/ActionType';
8import { Scenario } from './model/Scenario';
9import { TestResult } from './model/TestResult';
10import { loadAllScenarios, loadScenariosById } from './scenarioLoading';
11import { loadYamlConfiguration } from './yamlParsing';
12import { ActionCallback } from './model/ActionCallback';
13
14const RESULTS: Map<string, TestResult[]> = new Map();
15
16let OUT_DIR = '';
17
18// increase the pixel-size (width/height) limit of PlantUML (the default is 4096 which is not enough for some diagrams)
19process.env.PLANTUML_LIMIT_SIZE = '16384';
20
21interface RunConfiguration {
22 numberOfScenariosRunInParallel?: number;
23 environmentNameToBeUsed?: string;
24 drawDiagrams?: boolean;
25}
26
27/**
28 * @deprecated since 1.8.0, use {@link runMultipleScenariosWithConfig} instead
29 */
30export const runMultipleSceanriosWithConfig = (
31 actionDir: string,
32 outDir = 'out',
33 envConfigDir: string,
34 runConfig: RunConfiguration,
35 scenarioPaths: string[],
36): void =>
37 runMultipleScenariosWithConfig(
38 actionDir,
39 outDir,
40 envConfigDir,
41 runConfig,
42 scenarioPaths,
43 );
44
45export const runMultipleScenariosWithConfig = (
46 actionDir: string,
47 outDir = 'out',
48 envConfigDir: string,
49 runConfig: RunConfiguration,
50 scenarioPaths: string[],
51): void => {
52 runMultipleScenariosWithConfigAsync(
53 actionDir,
54 outDir,
55 envConfigDir,
56 runConfig,
57 scenarioPaths,
58 ).then(result => {
59 if (!result) process.exit(1);
60 });
61};
62
63export const runMultipleScenariosWithConfigAsync = async (
64 actionDir: string,
65 outDir = 'out',
66 envConfigDir: string,
67 runConfig: RunConfiguration,
68 scenarioPaths: string[],
69): Promise<boolean> => {
70 // TODO: global variables should be avoided
71 RESULTS.clear();
72 const {
73 numberOfScenariosRunInParallel = 10,
74 environmentNameToBeUsed = 'none',
75 drawDiagrams = true,
76 } = runConfig;
77
78 try {
79 if (
80 typeof scenarioPaths === 'undefined' ||
81 scenarioPaths.length === 0
82 ) {
83 getLogger().error(
84 'Please provide correct path(s) to the SCENARIO file!',
85 );
86 process.exit(1);
87 }
88 if (typeof actionDir === 'undefined' || actionDir === '') {
89 getLogger().error(
90 'Please provide correct path to the ACTION files!',
91 );
92 process.exit(1);
93 }
94
95 getLogger('setup').info(
96 `RUNNING: scenario(s): ${scenarioPaths} (actions: ${actionDir}, out: ${outDir}, envDir: ${envConfigDir}, numberOfScenariosRunInParallel: ${numberOfScenariosRunInParallel}, environmentNameToBeUsed: ${environmentNameToBeUsed})`,
97 );
98
99 OUT_DIR = outDir;
100
101 const envConfig = envConfigDir
102 ? loadYamlConfiguration(
103 `${envConfigDir}/${environmentNameToBeUsed}.yaml`,
104 )
105 : {};
106 getLogger('setup').debug(
107 `Using '${environmentNameToBeUsed}' configuration: ${stringify(
108 envConfig,
109 )}`,
110 );
111
112 const actions: Action[] = loadAllActions(actionDir, envConfig);
113 getLogger('setup').debug(
114 `Successfully loaded ${actions.length} actions`,
115 );
116
117 const resultPromises: Promise<boolean>[] = [];
118 scenarioPaths.forEach(scenarioPath => {
119 getLogger('setup').debug(`Loading: ${scenarioPath} ...`);
120 const scenarios: Scenario[] = scenarioPath.endsWith('yaml')
121 ? loadScenariosById(scenarioPath, actions)
122 : loadAllScenarios(scenarioPath, actions);
123 getLogger('setup').debug(
124 `Successfully loaded ${scenarios.length} scenario(s): ${scenarioPath}`,
125 );
126
127 resultPromises.push(
128 processScenarios(
129 scenarios,
130 numberOfScenariosRunInParallel,
131 drawDiagrams,
132 ),
133 );
134 });
135
136 const results = await Promise.all(resultPromises);
137 return results.every(result => result);
138 } catch (e) {
139 getLogger('setup').error(e);
140 return false;
141 }
142};
143
144/**
145 * @deprecated since 1.5.0, use {@link runMultipleSceanriosWithConfig} or
146 * {@link runMultipleSceanriosWithConfigAsync} instead
147 */
148export const runScenario = (
149 scenarioPath: string,
150 actionDir: string,
151 outDir = 'out',
152 envConfigFile: string,
153): void => {
154 runMultipleSceanriosWithConfig(
155 actionDir,
156 outDir,
157 envConfigFile.substring(
158 0,
159 envConfigFile.length - 12,
160 ) /* substracting '/config.yaml' from the string */,
161 { environmentNameToBeUsed: 'config' },
162 [scenarioPath],
163 );
164};
165
166async function processScenarios(
167 scenarios: Scenario[],
168 numberOfScenariosRunInParallel: number,
169 drawDiagrams: boolean,
170): Promise<boolean> {
171 for (let i = 0; i < scenarios.length; i += numberOfScenariosRunInParallel) {
172 // eslint-disable-next-line no-await-in-loop
173 await Promise.all(
174 scenarios
175 .slice(i, i + numberOfScenariosRunInParallel)
176 .map(invokeActionsSynchronously),
177 );
178 }
179 printResults();
180 return generateDiagramsAndDetermineSuccess(drawDiagrams);
181}
182
183async function invokeActionsSynchronously(scenario: Scenario): Promise<void> {
184 const scenarioName = scenario.name;
185 RESULTS.set(scenarioName, []);
186
187 const ctx = { scenario: scenarioName };
188 const MSG_WIDTH = 100;
189
190 getLogger(scenarioName).debug(pad(MSG_WIDTH, '#', '#'), ctx);
191 getLogger(scenarioName).debug(
192 pad(
193 `#### (S): ${scenarioName}: ${scenario.description} `,
194 MSG_WIDTH,
195 '#',
196 ),
197 ctx,
198 );
199 getLogger(scenarioName).debug(pad(MSG_WIDTH, '#', '#'), ctx);
200 initDiagramCreation(scenarioName);
201
202 const timeDiffInMs = (stop: [number, number]): number =>
203 (stop[0] * 1e9 + stop[1]) * 1e-6;
204
205 let successful = true;
206 const actionsToCancel: ActionCallback[] = [];
207 const actionsToAwaitAtEnd: Promise<unknown>[] = [];
208
209 const handleError = (
210 reason: unknown,
211 action: Action,
212 start: [number, number],
213 context: { scenario: string; action: string },
214 ): void => {
215 const duration = timeDiffInMs(process.hrtime(start)).toFixed(2);
216
217 const scenarioResults = RESULTS.get(scenarioName);
218 if (scenarioResults)
219 scenarioResults.push(
220 new TestResult(
221 action.description,
222 duration,
223 false,
224 action.allowFailure,
225 ),
226 );
227
228 if (reason)
229 getLogger(scenario.name).error(
230 reason instanceof Error
231 ? reason.toString()
232 : JSON.stringify(reason),
233 context,
234 );
235 getLogger(scenario.name).info(
236 pad(MSG_WIDTH, ` Time: ${duration} ms ###########`, '#'),
237 context,
238 );
239
240 if (action.allowFailure !== true) {
241 successful = false;
242 }
243 };
244
245 for (const action of scenario.actions) {
246 if (!successful) {
247 // after first ERROR skip further actions unless 'Action#invokeEvenOnFail' is set to TRUE
248 if (!action.invokeEvenOnFail) continue;
249 }
250
251 const context = { ...ctx, action: action.name };
252
253 getLogger(scenarioName).info(
254 pad(`#### (A): ${action.description} `, MSG_WIDTH, '#'),
255 context,
256 );
257 const start = process.hrtime();
258
259 const actionCallback = action.invoke(scenario);
260 const actionPromise = actionCallback.promise
261 .then(result => {
262 const duration = timeDiffInMs(process.hrtime(start)).toFixed(2);
263
264 const scenarioResults = RESULTS.get(scenarioName);
265 if (scenarioResults)
266 scenarioResults.push(
267 new TestResult(
268 action.description,
269 duration,
270 true,
271 action.allowFailure,
272 ),
273 );
274
275 if (result)
276 getLogger(scenario.name).debug(
277 JSON.stringify(result),
278 context,
279 );
280 getLogger(scenario.name).info(
281 pad(MSG_WIDTH, ` Time: ${duration} ms ###########`, '#'),
282 context,
283 );
284 })
285 .catch(reason => handleError(reason, action, start, context));
286
287 if (
288 action.type === ActionType.WEBSOCKET ||
289 action.type === ActionType.AMQP_LISTEN
290 ) {
291 actionsToCancel.push(actionCallback);
292 }
293 if (
294 action.type === ActionType.MQTT ||
295 action.type === ActionType.WEBSOCKET ||
296 action.type === ActionType.AMQP_LISTEN
297 ) {
298 actionsToAwaitAtEnd.push(actionPromise);
299 } else {
300 await actionPromise; // eslint-disable-line no-await-in-loop
301 }
302 }
303
304 // stop all async running actions
305 actionsToCancel.forEach(callback => callback.cancel());
306 await Promise.all(actionsToAwaitAtEnd);
307}
308
309function printResults(): void {
310 RESULTS.forEach((result, scenario) => {
311 const ctx = { scenario };
312 const MSG_WIDTH = 100;
313
314 getLogger(scenario).info(
315 pad(`#### SUMMARY: ${scenario} `, MSG_WIDTH, '#'),
316 ctx,
317 );
318
319 result.forEach((res: TestResult) => {
320 if (res.successful) {
321 getLogger(scenario).info(
322 ` OK: ${pad(res.action, 50)} ${res.duration} ms`,
323 ctx,
324 );
325 } else if (res.allowFailure) {
326 getLogger(scenario).info(
327 `IGN: ${pad(res.action, 50)} ${res.duration} ms`,
328 ctx,
329 );
330 } else {
331 getLogger(scenario).info(
332 `NOK: ${pad(res.action, 50)} ${res.duration} ms`,
333 ctx,
334 );
335 }
336 });
337
338 getLogger(scenario).info(pad(MSG_WIDTH, '#', '#'), ctx);
339 });
340}
341
342async function generateDiagramsAndDetermineSuccess(
343 drawDiagrams: boolean,
344): Promise<boolean> {
345 let anyError = false;
346 const diagrams: Promise<void>[] = [];
347 RESULTS.forEach((results, scenario) => {
348 if (drawDiagrams) {
349 diagrams.push(generateSequenceDiagram(scenario));
350 }
351 anyError =
352 anyError || results.some(result => result.isConsideredFailure());
353 });
354 await Promise.all(diagrams);
355 return !anyError;
356}
357
358export const OUTPUT_DIR = (): string => OUT_DIR;