Skip to content

Commit d5bfecf

Browse files
add basic support for testing matrix
1 parent cbf4c4e commit d5bfecf

File tree

6 files changed

+152
-13
lines changed

6 files changed

+152
-13
lines changed

src/common_types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ export type RunOptions = BaseArgs & {
237237
environment?: string;
238238
playwrightOptions?: PlaywrightOptions;
239239
networkConditions?: NetworkConditions;
240+
matrix?: Matrix;
240241
reporter?: BuiltInReporterName | ReporterInstance;
241242
};
242243

@@ -254,12 +255,18 @@ export type ProjectSettings = {
254255
space: string;
255256
};
256257

258+
export type Matrix = {
259+
values: Record<string, unknown[]>;
260+
adjustments?: Array<Record<string, unknown>>;
261+
}
262+
257263
export type PlaywrightOptions = LaunchOptions & BrowserContextOptions;
258264
export type SyntheticsConfig = {
259265
params?: Params;
260266
playwrightOptions?: PlaywrightOptions;
261267
monitor?: MonitorConfig;
262268
project?: ProjectSettings;
269+
matrix?: Matrix;
263270
};
264271

265272
/** Runner Payload types */

src/core/index.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,21 @@
2222
* THE SOFTWARE.
2323
*
2424
*/
25-
2625
import { Journey, JourneyCallback, JourneyOptions } from '../dsl';
2726
import Runner from './runner';
2827
import { VoidCallback, HooksCallback, Location } from '../common_types';
28+
import { normalizeOptions } from '../options';
2929
import { wrapFnWithLocation } from '../helpers';
30+
import { getCombinations, getCombinationName } from '../matrix';
3031
import { log } from './logger';
3132
import { MonitorConfig } from '../dsl/monitor';
3233

