1 | import * as pad from 'pad';
|
2 | import { stringify } from 'querystring';
|
3 | import { loadAllActions } from './actionLoading';
|
4 | import { generateSequenceDiagram, initDiagramCreation } from './diagramDrawing';
|
5 | import { getLogger } from './logging';
|
6 | import { Action } from './model/Action';
|
7 | import { ActionType } from './model/ActionType';
|
8 | import { Scenario } from './model/Scenario';
|
9 | import { TestResult } from './model/TestResult';
|
10 | import { loadAllScenarios, loadScenariosById } from './scenarioLoading';
|
11 | import { loadYamlConfiguration } from './yamlParsing';
|
12 | import { ActionCallback } from './model/ActionCallback';
|
13 |
|
14 | const RESULTS: Map<string, TestResult[]> = new Map();
|
15 |
|
16 | let OUT_DIR = '';
|
17 |
|
18 |
|
19 | process.env.PLANTUML_LIMIT_SIZE = '16384';
|
20 |
|
21 | interface RunConfiguration {
|
22 | numberOfScenariosRunInParallel?: number;
|
23 | environmentNameToBeUsed?: string;
|
24 | drawDiagrams?: boolean;
|
25 | }
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | export 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 |
|
45 | export 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 |
|
63 | export const runMultipleScenariosWithConfigAsync = async (
|
64 | actionDir: string,
|
65 | outDir = 'out',
|
66 | envConfigDir: string,
|
67 | runConfig: RunConfiguration,
|
68 | scenarioPaths: string[],
|
69 | ): Promise<boolean> => {
|
70 |
|
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 |
|
146 |
|
147 |
|
148 | export 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 | ) ,
|
161 | { environmentNameToBeUsed: 'config' },
|
162 | [scenarioPath],
|
163 | );
|
164 | };
|
165 |
|
166 | async 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 |
|
173 | await Promise.all(
|
174 | scenarios
|
175 | .slice(i, i + numberOfScenariosRunInParallel)
|
176 | .map(invokeActionsSynchronously),
|
177 | );
|
178 | }
|
179 | printResults();
|
180 | return generateDiagramsAndDetermineSuccess(drawDiagrams);
|
181 | }
|
182 |
|
183 | async 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 |
|
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;
|
301 | }
|
302 | }
|
303 |
|
304 |
|
305 | actionsToCancel.forEach(callback => callback.cancel());
|
306 | await Promise.all(actionsToAwaitAtEnd);
|
307 | }
|
308 |
|
309 | function 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 |
|
342 | async 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 |
|
358 | export const OUTPUT_DIR = (): string => OUT_DIR;
|