@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 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: parseIntegerConfigValue(config.allureService.uploadConcurrency, DEFAULT_ALLURE_SERVICE_UPLOAD_CONCURRENCY, 1),
259
- uploadMaxAttempts: parseIntegerConfigValue(config.allureService.uploadMaxAttempts, DEFAULT_ALLURE_SERVICE_UPLAOD_MAX_ATTEMPTS, 1),
260
- uploadMaxSimultaneousFailures: parseIntegerConfigValue(config.allureService.uploadMaxSimultaneousFailures, DEFAULT_ALLURE_SERVICE_UPLOAD_MAX_SIMULTANEOUS_FAILURES, 0),
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 createHistory: (reportUuid: string, reportName: string | undefined, testCases: TestCase[], testResults: TestResult[], remoteUrl?: string) => HistoryDataPoint;
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
- export const createHistory = (reportUuid, reportName = "Allure Report", testCases, testResults, remoteUrl = "") => {
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
- const dst = historyFile.createWriteStream({ encoding: "utf-8", start: 0, autoClose: false });
169
- if (limit === 0 && historyExists) {
170
- await historyFile.truncate(0);
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
- await historyFile.close();
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: any, testResults?: string[]): void;
8
- getResult(rule: string): any;
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
- if (results.length === 0) {
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(...results.map((r) => r.message.length));
19
+ const maxMessageLength = Math.max(...failedResults.map((r) => r.message.length));
16
20
  lines.push("");
17
- results.forEach((result) => {
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(`${results.length} quality gate rules have been failed.`));
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
- if (result.success) {
104
- continue;
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: rule.message({
111
- actual: result.actual,
112
- expected,
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 qualityGateDefaultRules: (QualityGateRule<number> | QualityGateRule<number, {
12
- totalCount: number;
13
- passedCount: number;
14
- }> | QualityGateRule<string, number> | QualityGateRule<string[]>)[];
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() ?? 0;
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() ?? 0);
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() ?? { totalCount: 0, passedCount: 0 };
37
- const passedTrs = trs.filter(filterSuccessful);
38
- const notPassedTrs = trs.filter((tr) => !filterSuccessful(tr));
39
- const totalCount = previous.totalCount + trs.length;
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 = totalCount === 0 ? 0 : passedCount / totalCount;
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() ?? 0;
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() ?? 0;
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
  ];