@allurereport/core 3.16.1 → 3.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +3 -1
- package/dist/config.d.ts +2 -1
- package/dist/config.js +18 -13
- package/dist/history.d.ts +5 -2
- package/dist/history.js +53 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/qualityGate/qualityGate.d.ts +7 -4
- package/dist/qualityGate/qualityGate.js +21 -15
- package/dist/qualityGate/rules.d.ts +13 -8
- package/dist/qualityGate/rules.js +149 -12
- package/dist/report.js +156 -40
- package/dist/resolutions.d.ts +8 -0
- package/dist/resolutions.js +23 -2
- package/dist/store/convert.js +1 -0
- package/dist/store/store.d.ts +4 -1
- package/dist/store/store.js +118 -41
- package/dist/utils/artifacts.d.ts +14 -0
- package/dist/utils/artifacts.js +35 -0
- package/dist/utils/event.d.ts +20 -0
- package/dist/utils/event.js +21 -0
- package/dist/utils/perf.d.ts +6 -10
- package/dist/utils/perf.js +41 -28
- package/package.json +22 -22
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CategoriesConfig, ResolvedAllureServiceConfig } from "@allurereport/core-api";
|
|
1
|
+
import type { CategoriesConfig, PerformanceConfig, ResolvedAllureServiceConfig } from "@allurereport/core-api";
|
|
2
2
|
import type { Plugin, ReportFiles, Config } from "@allurereport/plugin-api";
|
|
3
3
|
import type { ResultsReader } from "@allurereport/reader-api";
|
|
4
4
|
export interface PluginInstance {
|
|
@@ -16,9 +16,11 @@ export interface FullConfig extends Omit<Config, "name" | "output" | "open" | "a
|
|
|
16
16
|
plugins?: PluginInstance[];
|
|
17
17
|
realTime?: any;
|
|
18
18
|
qualityGate?: Config["qualityGate"];
|
|
19
|
+
performance?: PerformanceConfig;
|
|
19
20
|
allureService?: ResolvedAllureServiceConfig;
|
|
20
21
|
categories?: CategoriesConfig;
|
|
21
22
|
globalAttachments?: string[];
|
|
23
|
+
cwd?: string;
|
|
22
24
|
resultsDir?: string[];
|
|
23
25
|
}
|
|
24
26
|
export {};
|
package/dist/config.d.ts
CHANGED
|
@@ -2,17 +2,18 @@ import type { Config, Plugin, PluginConstructorContext, PluginDescriptor } from
|
|
|
2
2
|
import type { FullConfig, PluginInstance } from "./api.js";
|
|
3
3
|
type PluginConstructor = new (options?: Record<string, any>, context?: PluginConstructorContext) => Plugin;
|
|
4
4
|
export interface ConfigOverride {
|
|
5
|
+
cwd?: string;
|
|
5
6
|
name?: Config["name"];
|
|
6
7
|
output?: Config["output"];
|
|
7
8
|
open?: Config["open"];
|
|
8
9
|
port?: Config["port"];
|
|
9
10
|
hideLabels?: Config["hideLabels"];
|
|
10
11
|
historyPath?: Config["historyPath"];
|
|
12
|
+
historyBaseUrl?: Config["historyBaseUrl"];
|
|
11
13
|
historyLimit?: Config["historyLimit"];
|
|
12
14
|
resolutions?: Pick<NonNullable<Config["resolutions"]>, "knownIssuesPath">;
|
|
13
15
|
plugins?: Config["plugins"];
|
|
14
16
|
}
|
|
15
|
-
export declare const parseIntegerConfigValue: (value: unknown, defaultValue: number, minValue: number) => number;
|
|
16
17
|
export declare const isAgentDescriptor: (value: string | undefined) => value is "agent" | "@allurereport/plugin-agent";
|
|
17
18
|
export declare const hasConfiguredAgent: (plugins: Record<string, PluginDescriptor>) => boolean;
|
|
18
19
|
export declare const assertValidPluginId: (id: string) => void;
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { extname, resolve } from "node:path";
|
|
3
3
|
import * as process from "node:process";
|
|
4
|
-
import { validateEnvironmentName } from "@allurereport/core-api";
|
|
4
|
+
import { parseIntegerConfigValue, validateEnvironmentName } from "@allurereport/core-api";
|
|
5
5
|
import { createJiti } from "jiti";
|
|
6
6
|
import { parse } from "yaml";
|
|
7
7
|
import { FileSystemReportFiles } from "./plugin.js";
|
|
@@ -23,16 +23,8 @@ const CONFIG_FILENAMES = [
|
|
|
23
23
|
"allurerc.yml",
|
|
24
24
|
];
|
|
25
25
|
const DEFAULT_CONFIG = {};
|
|
26
|
-
const DEFAULT_ALLURE_SERVICE_UPLOAD_CONCURRENCY = 100;
|
|
27
26
|
const DEFAULT_ALLURE_SERVICE_UPLAOD_MAX_ATTEMPTS = 5;
|
|
28
27
|
const DEFAULT_ALLURE_SERVICE_UPLOAD_MAX_SIMULTANEOUS_FAILURES = 5;
|
|
29
|
-
export const parseIntegerConfigValue = (value, defaultValue, minValue) => {
|
|
30
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
31
|
-
return defaultValue;
|
|
32
|
-
}
|
|
33
|
-
const normalized = Math.floor(value);
|
|
34
|
-
return normalized >= minValue ? normalized : defaultValue;
|
|
35
|
-
};
|
|
36
28
|
export const isAgentDescriptor = (value) => {
|
|
37
29
|
return value === "agent" || value === "@allurereport/plugin-agent";
|
|
38
30
|
};
|
|
@@ -97,6 +89,7 @@ export const validateConfig = (config) => {
|
|
|
97
89
|
"port",
|
|
98
90
|
"hideLabels",
|
|
99
91
|
"historyPath",
|
|
92
|
+
"historyBaseUrl",
|
|
100
93
|
"historyLimit",
|
|
101
94
|
"resolutions",
|
|
102
95
|
"plugins",
|
|
@@ -107,10 +100,12 @@ export const validateConfig = (config) => {
|
|
|
107
100
|
"environments",
|
|
108
101
|
"appendHistory",
|
|
109
102
|
"qualityGate",
|
|
103
|
+
"performance",
|
|
110
104
|
"allureService",
|
|
111
105
|
"categories",
|
|
112
106
|
"globalAttachments",
|
|
113
107
|
"resultsDir",
|
|
108
|
+
"dump",
|
|
114
109
|
];
|
|
115
110
|
const unsupportedFields = Object.keys(config).filter((key) => !supportedFields.includes(key));
|
|
116
111
|
return {
|
|
@@ -201,7 +196,9 @@ export const resolveConfig = async (config, override = {}) => {
|
|
|
201
196
|
const port = override.port ?? config.port ?? undefined;
|
|
202
197
|
const hideLabels = override.hideLabels ?? config.hideLabels;
|
|
203
198
|
const historyPath = override.historyPath ?? config.historyPath;
|
|
199
|
+
const historyBaseUrl = override.historyBaseUrl ?? config.historyBaseUrl;
|
|
204
200
|
const historyLimit = override.historyLimit ?? config.historyLimit;
|
|
201
|
+
const dump = config.dump;
|
|
205
202
|
const appendHistory = config.appendHistory ?? true;
|
|
206
203
|
const configuredKnownIssuesPath = override.resolutions?.knownIssuesPath ?? config.resolutions?.knownIssuesPath;
|
|
207
204
|
const knownIssuesPathInput = configuredKnownIssuesPath ?? (config.resolutions ? DEFAULT_KNOWN_ISSUES_PATH : undefined);
|
|
@@ -210,6 +207,7 @@ export const resolveConfig = async (config, override = {}) => {
|
|
|
210
207
|
? { ...(config.resolutions ?? { rules: [] }), knownIssuesPath }
|
|
211
208
|
: undefined;
|
|
212
209
|
const output = resolve(override.output ?? config.output ?? "./allure-report");
|
|
210
|
+
const cwd = resolve(override.cwd ?? process.cwd());
|
|
213
211
|
const variables = config.variables ?? {};
|
|
214
212
|
const resultsDir = normalizeResultsDir(config.resultsDir);
|
|
215
213
|
let pluginInstances = [];
|
|
@@ -233,8 +231,10 @@ export const resolveConfig = async (config, override = {}) => {
|
|
|
233
231
|
};
|
|
234
232
|
pluginInstances = await resolvePlugins(pluginsWithAgent);
|
|
235
233
|
}
|
|
234
|
+
const uploadConcurrency = parseIntegerConfigValue(config.allureService?.uploadConcurrency, 1);
|
|
236
235
|
return {
|
|
237
236
|
name,
|
|
237
|
+
cwd,
|
|
238
238
|
output,
|
|
239
239
|
open,
|
|
240
240
|
port,
|
|
@@ -247,17 +247,22 @@ export const resolveConfig = async (config, override = {}) => {
|
|
|
247
247
|
appendHistory,
|
|
248
248
|
historyLimit,
|
|
249
249
|
historyPath: historyPath ? resolve(historyPath) : undefined,
|
|
250
|
+
historyBaseUrl,
|
|
251
|
+
dump,
|
|
250
252
|
reportFiles: new FileSystemReportFiles(output),
|
|
251
253
|
plugins: pluginInstances,
|
|
252
254
|
defaultLabels: config.defaultLabels ?? {},
|
|
253
255
|
qualityGate: config.qualityGate,
|
|
256
|
+
performance: config.performance,
|
|
254
257
|
allureService: config.allureService
|
|
255
258
|
? {
|
|
256
259
|
accessToken: config.allureService.accessToken,
|
|
257
260
|
private: config.allureService.private,
|
|
258
|
-
uploadConcurrency:
|
|
259
|
-
uploadMaxAttempts: parseIntegerConfigValue(config.allureService.uploadMaxAttempts,
|
|
260
|
-
|
|
261
|
+
...(uploadConcurrency === undefined ? {} : { uploadConcurrency }),
|
|
262
|
+
uploadMaxAttempts: parseIntegerConfigValue(config.allureService.uploadMaxAttempts, 1) ??
|
|
263
|
+
DEFAULT_ALLURE_SERVICE_UPLAOD_MAX_ATTEMPTS,
|
|
264
|
+
uploadMaxSimultaneousFailures: parseIntegerConfigValue(config.allureService.uploadMaxSimultaneousFailures, 0) ??
|
|
265
|
+
DEFAULT_ALLURE_SERVICE_UPLOAD_MAX_SIMULTANEOUS_FAILURES,
|
|
261
266
|
}
|
|
262
267
|
: undefined,
|
|
263
268
|
categories: config.categories,
|
|
@@ -289,7 +294,7 @@ export const readConfig = async (cwd = process.cwd(), configPath, override) => {
|
|
|
289
294
|
default:
|
|
290
295
|
config = DEFAULT_CONFIG;
|
|
291
296
|
}
|
|
292
|
-
const fullConfig = await resolveConfig(config, override);
|
|
297
|
+
const fullConfig = await resolveConfig(config, { ...override, cwd });
|
|
293
298
|
return fullConfig;
|
|
294
299
|
};
|
|
295
300
|
export const readRawConfig = async (cwd = process.cwd(), configPath) => {
|
package/dist/history.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { type AllureHistory, type HistoryDataPoint, type TestCase, type TestResult } from "@allurereport/core-api";
|
|
2
|
-
export declare const
|
|
1
|
+
import { type AllureHistory, type HistoryDataPoint, type MetricSample, type TestCase, type TestResult } from "@allurereport/core-api";
|
|
2
|
+
export declare const normalizeHistoryBaseUrl: (historyBaseUrl: string) => string;
|
|
3
|
+
export declare const setHistoryDataPointUrl: (point: HistoryDataPoint, url: string) => HistoryDataPoint;
|
|
4
|
+
export declare const createHistory: (reportUuid: string, reportName: string | undefined, testCases: TestCase[], testResults: TestResult[], remoteUrl?: string, metrics?: MetricSample[]) => HistoryDataPoint;
|
|
3
5
|
export declare class AllureLocalHistory implements AllureHistory {
|
|
4
6
|
#private;
|
|
5
7
|
private readonly params;
|
|
@@ -7,6 +9,7 @@ export declare class AllureLocalHistory implements AllureHistory {
|
|
|
7
9
|
historyPath: string;
|
|
8
10
|
limit?: number;
|
|
9
11
|
});
|
|
12
|
+
resolveTestResultUrl(historyUrl: string, pluginId: string, historicalResultId: string): string;
|
|
10
13
|
readHistory(): Promise<HistoryDataPoint[]>;
|
|
11
14
|
appendHistory(data: HistoryDataPoint): Promise<void>;
|
|
12
15
|
}
|
package/dist/history.js
CHANGED
|
@@ -43,7 +43,39 @@ const createHistoryItems = (testResults, remoteUrl) => {
|
|
|
43
43
|
return acc;
|
|
44
44
|
}, {});
|
|
45
45
|
};
|
|
46
|
-
|
|
46
|
+
const metricsToHistoryValues = (metrics) => {
|
|
47
|
+
const grouped = new Map();
|
|
48
|
+
for (const metric of metrics) {
|
|
49
|
+
if (!metric.key || !Number.isFinite(metric.value)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
grouped.set(metric.key, [...(grouped.get(metric.key) ?? []), metric.value]);
|
|
53
|
+
}
|
|
54
|
+
return Object.fromEntries([...grouped.entries()].map(([key, values]) => [key, values.reduce((acc, value) => acc + value, 0) / values.length]));
|
|
55
|
+
};
|
|
56
|
+
export const normalizeHistoryBaseUrl = (historyBaseUrl) => {
|
|
57
|
+
let url;
|
|
58
|
+
try {
|
|
59
|
+
url = new URL(historyBaseUrl);
|
|
60
|
+
}
|
|
61
|
+
catch (cause) {
|
|
62
|
+
throw new Error(`Invalid historyBaseUrl ${JSON.stringify(historyBaseUrl)}: expected an absolute URL`, { cause });
|
|
63
|
+
}
|
|
64
|
+
if (url.href.includes("#")) {
|
|
65
|
+
throw new Error(`Invalid historyBaseUrl ${JSON.stringify(historyBaseUrl)}: fragments are not allowed`);
|
|
66
|
+
}
|
|
67
|
+
if (/\.html\/?$/iu.test(url.pathname)) {
|
|
68
|
+
throw new Error(`Invalid historyBaseUrl ${JSON.stringify(historyBaseUrl)}: expected a base URL, not an HTML document`);
|
|
69
|
+
}
|
|
70
|
+
url.pathname = `${url.pathname.replace(/\/+$/u, "")}/`;
|
|
71
|
+
return url.toString();
|
|
72
|
+
};
|
|
73
|
+
export const setHistoryDataPointUrl = (point, url) => ({
|
|
74
|
+
...point,
|
|
75
|
+
url,
|
|
76
|
+
testResults: Object.fromEntries(Object.entries(point.testResults).map(([historyId, item]) => [historyId, { ...item, url }])),
|
|
77
|
+
});
|
|
78
|
+
export const createHistory = (reportUuid, reportName = "Allure Report", testCases, testResults, remoteUrl = "", metrics = []) => {
|
|
47
79
|
const knownTestCaseIds = testCases.map((tc) => tc.id);
|
|
48
80
|
return {
|
|
49
81
|
uuid: reportUuid,
|
|
@@ -51,7 +83,7 @@ export const createHistory = (reportUuid, reportName = "Allure Report", testCase
|
|
|
51
83
|
timestamp: new Date().getTime(),
|
|
52
84
|
knownTestCaseIds,
|
|
53
85
|
testResults: createHistoryItems(testResults, remoteUrl),
|
|
54
|
-
metrics:
|
|
86
|
+
metrics: metricsToHistoryValues(metrics),
|
|
55
87
|
url: remoteUrl,
|
|
56
88
|
};
|
|
57
89
|
};
|
|
@@ -128,6 +160,17 @@ export class AllureLocalHistory {
|
|
|
128
160
|
`doesn't match the actual number ${actualBytes}`);
|
|
129
161
|
});
|
|
130
162
|
}
|
|
163
|
+
resolveTestResultUrl(historyUrl, pluginId, historicalResultId) {
|
|
164
|
+
if (!historyUrl) {
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
const url = new URL(historyUrl);
|
|
168
|
+
if (url.pathname.endsWith("/")) {
|
|
169
|
+
url.pathname = `${url.pathname}${pluginId}/index.html`;
|
|
170
|
+
}
|
|
171
|
+
url.hash = historicalResultId;
|
|
172
|
+
return url.toString();
|
|
173
|
+
}
|
|
131
174
|
async readHistory() {
|
|
132
175
|
if (__classPrivateFieldGet(this, _AllureLocalHistory_cachedHistory, "f").length > 0) {
|
|
133
176
|
return __classPrivateFieldGet(this, _AllureLocalHistory_cachedHistory, "f");
|
|
@@ -164,15 +207,14 @@ export class AllureLocalHistory {
|
|
|
164
207
|
const { limit } = this.params;
|
|
165
208
|
await mkdir(parentDir, { recursive: true });
|
|
166
209
|
const { file: historyFile, exists: historyExists } = await __classPrivateFieldGet(this, _AllureLocalHistory_ensureFileOpenedToAppend, "f").call(this, fullPath);
|
|
210
|
+
let dst;
|
|
167
211
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (limit === 0 && !historyExists) {
|
|
212
|
+
if (limit === 0) {
|
|
213
|
+
if (historyExists)
|
|
214
|
+
await historyFile.truncate(0);
|
|
174
215
|
return;
|
|
175
216
|
}
|
|
217
|
+
dst = historyFile.createWriteStream({ encoding: "utf-8", start: 0, autoClose: false });
|
|
176
218
|
if (historyExists) {
|
|
177
219
|
const start = await __classPrivateFieldGet(this, _AllureLocalHistory_findFirstEntryAddress, "f").call(this, historyFile, limit ? limit - 1 : undefined);
|
|
178
220
|
const src = historyFile.createReadStream({ start, autoClose: false });
|
|
@@ -185,7 +227,9 @@ export class AllureLocalHistory {
|
|
|
185
227
|
}
|
|
186
228
|
}
|
|
187
229
|
finally {
|
|
188
|
-
|
|
230
|
+
const closing = historyFile.close();
|
|
231
|
+
dst?.destroy();
|
|
232
|
+
await closing;
|
|
189
233
|
if (limit !== 0) {
|
|
190
234
|
__classPrivateFieldGet(this, _AllureLocalHistory_cachedHistory, "f").push(normalizedData);
|
|
191
235
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,4 +11,4 @@ export * from "./resolutions.js";
|
|
|
11
11
|
export { resolveConfig, readConfig, readRawConfig, getPluginInstance } from "./config.js";
|
|
12
12
|
export * from "./report.js";
|
|
13
13
|
export * from "./plugin.js";
|
|
14
|
-
export { QualityGateState, qualityGateDefaultRules, maxFailuresRule, minTestsCountRule, successRateRule, maxDurationRule, allTestsContainEnvRule, environmentsTestedRule, convertQualityGateResultsToTestErrors, stringifyQualityGateResults, } from "./qualityGate/index.js";
|
|
14
|
+
export { QualityGateState, qualityGateDefaultRules, maxFailuresRule, minTestsCountRule, successRateRule, maxDurationRule, allTestsContainEnvRule, environmentsTestedRule, metricMaxRule, metricMinRule, metricMaxDeltaRule, metricMaxDeltaPercentRule, convertQualityGateResultsToTestErrors, filterFailedQualityGateResults, stringifyQualityGateResults, } from "./qualityGate/index.js";
|
package/dist/index.js
CHANGED
|
@@ -10,4 +10,4 @@ export * from "./resolutions.js";
|
|
|
10
10
|
export { resolveConfig, readConfig, readRawConfig, getPluginInstance } from "./config.js";
|
|
11
11
|
export * from "./report.js";
|
|
12
12
|
export * from "./plugin.js";
|
|
13
|
-
export { QualityGateState, qualityGateDefaultRules, maxFailuresRule, minTestsCountRule, successRateRule, maxDurationRule, allTestsContainEnvRule, environmentsTestedRule, convertQualityGateResultsToTestErrors, stringifyQualityGateResults, } from "./qualityGate/index.js";
|
|
13
|
+
export { QualityGateState, qualityGateDefaultRules, maxFailuresRule, minTestsCountRule, successRateRule, maxDurationRule, allTestsContainEnvRule, environmentsTestedRule, metricMaxRule, metricMinRule, metricMaxDeltaRule, metricMaxDeltaPercentRule, convertQualityGateResultsToTestErrors, filterFailedQualityGateResults, stringifyQualityGateResults, } from "./qualityGate/index.js";
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { type TestError, type TestResult } from "@allurereport/core-api";
|
|
2
|
-
import type { QualityGateConfig, QualityGateValidationResult } from "@allurereport/plugin-api";
|
|
1
|
+
import { type MetricSample, type TestError, type TestResult } from "@allurereport/core-api";
|
|
2
|
+
import type { QualityGateConfig, QualityGateMetricHistoryPoint, QualityGateValidationResult } from "@allurereport/plugin-api";
|
|
3
|
+
export declare const filterFailedQualityGateResults: (results: QualityGateValidationResult[]) => QualityGateValidationResult[];
|
|
3
4
|
export declare const stringifyQualityGateResults: (results: QualityGateValidationResult[]) => string;
|
|
4
5
|
export declare const convertQualityGateResultsToTestErrors: (results: QualityGateValidationResult[]) => TestError[];
|
|
5
6
|
export declare class QualityGateState {
|
|
6
7
|
#private;
|
|
7
|
-
setResult(rule: string, value:
|
|
8
|
-
getResult(rule: string):
|
|
8
|
+
setResult(rule: string, value: unknown, testResults?: string[]): void;
|
|
9
|
+
getResult(rule: string): unknown;
|
|
9
10
|
getTestResults(rule: string): string[];
|
|
10
11
|
markTestResultsProcessed(testResults: TestResult[]): void;
|
|
11
12
|
isTestResultProcessed(trId: string): boolean;
|
|
@@ -16,6 +17,8 @@ export declare class QualityGate {
|
|
|
16
17
|
validate(payload: {
|
|
17
18
|
state?: QualityGateState;
|
|
18
19
|
trs: TestResult[];
|
|
20
|
+
metrics?: MetricSample[];
|
|
21
|
+
previousHistory?: QualityGateMetricHistoryPoint[];
|
|
19
22
|
environment?: string;
|
|
20
23
|
}): Promise<{
|
|
21
24
|
fastFailed: boolean;
|
|
@@ -4,25 +4,29 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
4
4
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
5
|
};
|
|
6
6
|
var _QualityGateState_state, _QualityGateState_processedTestResultIds;
|
|
7
|
-
import { gray, red } from "yoctocolors";
|
|
7
|
+
import { bold, gray, red } from "yoctocolors";
|
|
8
8
|
import { isIgnoredFailure } from "../resolutions.js";
|
|
9
9
|
import { qualityGateDefaultRules } from "./rules.js";
|
|
10
|
+
export const filterFailedQualityGateResults = (results) => results.filter(({ success }) => !success);
|
|
11
|
+
const formatQualityGateValue = (value) => (Array.isArray(value) ? value.join(", ") : String(value));
|
|
12
|
+
const defaultSuccessMessage = ({ actual, expected }) => `The rule has been passed; actual ${bold(formatQualityGateValue(actual))}, expected ${bold(formatQualityGateValue(expected))}`;
|
|
10
13
|
export const stringifyQualityGateResults = (results) => {
|
|
11
|
-
|
|
14
|
+
const failedResults = filterFailedQualityGateResults(results);
|
|
15
|
+
if (failedResults.length === 0) {
|
|
12
16
|
return "";
|
|
13
17
|
}
|
|
14
18
|
const lines = [red("Quality Gate failed with following issues:")];
|
|
15
|
-
const maxMessageLength = Math.max(...
|
|
19
|
+
const maxMessageLength = Math.max(...failedResults.map((r) => r.message.length));
|
|
16
20
|
lines.push("");
|
|
17
|
-
|
|
21
|
+
failedResults.forEach((result) => {
|
|
18
22
|
lines.push(` ${red("⨯")} ${result.message.padEnd(maxMessageLength, " ")} ${gray(result.rule)}`);
|
|
19
23
|
});
|
|
20
24
|
lines.push("");
|
|
21
|
-
lines.push(red(`${
|
|
25
|
+
lines.push(red(`${failedResults.length} quality gate rules have been failed.`));
|
|
22
26
|
return lines.join("\n");
|
|
23
27
|
};
|
|
24
28
|
export const convertQualityGateResultsToTestErrors = (results) => {
|
|
25
|
-
return results.map((result) => ({
|
|
29
|
+
return filterFailedQualityGateResults(results).map((result) => ({
|
|
26
30
|
message: `Quality Gate (${result.rule}): ${result.message}`,
|
|
27
31
|
actual: result.actual,
|
|
28
32
|
expected: result.expected,
|
|
@@ -59,7 +63,7 @@ export class QualityGate {
|
|
|
59
63
|
this.config = config;
|
|
60
64
|
}
|
|
61
65
|
async validate(payload) {
|
|
62
|
-
const { state, trs, environment } = payload;
|
|
66
|
+
const { state, trs, metrics = [], previousHistory = [], environment } = payload;
|
|
63
67
|
const trsToValidateById = new Map();
|
|
64
68
|
for (const tr of trs) {
|
|
65
69
|
if (tr.isRetry || isIgnoredFailure(tr) || state?.isTestResultProcessed(tr.id)) {
|
|
@@ -98,23 +102,25 @@ export class QualityGate {
|
|
|
98
102
|
setResult: (value, testResults) => state?.setResult?.(ruleId, value, testResults),
|
|
99
103
|
},
|
|
100
104
|
expected,
|
|
105
|
+
metrics,
|
|
106
|
+
previousHistory,
|
|
101
107
|
environment,
|
|
102
108
|
});
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
const messagePayload = {
|
|
110
|
+
actual: result.actual,
|
|
111
|
+
expected,
|
|
112
|
+
};
|
|
106
113
|
results.push({
|
|
107
114
|
...result,
|
|
108
115
|
expected,
|
|
109
116
|
rule: ruleId,
|
|
110
|
-
message:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}),
|
|
117
|
+
message: result.success
|
|
118
|
+
? (rule.successMessage ?? defaultSuccessMessage)(messagePayload)
|
|
119
|
+
: rule.message(messagePayload),
|
|
114
120
|
environment,
|
|
115
121
|
testResults: [...new Set([...(state?.getTestResults(ruleId) ?? []), ...result.testResults])],
|
|
116
122
|
});
|
|
117
|
-
if (ruleset.fastFail) {
|
|
123
|
+
if (!result.success && ruleset.fastFail) {
|
|
118
124
|
fastFailed = true;
|
|
119
125
|
break;
|
|
120
126
|
}
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { type QualityGateRule } from "@allurereport/plugin-api";
|
|
2
|
+
type MetricRuleConfig = {
|
|
3
|
+
key: string;
|
|
4
|
+
value: number;
|
|
5
|
+
title?: string;
|
|
6
|
+
unit?: string;
|
|
7
|
+
};
|
|
2
8
|
export declare const maxFailuresRule: QualityGateRule<number>;
|
|
3
9
|
export declare const minTestsCountRule: QualityGateRule<number>;
|
|
4
|
-
export declare const successRateRule: QualityGateRule<number
|
|
5
|
-
totalCount: number;
|
|
6
|
-
passedCount: number;
|
|
7
|
-
}>;
|
|
10
|
+
export declare const successRateRule: QualityGateRule<number>;
|
|
8
11
|
export declare const maxDurationRule: QualityGateRule<number>;
|
|
9
12
|
export declare const allTestsContainEnvRule: QualityGateRule<string, number>;
|
|
10
13
|
export declare const environmentsTestedRule: QualityGateRule<string[]>;
|
|
11
|
-
export declare const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
export declare const metricMaxRule: QualityGateRule<MetricRuleConfig>;
|
|
15
|
+
export declare const metricMinRule: QualityGateRule<MetricRuleConfig>;
|
|
16
|
+
export declare const metricMaxDeltaRule: QualityGateRule<MetricRuleConfig>;
|
|
17
|
+
export declare const metricMaxDeltaPercentRule: QualityGateRule<MetricRuleConfig>;
|
|
18
|
+
export declare const qualityGateDefaultRules: (QualityGateRule<number> | QualityGateRule<string, number> | QualityGateRule<string[]> | QualityGateRule<MetricRuleConfig>)[];
|
|
19
|
+
export {};
|
|
@@ -1,10 +1,81 @@
|
|
|
1
|
-
import { filterSuccessful, filterUnsuccessful } from "@allurereport/core-api";
|
|
1
|
+
import { filterIncludedInSuccessRate, filterSuccessful, filterUnsuccessful, getSuccessRate, } from "@allurereport/core-api";
|
|
2
2
|
import { bold } from "yoctocolors";
|
|
3
|
+
const numberStateValue = (value) => (Number.isFinite(value) ? Number(value) : 0);
|
|
4
|
+
const stringArrayStateValue = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
5
|
+
const successRateStateValue = (value) => {
|
|
6
|
+
if (typeof value !== "object" || value === null) {
|
|
7
|
+
return { totalCount: 0, passedCount: 0 };
|
|
8
|
+
}
|
|
9
|
+
const state = value;
|
|
10
|
+
return {
|
|
11
|
+
totalCount: numberStateValue(state.totalCount),
|
|
12
|
+
passedCount: numberStateValue(state.passedCount),
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
const metricValues = (metrics, key) => metrics
|
|
16
|
+
.map((metric) => (metric.key === key ? metric.value : undefined))
|
|
17
|
+
.filter((value) => Number.isFinite(value));
|
|
18
|
+
const metricAverage = (metrics, key) => {
|
|
19
|
+
const values = metricValues(metrics, key);
|
|
20
|
+
if (values.length === 0) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return values.reduce((acc, value) => acc + value, 0) / values.length;
|
|
24
|
+
};
|
|
25
|
+
const previousMetricValue = (history, key) => {
|
|
26
|
+
const previousHistory = [...history].sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0));
|
|
27
|
+
for (const historyPoint of previousHistory) {
|
|
28
|
+
const value = historyPoint.metrics?.[key];
|
|
29
|
+
if (Number.isFinite(value)) {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
};
|
|
35
|
+
const metricActual = (metrics, key, value) => {
|
|
36
|
+
const sample = metrics.find((metric) => metric.key === key);
|
|
37
|
+
return {
|
|
38
|
+
key,
|
|
39
|
+
value,
|
|
40
|
+
...(sample?.title ? { title: sample.title } : {}),
|
|
41
|
+
...(sample?.unit ? { unit: sample.unit } : {}),
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const formatMetricValue = (value, unit) => {
|
|
45
|
+
if (!Number.isFinite(value)) {
|
|
46
|
+
return "n/a";
|
|
47
|
+
}
|
|
48
|
+
const finiteValue = value;
|
|
49
|
+
const formatted = Number.isInteger(finiteValue) ? String(finiteValue) : String(Number(finiteValue.toFixed(3)));
|
|
50
|
+
return unit ? `${formatted} ${unit}` : formatted;
|
|
51
|
+
};
|
|
52
|
+
const metricRuleTitle = (actual) => actual.title ?? actual.key;
|
|
53
|
+
const metricRuleUnit = (actual, expected) => actual.unit ?? expected.unit;
|
|
54
|
+
const missingMetricResult = (actual) => ({
|
|
55
|
+
success: false,
|
|
56
|
+
actual,
|
|
57
|
+
testResults: [],
|
|
58
|
+
});
|
|
59
|
+
const metricRuleResult = (success, actual) => ({
|
|
60
|
+
success,
|
|
61
|
+
actual,
|
|
62
|
+
testResults: [],
|
|
63
|
+
});
|
|
64
|
+
const metricDeltaWithinThreshold = (delta, threshold, better) => {
|
|
65
|
+
if (better === "lower") {
|
|
66
|
+
return delta <= threshold;
|
|
67
|
+
}
|
|
68
|
+
if (better === "higher") {
|
|
69
|
+
return -delta <= threshold;
|
|
70
|
+
}
|
|
71
|
+
return Math.abs(delta) <= threshold;
|
|
72
|
+
};
|
|
3
73
|
export const maxFailuresRule = {
|
|
4
74
|
rule: "maxFailures",
|
|
5
75
|
message: ({ actual, expected }) => `The number of failed tests ${bold(String(actual))} exceeds the allowed threshold value ${bold(String(expected))}`,
|
|
76
|
+
successMessage: ({ actual, expected }) => `The number of failed tests ${bold(String(actual))} is within the allowed threshold value ${bold(String(expected))}`,
|
|
6
77
|
validate: async ({ trs, expected, state }) => {
|
|
7
|
-
const previous = state.getResult()
|
|
78
|
+
const previous = numberStateValue(state.getResult());
|
|
8
79
|
const failedTrs = trs.filter(filterUnsuccessful);
|
|
9
80
|
const testResults = failedTrs.map((tr) => tr.id);
|
|
10
81
|
const actual = previous + failedTrs.length;
|
|
@@ -19,8 +90,9 @@ export const maxFailuresRule = {
|
|
|
19
90
|
export const minTestsCountRule = {
|
|
20
91
|
rule: "minTestsCount",
|
|
21
92
|
message: ({ actual, expected }) => `The total number of tests ${bold(String(actual))} is less than the expected threshold value ${bold(String(expected))}`,
|
|
93
|
+
successMessage: ({ actual, expected }) => `The total number of tests ${bold(String(actual))} meets the expected threshold value ${bold(String(expected))}`,
|
|
22
94
|
validate: async ({ trs, expected, state }) => {
|
|
23
|
-
const actual = trs.length + (state.getResult()
|
|
95
|
+
const actual = trs.length + numberStateValue(state.getResult());
|
|
24
96
|
state.setResult(actual, []);
|
|
25
97
|
return {
|
|
26
98
|
success: actual >= expected,
|
|
@@ -32,14 +104,16 @@ export const minTestsCountRule = {
|
|
|
32
104
|
export const successRateRule = {
|
|
33
105
|
rule: "successRate",
|
|
34
106
|
message: ({ actual, expected }) => `Success rate ${bold(String(actual))} is less, than expected ${bold(String(expected))}`,
|
|
107
|
+
successMessage: ({ actual, expected }) => `Success rate ${bold(String(actual))} is not less, than expected ${bold(String(expected))}`,
|
|
35
108
|
validate: async ({ trs, expected, state }) => {
|
|
36
|
-
const previous = state.getResult()
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
109
|
+
const previous = successRateStateValue(state.getResult());
|
|
110
|
+
const eligibleTrs = trs.filter(filterIncludedInSuccessRate);
|
|
111
|
+
const passedTrs = eligibleTrs.filter(filterSuccessful);
|
|
112
|
+
const notPassedTrs = eligibleTrs.filter(filterUnsuccessful);
|
|
113
|
+
const totalCount = previous.totalCount + eligibleTrs.length;
|
|
40
114
|
const passedCount = previous.passedCount + passedTrs.length;
|
|
41
115
|
const testResults = notPassedTrs.map((tr) => tr.id);
|
|
42
|
-
const rate =
|
|
116
|
+
const rate = getSuccessRate({ passed: passedCount, failed: totalCount - passedCount });
|
|
43
117
|
state.setResult({ totalCount, passedCount }, testResults);
|
|
44
118
|
return {
|
|
45
119
|
success: rate >= expected,
|
|
@@ -51,8 +125,9 @@ export const successRateRule = {
|
|
|
51
125
|
export const maxDurationRule = {
|
|
52
126
|
rule: "maxDuration",
|
|
53
127
|
message: ({ actual, expected }) => `Maximum duration of some tests exceed the defined limit; actual ${bold(String(actual))}, expected ${bold(String(expected))}`,
|
|
128
|
+
successMessage: ({ actual, expected }) => `Maximum duration of the tests is within the defined limit; actual ${bold(String(actual))}, expected ${bold(String(expected))}`,
|
|
54
129
|
validate: async ({ trs, expected, state }) => {
|
|
55
|
-
const previous = state.getResult()
|
|
130
|
+
const previous = numberStateValue(state.getResult());
|
|
56
131
|
const actual = Math.max(previous, ...trs.map((tr) => tr.duration ?? 0));
|
|
57
132
|
const tooLongTrs = trs.filter((tr) => (tr.duration ?? 0) > expected);
|
|
58
133
|
const testResults = tooLongTrs.map((tr) => tr.id);
|
|
@@ -66,9 +141,10 @@ export const maxDurationRule = {
|
|
|
66
141
|
};
|
|
67
142
|
export const allTestsContainEnvRule = {
|
|
68
143
|
rule: "allTestsContainEnv",
|
|
69
|
-
message: ({ actual, expected }) => `Not all tests contain the required "${bold(expected)}" environment, ${bold(actual)} tests have different or missing environment`,
|
|
144
|
+
message: ({ actual, expected }) => `Not all tests contain the required "${bold(expected)}" environment, ${bold(String(actual))} tests have different or missing environment`,
|
|
145
|
+
successMessage: ({ expected }) => `All tests contain the required "${bold(expected)}" environment`,
|
|
70
146
|
validate: async ({ trs, expected, state }) => {
|
|
71
|
-
const previous = state.getResult()
|
|
147
|
+
const previous = numberStateValue(state.getResult());
|
|
72
148
|
const testsWithoutEnv = trs.filter((tr) => (tr.environment ?? "") !== expected);
|
|
73
149
|
const testResults = testsWithoutEnv.map((tr) => tr.id);
|
|
74
150
|
const actual = testsWithoutEnv.length + previous;
|
|
@@ -83,8 +159,9 @@ export const allTestsContainEnvRule = {
|
|
|
83
159
|
export const environmentsTestedRule = {
|
|
84
160
|
rule: "environmentsTested",
|
|
85
161
|
message: ({ actual, expected }) => `The following environments were not tested: "${actual.join('", "')}"; expected all of: "${expected.join('", "')}"`,
|
|
162
|
+
successMessage: ({ expected }) => `All expected environments were tested: "${expected.join('", "')}"`,
|
|
86
163
|
validate: async ({ trs, expected, state }) => {
|
|
87
|
-
const previouslyTested = new Set(state.getResult()
|
|
164
|
+
const previouslyTested = new Set(stringArrayStateValue(state.getResult()));
|
|
88
165
|
const batchTested = trs.map((tr) => tr.environment).filter((env) => env != null && env !== "");
|
|
89
166
|
const testedEnvs = new Set([...previouslyTested, ...batchTested]);
|
|
90
167
|
state.setResult([...testedEnvs], []);
|
|
@@ -96,6 +173,62 @@ export const environmentsTestedRule = {
|
|
|
96
173
|
};
|
|
97
174
|
},
|
|
98
175
|
};
|
|
176
|
+
export const metricMaxRule = {
|
|
177
|
+
rule: "metricMax",
|
|
178
|
+
message: ({ actual, expected }) => `${bold(metricRuleTitle(actual))} ${bold(formatMetricValue(actual.value, metricRuleUnit(actual, expected)))} exceeds the allowed maximum ${bold(formatMetricValue(expected.value, metricRuleUnit(actual, expected)))}`,
|
|
179
|
+
validate: async ({ metrics = [], expected }) => {
|
|
180
|
+
const value = metricAverage(metrics, expected.key);
|
|
181
|
+
const actual = metricActual(metrics, expected.key, value ?? Number.NaN);
|
|
182
|
+
return value === undefined ? missingMetricResult(actual) : metricRuleResult(value <= expected.value, actual);
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
export const metricMinRule = {
|
|
186
|
+
rule: "metricMin",
|
|
187
|
+
message: ({ actual, expected }) => `${bold(metricRuleTitle(actual))} ${bold(formatMetricValue(actual.value, metricRuleUnit(actual, expected)))} is below the required minimum ${bold(formatMetricValue(expected.value, metricRuleUnit(actual, expected)))}`,
|
|
188
|
+
validate: async ({ metrics = [], expected }) => {
|
|
189
|
+
const value = metricAverage(metrics, expected.key);
|
|
190
|
+
const actual = metricActual(metrics, expected.key, value ?? Number.NaN);
|
|
191
|
+
return value === undefined ? missingMetricResult(actual) : metricRuleResult(value >= expected.value, actual);
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
export const metricMaxDeltaRule = {
|
|
195
|
+
rule: "metricMaxDelta",
|
|
196
|
+
message: ({ actual, expected }) => `${bold(metricRuleTitle(actual))} changed by ${bold(formatMetricValue(actual.value, metricRuleUnit(actual, expected)))}, which exceeds ${bold(formatMetricValue(expected.value, metricRuleUnit(actual, expected)))}`,
|
|
197
|
+
validate: async ({ metrics = [], previousHistory = [], expected }) => {
|
|
198
|
+
const current = metricAverage(metrics, expected.key);
|
|
199
|
+
if (current === undefined) {
|
|
200
|
+
return missingMetricResult(metricActual(metrics, expected.key, Number.NaN));
|
|
201
|
+
}
|
|
202
|
+
const previous = previousMetricValue(previousHistory, expected.key);
|
|
203
|
+
if (previous === undefined) {
|
|
204
|
+
return metricRuleResult(true, metricActual(metrics, expected.key, 0));
|
|
205
|
+
}
|
|
206
|
+
const value = current - previous;
|
|
207
|
+
const actual = metricActual(metrics, expected.key, value);
|
|
208
|
+
const better = metrics.find((metric) => metric.key === expected.key)?.better;
|
|
209
|
+
return metricRuleResult(metricDeltaWithinThreshold(value, expected.value, better), actual);
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
export const metricMaxDeltaPercentRule = {
|
|
213
|
+
rule: "metricMaxDeltaPercent",
|
|
214
|
+
message: ({ actual, expected }) => `${bold(metricRuleTitle(actual))} changed by ${bold(formatMetricValue(actual.value, "%"))}, which exceeds ${bold(formatMetricValue(expected.value, "%"))}`,
|
|
215
|
+
validate: async ({ metrics = [], previousHistory = [], expected }) => {
|
|
216
|
+
const current = metricAverage(metrics, expected.key);
|
|
217
|
+
if (current === undefined) {
|
|
218
|
+
return missingMetricResult(metricActual(metrics, expected.key, Number.NaN));
|
|
219
|
+
}
|
|
220
|
+
const previous = previousMetricValue(previousHistory, expected.key);
|
|
221
|
+
if (previous === undefined) {
|
|
222
|
+
return metricRuleResult(true, metricActual(metrics, expected.key, 0));
|
|
223
|
+
}
|
|
224
|
+
const value = previous !== 0 ? ((current - previous) / Math.abs(previous)) * 100 : Number.NaN;
|
|
225
|
+
const actual = metricActual(metrics, expected.key, value);
|
|
226
|
+
const better = metrics.find((metric) => metric.key === expected.key)?.better;
|
|
227
|
+
return Number.isFinite(value)
|
|
228
|
+
? metricRuleResult(metricDeltaWithinThreshold(value, expected.value, better), actual)
|
|
229
|
+
: missingMetricResult(actual);
|
|
230
|
+
},
|
|
231
|
+
};
|
|
99
232
|
export const qualityGateDefaultRules = [
|
|
100
233
|
maxFailuresRule,
|
|
101
234
|
minTestsCountRule,
|
|
@@ -103,4 +236,8 @@ export const qualityGateDefaultRules = [
|
|
|
103
236
|
maxDurationRule,
|
|
104
237
|
allTestsContainEnvRule,
|
|
105
238
|
environmentsTestedRule,
|
|
239
|
+
metricMaxRule,
|
|
240
|
+
metricMinRule,
|
|
241
|
+
metricMaxDeltaRule,
|
|
242
|
+
metricMaxDeltaPercentRule,
|
|
106
243
|
];
|