34+
35+
/* TODO: Testing
36+
* Local vs global matrix: Local matrix fully overwrites global matrix, rather than merging
37+
* Adjustments: Duplicates in adjustments do not run extra journeys
38+
*
39+
3340
/**
3441
* Use a gloabl Runner which would be accessed by the runtime and
3542
* required to handle the local vs global invocation through CLI
@@ -38,7 +45,6 @@ const SYNTHETICS_RUNNER = Symbol.for('SYNTHETICS_RUNNER');
3845
if (!global[SYNTHETICS_RUNNER]) {
3946
global[SYNTHETICS_RUNNER] = new Runner();
4047
}
41-
4248
export const runner: Runner = global[SYNTHETICS_RUNNER];
4349

4450
export const journey = wrapFnWithLocation(
@@ -51,9 +57,27 @@ export const journey = wrapFnWithLocation(
5157
if (typeof options === 'string') {
5258
options = { name: options, id: options };
5359
}
54-
const j = new Journey(options, callback, location);
55-
runner.addJourney(j);
56-
return j;
60+
const { matrix: globalMatrix } = normalizeOptions({});
61+
const { matrix: journeyMatrix } = options;
62+
if (!globalMatrix && !journeyMatrix) {
63+
const j = new Journey(options, callback, location);
64+
runner.addJourney(j);
65+
return j;
66+
}
67+
68+
// local journey matrix takes priority over global matrix
69+
const matrix = journeyMatrix || globalMatrix;
70+
71+
if (!matrix.values) {
72+
throw new Error('Please specify values for your testing matrix');
73+
}
74+
75+
const combinations = getCombinations(matrix);
76+
combinations.forEach(matrixParams => {
77+
const name = getCombinationName((options as JourneyOptions)?.name, matrixParams);
78+
const j = new Journey({...options as JourneyOptions, name}, callback, location, matrixParams);
79+
runner.addJourney(j);
80+
})
5781
}
5882
);
5983

src/core/runner.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -339,16 +339,18 @@ export default class Runner {
339339

340340
async runJourney(journey: Journey, options: RunOptions) {
341341
const result: JourneyResult = { status: 'succeeded' };
342-
const context = await Runner.createContext(options);
342+
const params = {...options.params, ...journey.params};
343+
const journeyOptions = { ...options, params};
344+
const context = await Runner.createContext(journeyOptions);
343345
log(`Runner: start journey (${journey.name})`);
344346
try {
345347
this.registerJourney(journey, context);
346348
const hookArgs = {
347349
env: options.environment,
348-
params: options.params,
350+
params: params,
349351
};
350352
await this.runBeforeHook(journey, hookArgs);
351-
const stepResults = await this.runSteps(journey, context, options);
353+
const stepResults = await this.runSteps(journey, context, journeyOptions);
352354
/**
353355
* Mark journey as failed if any intermediate step fails
354356
*/
@@ -363,7 +365,7 @@ export default class Runner {
363365
result.status = 'failed';
364366
result.error = e;
365367
} finally {
366-
await this.endJourney(journey, { ...context, ...result }, options);
368+
await this.endJourney(journey, { ...context, ...result }, journeyOptions);
367369
await Gatherer.dispose(context.driver);
368370
}
369371
log(`Runner: end journey (${journey.name})`);
@@ -399,6 +401,7 @@ export default class Runner {
399401
const { match, tags } = options;
400402
const monitors: Monitor[] = [];
401403
for (const journey of this.journeys) {
404+
const params = { ...this.monitor.config?.params, ...options.params, ...journey.params };
402405
if (!journey.isMatch(match, tags)) {
403406
continue;
404407
}
@@ -407,8 +410,8 @@ export default class Runner {
407410
* Execute dummy callback to get all monitor specific
408411
* configurations for the current journey
409412
*/
410-
journey.callback({ params: options.params } as any);
411-
journey.monitor.update(this.monitor?.config);
413+
journey.callback({ params: params } as any);
414+
journey.monitor.update({ ...this.monitor?.config, params });
412415
journey.monitor.validate();
413416
monitors.push(journey.monitor);
414417
}

src/dsl/journey.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ import {
3232
} from 'playwright-chromium';
3333
import micromatch, { isMatch } from 'micromatch';
3434
import { Step } from './step';
35-
import { VoidCallback, HooksCallback, Params, Location } from '../common_types';
35+
import { VoidCallback, HooksCallback, Params, Location, Matrix } from '../common_types';
3636
import { Monitor, MonitorConfig } from './monitor';
3737

3838
export type JourneyOptions = {
3939
name: string;
4040
id?: string;
4141
tags?: string[];
42+
matrix?: Matrix;
4243
};
4344

4445
type HookType = 'before' | 'after';
@@ -61,18 +62,21 @@ export class Journey {
6162
steps: Step[] = [];
6263
hooks: Hooks = { before: [], after: [] };
6364
monitor: Monitor;
65+
params: Params = {};
6466

6567
constructor(
6668
options: JourneyOptions,
6769
callback: JourneyCallback,
68-
location?: Location
70+
location?: Location,
71+
params?: Params,
6972
) {
7073
this.name = options.name;
7174
this.id = options.id || options.name;
7275
this.tags = options.tags;
7376
this.callback = callback;
7477
this.location = location;
7578
this.updateMonitor({});
79+
this.params = params;
7680
}
7781

7882
addStep(name: string, callback: VoidCallback, location?: Location) {

src/matrix.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* MIT License
3+
*
4+
* Copyright (c) 2020-present, Elastic NV
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*
24+
*/
25+
26+
import { createHash } from 'crypto';
27+
import type { Matrix } from './common_types';
28+
29+
export const getCombinations = (matrix: Matrix): Array<Record<string, unknown>> => {
30+
const { values, adjustments } = matrix;
31+
const matrixKeys = Object.keys(matrix.values);
32+
const entries = Object.values(values);
33+
34+
let combinations = calculateCombinations(entries[0]);
35+
for (let i = 1; i < entries.length; i++) {
36+
combinations = calculateCombinations(combinations, entries[i]);
37+
}
38+
39+
const matrixParams = combinations.map(combination => {
40+
return getCombinationParams(matrixKeys, combination);
41+
});
42+
43+
if (!adjustments) {
44+
return matrixParams;
45+
}
46+
47+
const currentCombinations = new Set(matrixParams.map(params => {
48+
const hash = createHash('sha256');
49+
const paramHash = hash.update(JSON.stringify(params)).digest('base64');
50+
return paramHash;
51+
}));
52+
53+
adjustments.forEach(adjustment => {
54+
const hash = createHash('sha256');
55+
const adjustmentHash = hash.update(JSON.stringify(adjustment)).digest('base64');
56+
if (!currentCombinations.has(adjustmentHash)) {
57+
matrixParams.push(adjustment);
58+
}
59+
});
60+
61+
return matrixParams;
62+
}
63+
64+
export const calculateCombinations = (groupA: Array<unknown | unknown[]>, groupB?: Array<unknown>): Array<unknown[]> => {
65+
const results = [];
66+
groupA.forEach(optionA => {
67+
if (!groupB) {
68+
results.push([optionA]);
69+
return;
70+
}
71+
groupB.forEach(optionB => {
72+
if (Array.isArray(optionA)) {
73+
return results.push([...optionA, optionB])
74+
} else {
75+
return results.push([optionA, optionB])
76+
}
77+
});
78+
});
79+
return results;
80+
}
81+
82+
export const getCombinationName = (name: string, combinations: Record<string, unknown>) => {
83+
const values = Object.values(combinations);
84+
return values.reduce<string>((acc, combination) => {
85+
const nameAdjustment = typeof combination === 'object' ? JSON.stringify(combination) : combination.toString();
86+
acc += ` - ${nameAdjustment.toString()}`;
87+
return acc;
88+
}, name).trim();
89+
}
90+
91+
export const getCombinationParams = (keys: string[], values: unknown[]): Record<string, unknown> => {
92+
return keys.reduce<Record<string, unknown>>((acc, key, index) => {
93+
acc[key] = values[index];
94+
return acc;
95+
}, {});
96+
}

src/options.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ export function normalizeOptions(cliArgs: CliArgs): RunOptions {
100100
*/
101101
options.params = Object.freeze(merge(config.params, cliArgs.params || {}));
102102

103+
/**
104+
* Grab matrix only from config and not cliArgs
105+
*/
106+
options.matrix = Object.freeze(config.matrix);
107+
103108
/**
104109
* Merge playwright options from CLI and Synthetics config
105110
* and prefer individual options over other option

0 commit comments

Comments
 (0)