@allurereport/core 3.0.0-beta.8 → 3.0.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,24 +1,34 @@
1
- import type { HistoryDataPoint, KnownTestFailure } from "@allurereport/core-api";
2
- import type { DefaultLabelsConfig, Plugin, QualityGateConfig, ReportFiles } from "@allurereport/plugin-api";
1
+ import type { DefaultLabelsConfig, EnvironmentsConfig, KnownTestFailure, ReportVariables } from "@allurereport/core-api";
2
+ import type { Plugin, QualityGateConfig, ReportFiles } from "@allurereport/plugin-api";
3
3
  import type { ResultsReader } from "@allurereport/reader-api";
4
+ export interface PluginInstance {
5
+ id: string;
6
+ enabled: boolean;
7
+ plugin: Plugin;
8
+ options: Record<string, any>;
9
+ }
4
10
  export interface FullConfig {
5
11
  name: string;
6
12
  output: string;
13
+ open: boolean;
14
+ port: string | undefined;
15
+ historyPath?: string;
16
+ knownIssuesPath: string;
17
+ defaultLabels?: DefaultLabelsConfig;
18
+ stage?: string;
19
+ environment?: string;
20
+ environments?: EnvironmentsConfig;
21
+ variables?: ReportVariables;
7
22
  reportFiles: ReportFiles;
8
23
  readers?: ResultsReader[];
9
24
  plugins?: PluginInstance[];
10
- history: HistoryDataPoint[];
11
- historyPath: string;
12
25
  appendHistory?: boolean;
13
- knownIssuesPath: string;
14
26
  known?: KnownTestFailure[];
15
- qualityGate?: QualityGateConfig;
16
- defaultLabels?: DefaultLabelsConfig;
17
27
  realTime?: any;
18
- }
19
- export interface PluginInstance {
20
- id: string;
21
- enabled: boolean;
22
- plugin: Plugin;
23
- options: Record<string, any>;
28
+ qualityGate?: QualityGateConfig;
29
+ allureService?: {
30
+ url?: string;
31
+ project?: string;
32
+ accessToken?: string;
33
+ };
24
34
  }
package/dist/config.d.ts CHANGED
@@ -1,18 +1,24 @@
1
1
  import type { Config } from "@allurereport/plugin-api";
2
- import type { FullConfig } from "./api.js";
3
- export declare const getPluginId: (key: string) => string;
4
- export declare const findConfig: (cwd: string, configPath?: string) => Promise<string | undefined>;
2
+ import type { FullConfig, PluginInstance } from "./api.js";
5
3
  export interface ConfigOverride {
6
- name?: string;
7
- output?: string;
8
- historyPath?: string;
9
- knownIssuesPath?: string;
4
+ name?: Config["name"];
5
+ output?: Config["output"];
6
+ open?: Config["open"];
7
+ port?: Config["port"];
8
+ historyPath?: Config["historyPath"];
9
+ knownIssuesPath?: Config["knownIssuesPath"];
10
+ plugins?: Config["plugins"];
10
11
  }
12
+ export declare const getPluginId: (key: string) => string;
13
+ export declare const findConfig: (cwd: string, configPath?: string) => Promise<string | undefined>;
11
14
  export declare const validateConfig: (config: Config) => {
12
15
  valid: boolean;
13
16
  fields: string[];
14
17
  };
15
- export declare const loadConfig: (configPath: string) => Promise<Config>;
18
+ export declare const loadYamlConfig: (configPath: string) => Promise<Config>;
19
+ export declare const loadJsonConfig: (configPath: string) => Promise<Config>;
20
+ export declare const loadJsConfig: (configPath: string) => Promise<Config>;
16
21
  export declare const resolveConfig: (config: Config, override?: ConfigOverride) => Promise<FullConfig>;
17
22
  export declare const readConfig: (cwd?: string, configPath?: string, override?: ConfigOverride) => Promise<FullConfig>;
23
+ export declare const getPluginInstance: (config: FullConfig, predicate: (plugin: PluginInstance) => boolean) => PluginInstance | undefined;
18
24
  export declare const resolvePlugin: (path: string) => Promise<any>;
package/dist/config.js CHANGED
@@ -1,17 +1,24 @@
1
1
  import * as console from "node:console";
2
- import { stat } from "node:fs/promises";
3
- import { resolve } from "node:path";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { extname, resolve } from "node:path";
4
4
  import * as process from "node:process";
5
- import { readHistory } from "./history.js";
5
+ import { parse } from "yaml";
6
6
  import { readKnownIssues } from "./known.js";
7
7
  import { FileSystemReportFiles } from "./plugin.js";
8
8
  import { importWrapper } from "./utils/module.js";
9
9
  import { normalizeImportPath } from "./utils/path.js";
10
+ const CONFIG_FILENAMES = [
11
+ "allurerc.js",
12
+ "allurerc.mjs",
13
+ "allurerc.cjs",
14
+ "allurerc.json",
15
+ "allurerc.yaml",
16
+ "allurerc.yml",
17
+ ];
18
+ const DEFAULT_CONFIG = {};
10
19
  export const getPluginId = (key) => {
11
20
  return key.replace(/^@.*\//, "").replace(/[/\\]/g, "-");
12
21
  };
13
- const configNames = ["allurerc.js", "allurerc.mjs"];
14
- const defaultConfig = {};
15
22
  export const findConfig = async (cwd, configPath) => {
16
23
  if (configPath) {
17
24
  const resolved = resolve(cwd, configPath);
@@ -26,8 +33,8 @@ export const findConfig = async (cwd, configPath) => {
26
33
  }
27
34
  throw new Error(`invalid config path ${resolved}: not a regular file`);
28
35
  }
29
- for (const configName of configNames) {
30
- const resolved = resolve(cwd, configName);
36
+ for (const configFilename of CONFIG_FILENAMES) {
37
+ const resolved = resolve(cwd, configFilename);
31
38
  try {
32
39
  const stats = await stat(resolved);
33
40
  if (stats.isFile()) {
@@ -42,11 +49,17 @@ export const validateConfig = (config) => {
42
49
  const supportedFields = [
43
50
  "name",
44
51
  "output",
52
+ "open",
53
+ "port",
45
54
  "historyPath",
46
55
  "knownIssuesPath",
47
- "qualityGate",
48
56
  "plugins",
49
57
  "defaultLabels",
58
+ "variables",
59
+ "environments",
60
+ "appendHistory",
61
+ "qualityGate",
62
+ "allureService",
50
63
  ];
51
64
  const unsupportedFields = Object.keys(config).filter((key) => !supportedFields.includes(key));
52
65
  return {
@@ -54,7 +67,33 @@ export const validateConfig = (config) => {
54
67
  fields: unsupportedFields,
55
68
  };
56
69
  };
57
- export const loadConfig = async (configPath) => {
70
+ export const loadYamlConfig = async (configPath) => {
71
+ try {
72
+ const rawConfig = await readFile(configPath, "utf-8");
73
+ const parsedConfig = parse(rawConfig);
74
+ return parsedConfig || DEFAULT_CONFIG;
75
+ }
76
+ catch (err) {
77
+ if (err?.code === "ENOENT") {
78
+ return DEFAULT_CONFIG;
79
+ }
80
+ throw err;
81
+ }
82
+ };
83
+ export const loadJsonConfig = async (configPath) => {
84
+ try {
85
+ const rawConfig = await readFile(configPath, "utf-8");
86
+ const parsedConfig = JSON.parse(rawConfig);
87
+ return parsedConfig || DEFAULT_CONFIG;
88
+ }
89
+ catch (err) {
90
+ if (err?.code === "ENOENT") {
91
+ return DEFAULT_CONFIG;
92
+ }
93
+ throw err;
94
+ }
95
+ };
96
+ export const loadJsConfig = async (configPath) => {
58
97
  return (await import(normalizeImportPath(configPath))).default;
59
98
  };
60
99
  export const resolveConfig = async (config, override = {}) => {
@@ -63,12 +102,16 @@ export const resolveConfig = async (config, override = {}) => {
63
102
  throw new Error(`The provided Allure config contains unsupported fields: ${validationResult.fields.join(", ")}`);
64
103
  }
65
104
  const name = override.name ?? config.name ?? "Allure Report";
66
- const historyPath = resolve(override.historyPath ?? config.historyPath ?? "./.allure/history.jsonl");
105
+ const open = override.open ?? config.open ?? false;
106
+ const port = override.port ?? config.port ?? undefined;
107
+ const historyPath = override.historyPath ?? config.historyPath;
108
+ const appendHistory = config.appendHistory ?? true;
67
109
  const knownIssuesPath = resolve(override.knownIssuesPath ?? config.knownIssuesPath ?? "./allure/known.json");
68
110
  const output = resolve(override.output ?? config.output ?? "./allure-report");
69
- const history = await readHistory(historyPath);
70
111
  const known = await readKnownIssues(knownIssuesPath);
71
- const plugins = Object.keys(config?.plugins ?? {}).length === 0
112
+ const variables = config.variables ?? {};
113
+ const environments = config.environments ?? {};
114
+ const plugins = Object.keys(override?.plugins ?? config?.plugins ?? {}).length === 0
72
115
  ? {
73
116
  awesome: {
74
117
  options: {},
@@ -78,20 +121,46 @@ export const resolveConfig = async (config, override = {}) => {
78
121
  const pluginInstances = await resolvePlugins(plugins);
79
122
  return {
80
123
  name,
81
- reportFiles: new FileSystemReportFiles(output),
82
- plugins: pluginInstances,
83
124
  output,
84
- history,
85
- historyPath,
125
+ open,
126
+ port,
86
127
  knownIssuesPath,
87
128
  known,
129
+ variables,
130
+ environments,
131
+ appendHistory,
132
+ historyPath: historyPath ? resolve(historyPath) : undefined,
133
+ reportFiles: new FileSystemReportFiles(output),
134
+ plugins: pluginInstances,
135
+ defaultLabels: config.defaultLabels ?? {},
88
136
  qualityGate: config.qualityGate,
137
+ allureService: config.allureService,
89
138
  };
90
139
  };
91
140
  export const readConfig = async (cwd = process.cwd(), configPath, override) => {
92
- const cfg = await findConfig(cwd, configPath);
93
- const config = cfg ? await loadConfig(cfg) : { ...defaultConfig };
94
- return await resolveConfig(config, override);
141
+ const cfg = (await findConfig(cwd, configPath)) ?? "";
142
+ let config;
143
+ switch (extname(cfg)) {
144
+ case ".json":
145
+ config = await loadJsonConfig(cfg);
146
+ break;
147
+ case ".yaml":
148
+ case ".yml":
149
+ config = await loadYamlConfig(cfg);
150
+ break;
151
+ case ".js":
152
+ case ".cjs":
153
+ case ".mjs":
154
+ config = await loadJsConfig(cfg);
155
+ break;
156
+ default:
157
+ config = DEFAULT_CONFIG;
158
+ }
159
+ const fullConfig = await resolveConfig(config, override);
160
+ return fullConfig;
161
+ };
162
+ export const getPluginInstance = (config, predicate) => {
163
+ return config?.plugins?.find(predicate);
95
164
  };
96
165
  export const resolvePlugin = async (path) => {
97
166
  if (!path.startsWith("@allurereport/plugin-")) {
@@ -113,9 +182,10 @@ const resolvePlugins = async (plugins) => {
113
182
  const pluginInstances = [];
114
183
  for (const id in plugins) {
115
184
  const pluginConfig = plugins[id];
185
+ const pluginId = getPluginId(id);
116
186
  const Plugin = await resolvePlugin(pluginConfig.import ?? id);
117
187
  pluginInstances.push({
118
- id: getPluginId(id),
188
+ id: pluginId,
119
189
  enabled: pluginConfig.enabled ?? true,
120
190
  options: pluginConfig.options ?? {},
121
191
  plugin: new Plugin(pluginConfig.options),
package/dist/history.d.ts CHANGED
@@ -1,4 +1,9 @@
1
- import type { HistoryDataPoint, TestCase, TestResult } from "@allurereport/core-api";
2
- export declare const createHistory: (reportUuid: string, reportName: string | undefined, testCases: TestCase[], testResults: TestResult[]) => HistoryDataPoint;
3
- export declare const readHistory: (historyPath: string) => Promise<HistoryDataPoint[]>;
1
+ import type { AllureHistory, HistoryDataPoint, TestCase, TestResult } from "@allurereport/core-api";
2
+ export declare const createHistory: (reportUuid: string, reportName: string | undefined, testCases: TestCase[], testResults: TestResult[], remoteUrl?: string) => HistoryDataPoint;
4
3
  export declare const writeHistory: (historyPath: string, data: HistoryDataPoint) => Promise<void>;
4
+ export declare class AllureLocalHistory implements AllureHistory {
5
+ private readonly historyPath;
6
+ constructor(historyPath: string);
7
+ readHistory(): Promise<HistoryDataPoint[]>;
8
+ appendHistory(data: HistoryDataPoint): Promise<void>;
9
+ }
package/dist/history.js CHANGED
@@ -4,27 +4,30 @@ import { isFileNotFoundError } from "./utils/misc.js";
4
4
  const createHistoryItems = (testResults) => {
5
5
  return testResults
6
6
  .filter((tr) => tr.historyId)
7
- .map(({ id, name, fullName, historyId, status, error: { message, trace } = {}, start, stop, duration }) => {
7
+ .map(({ id, name, fullName, environment, historyId, status, error: { message, trace } = {}, start, stop, duration, labels, }) => {
8
8
  return {
9
9
  id,
10
10
  name,
11
11
  fullName,
12
+ environment,
12
13
  status,
13
14
  message,
14
15
  trace,
15
16
  start,
16
17
  stop,
17
18
  duration,
19
+ labels,
20
+ url: "",
18
21
  historyId: historyId,
19
22
  reportLinks: [],
20
23
  };
21
24
  })
22
- .reduce((previousValue, currentValue) => {
23
- previousValue[currentValue.historyId] = currentValue;
24
- return previousValue;
25
+ .reduce((acc, item) => {
26
+ acc[item.historyId] = item;
27
+ return acc;
25
28
  }, {});
26
29
  };
27
- export const createHistory = (reportUuid, reportName = "Allure Report", testCases, testResults) => {
30
+ export const createHistory = (reportUuid, reportName = "Allure Report", testCases, testResults, remoteUrl = "") => {
28
31
  const knownTestCaseIds = testCases.map((tc) => tc.id);
29
32
  return {
30
33
  uuid: reportUuid,
@@ -33,26 +36,38 @@ export const createHistory = (reportUuid, reportName = "Allure Report", testCase
33
36
  knownTestCaseIds,
34
37
  testResults: createHistoryItems(testResults),
35
38
  metrics: {},
39
+ url: remoteUrl,
36
40
  };
37
41
  };
38
- export const readHistory = async (historyPath) => {
39
- const path = resolve(historyPath);
40
- try {
41
- return (await readFile(path, { encoding: "utf-8" }))
42
- .split("\n")
43
- .filter((line) => line && line.trim() !== "")
44
- .map((line) => JSON.parse(line));
45
- }
46
- catch (e) {
47
- if (isFileNotFoundError(e)) {
48
- return [];
49
- }
50
- throw e;
51
- }
52
- };
53
42
  export const writeHistory = async (historyPath, data) => {
54
43
  const path = resolve(historyPath);
55
44
  const parentDir = dirname(path);
56
45
  await mkdir(parentDir, { recursive: true });
57
46
  await writeFile(path, `${JSON.stringify(data)}\n`, { encoding: "utf-8", flag: "a+" });
58
47
  };
48
+ export class AllureLocalHistory {
49
+ constructor(historyPath) {
50
+ this.historyPath = historyPath;
51
+ }
52
+ async readHistory() {
53
+ const path = resolve(this.historyPath);
54
+ try {
55
+ return (await readFile(path, { encoding: "utf-8" }))
56
+ .split("\n")
57
+ .filter((line) => line && line.trim() !== "")
58
+ .map((line) => JSON.parse(line));
59
+ }
60
+ catch (e) {
61
+ if (isFileNotFoundError(e)) {
62
+ return [];
63
+ }
64
+ throw e;
65
+ }
66
+ }
67
+ async appendHistory(data) {
68
+ const path = resolve(this.historyPath);
69
+ const parentDir = dirname(path);
70
+ await mkdir(parentDir, { recursive: true });
71
+ await writeFile(path, `${JSON.stringify(data)}\n`, { encoding: "utf-8", flag: "a+" });
72
+ }
73
+ }
package/dist/index.d.ts CHANGED
@@ -2,9 +2,11 @@ export type * from "./api.js";
2
2
  export * from "./utils/misc.js";
3
3
  export * from "./utils/crypto.js";
4
4
  export * from "./utils/path.js";
5
+ export * from "./utils/new.js";
6
+ export * from "./utils/flaky.js";
5
7
  export * from "./history.js";
6
8
  export * from "./known.js";
7
- export { resolveConfig, readConfig } from "./config.js";
9
+ export { resolveConfig, readConfig, getPluginInstance } from "./config.js";
8
10
  export * from "./report.js";
9
11
  export * from "./plugin.js";
10
- export * from "./qualityGate.js";
12
+ export { QualityGateState, qualityGateDefaultRules, maxFailuresRule, minTestsCountRule, successRateRule, convertQualityGateResultsToTestErrors, stringifyQualityGateResults, } from "./qualityGate/index.js";
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  export * from "./utils/misc.js";
2
2
  export * from "./utils/crypto.js";
3
3
  export * from "./utils/path.js";
4
+ export * from "./utils/new.js";
5
+ export * from "./utils/flaky.js";
4
6
  export * from "./history.js";
5
7
  export * from "./known.js";
6
- export { resolveConfig, readConfig } from "./config.js";
8
+ export { resolveConfig, readConfig, getPluginInstance } from "./config.js";
7
9
  export * from "./report.js";
8
10
  export * from "./plugin.js";
9
- export * from "./qualityGate.js";
11
+ export { QualityGateState, qualityGateDefaultRules, maxFailuresRule, minTestsCountRule, successRateRule, convertQualityGateResultsToTestErrors, stringifyQualityGateResults, } from "./qualityGate/index.js";
package/dist/plugin.d.ts CHANGED
@@ -3,20 +3,21 @@ export declare class DefaultPluginState implements PluginState {
3
3
  #private;
4
4
  constructor(state: Record<string, any>);
5
5
  set: (key: string, value: any) => Promise<void>;
6
- get: (key: string) => Promise<void>;
6
+ get: <T>(key: string) => Promise<T>;
7
7
  unset: (key: string) => Promise<void>;
8
8
  }
9
9
  export declare class PluginFiles implements ReportFiles {
10
10
  #private;
11
- constructor(parent: ReportFiles, pluginId: string);
12
- addFile: (key: string, data: Buffer) => Promise<void>;
11
+ readonly callback?: ((key: string, path: string) => void) | undefined;
12
+ constructor(parent: ReportFiles, pluginId: string, callback?: ((key: string, path: string) => void) | undefined);
13
+ addFile: (key: string, data: Buffer) => Promise<string>;
13
14
  }
14
15
  export declare class InMemoryReportFiles implements ReportFiles {
15
16
  #private;
16
- addFile: (path: string, data: Buffer) => Promise<void>;
17
+ addFile: (path: string, data: Buffer) => Promise<string>;
17
18
  }
18
19
  export declare class FileSystemReportFiles implements ReportFiles {
19
20
  #private;
20
21
  constructor(output: string);
21
- addFile: (path: string, data: Buffer) => Promise<void>;
22
+ addFile: (path: string, data: Buffer) => Promise<string>;
22
23
  }
package/dist/plugin.js CHANGED
@@ -11,7 +11,8 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
11
11
  };
12
12
  var _DefaultPluginState_state, _PluginFiles_parent, _PluginFiles_pluginId, _InMemoryReportFiles_state, _FileSystemReportFiles_output;
13
13
  import { mkdir, writeFile } from "node:fs/promises";
14
- import { dirname, join, resolve } from "node:path";
14
+ import { dirname, resolve } from "node:path";
15
+ import { join as joinPosix } from "node:path/posix";
15
16
  export class DefaultPluginState {
16
17
  constructor(state) {
17
18
  _DefaultPluginState_state.set(this, void 0);
@@ -29,11 +30,14 @@ export class DefaultPluginState {
29
30
  }
30
31
  _DefaultPluginState_state = new WeakMap();
31
32
  export class PluginFiles {
32
- constructor(parent, pluginId) {
33
+ constructor(parent, pluginId, callback) {
34
+ this.callback = callback;
33
35
  _PluginFiles_parent.set(this, void 0);
34
36
  _PluginFiles_pluginId.set(this, void 0);
35
37
  this.addFile = async (key, data) => {
36
- await __classPrivateFieldGet(this, _PluginFiles_parent, "f").addFile(join(__classPrivateFieldGet(this, _PluginFiles_pluginId, "f"), key), data);
38
+ const filepath = await __classPrivateFieldGet(this, _PluginFiles_parent, "f").addFile(joinPosix(__classPrivateFieldGet(this, _PluginFiles_pluginId, "f"), key), data);
39
+ this.callback?.(key, filepath);
40
+ return filepath;
37
41
  };
38
42
  __classPrivateFieldSet(this, _PluginFiles_parent, parent, "f");
39
43
  __classPrivateFieldSet(this, _PluginFiles_pluginId, pluginId, "f");
@@ -45,6 +49,7 @@ export class InMemoryReportFiles {
45
49
  _InMemoryReportFiles_state.set(this, {});
46
50
  this.addFile = async (path, data) => {
47
51
  __classPrivateFieldGet(this, _InMemoryReportFiles_state, "f")[path] = data;
52
+ return path;
48
53
  };
49
54
  }
50
55
  }
@@ -57,6 +62,7 @@ export class FileSystemReportFiles {
57
62
  const targetDirPath = dirname(targetPath);
58
63
  await mkdir(targetDirPath, { recursive: true });
59
64
  await writeFile(targetPath, data, { encoding: "utf-8" });
65
+ return targetPath;
60
66
  };
61
67
  __classPrivateFieldSet(this, _FileSystemReportFiles_output, resolve(output), "f");
62
68
  }
@@ -0,0 +1,2 @@
1
+ export * from "./qualityGate.js";
2
+ export * from "./rules.js";
@@ -0,0 +1,2 @@
1
+ export * from "./qualityGate.js";
2
+ export * from "./rules.js";
@@ -0,0 +1,21 @@
1
+ import type { KnownTestFailure, TestError, TestResult } from "@allurereport/core-api";
2
+ import type { QualityGateConfig, QualityGateValidationResult } from "@allurereport/plugin-api";
3
+ export declare const stringifyQualityGateResults: (results: QualityGateValidationResult[]) => string;
4
+ export declare const convertQualityGateResultsToTestErrors: (results: QualityGateValidationResult[]) => TestError[];
5
+ export declare class QualityGateState {
6
+ #private;
7
+ setResult(rule: string, value: any): void;
8
+ getResult(rule: string): any;
9
+ }
10
+ export declare class QualityGate {
11
+ private readonly config;
12
+ constructor(config: QualityGateConfig);
13
+ validate(payload: {
14
+ state?: QualityGateState;
15
+ trs: TestResult[];
16
+ knownIssues: KnownTestFailure[];
17
+ }): Promise<{
18
+ fastFailed: boolean;
19
+ results: QualityGateValidationResult[];
20
+ }>;
21
+ }
@@ -0,0 +1,103 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
6
+ var _QualityGateState_state;
7
+ import { gray, red } from "yoctocolors";
8
+ import { qualityGateDefaultRules } from "./rules.js";
9
+ export const stringifyQualityGateResults = (results) => {
10
+ if (results.length === 0) {
11
+ return "";
12
+ }
13
+ const lines = [red("Quality Gate failed with following issues:")];
14
+ const maxMessageLength = Math.max(...results.map((r) => r.message.length));
15
+ lines.push("");
16
+ results.forEach((result) => {
17
+ lines.push(` ${red("⨯")} ${result.message.padEnd(maxMessageLength, " ")} ${gray(result.rule)}`);
18
+ });
19
+ lines.push("");
20
+ lines.push(red(`${results.length} quality gate rules have been failed.`));
21
+ return lines.join("\n");
22
+ };
23
+ export const convertQualityGateResultsToTestErrors = (results) => {
24
+ return results.map((result) => ({
25
+ message: `Quality Gate (${result.rule}): ${result.message}`,
26
+ actual: result.actual,
27
+ expected: result.expected,
28
+ }));
29
+ };
30
+ export class QualityGateState {
31
+ constructor() {
32
+ _QualityGateState_state.set(this, {});
33
+ }
34
+ setResult(rule, value) {
35
+ __classPrivateFieldGet(this, _QualityGateState_state, "f")[rule] = value;
36
+ }
37
+ getResult(rule) {
38
+ return __classPrivateFieldGet(this, _QualityGateState_state, "f")[rule];
39
+ }
40
+ }
41
+ _QualityGateState_state = new WeakMap();
42
+ export class QualityGate {
43
+ constructor(config) {
44
+ this.config = config;
45
+ }
46
+ async validate(payload) {
47
+ const { state, trs, knownIssues } = payload;
48
+ const { rules, use = [...qualityGateDefaultRules] } = this.config;
49
+ const results = [];
50
+ let fastFailed = false;
51
+ if (!rules?.length) {
52
+ return {
53
+ fastFailed: false,
54
+ results,
55
+ };
56
+ }
57
+ for (const ruleset of rules) {
58
+ if (fastFailed) {
59
+ break;
60
+ }
61
+ for (const [key, expected] of Object.entries(ruleset)) {
62
+ if (key === "filter" || key === "id" || key === "fastFail") {
63
+ continue;
64
+ }
65
+ const rule = use.filter((r) => r.rule === key).pop();
66
+ if (!rule) {
67
+ throw new Error(`Rule ${key} is not provided. Make sure you have provided it in the "use" field of the quality gate config!`);
68
+ }
69
+ const trsToValidate = ruleset.filter ? trs.filter(ruleset.filter) : trs;
70
+ const ruleId = ruleset.id ? [ruleset.id, rule.rule].join("/") : rule.rule;
71
+ const result = await rule.validate({
72
+ trs: trsToValidate,
73
+ state: {
74
+ getResult: () => state?.getResult?.(ruleId),
75
+ setResult: (value) => state?.setResult?.(ruleId, value),
76
+ },
77
+ expected,
78
+ knownIssues,
79
+ });
80
+ if (result.success) {
81
+ continue;
82
+ }
83
+ results.push({
84
+ ...result,
85
+ expected,
86
+ rule: ruleset.id ? [ruleset.id, rule.rule].join("/") : rule.rule,
87
+ message: rule.message({
88
+ actual: result.actual,
89
+ expected,
90
+ }),
91
+ });
92
+ if (ruleset.fastFail) {
93
+ fastFailed = true;
94
+ break;
95
+ }
96
+ }
97
+ }
98
+ return {
99
+ fastFailed,
100
+ results,
101
+ };
102
+ }
103
+ }
@@ -0,0 +1,6 @@
1
+ import { type QualityGateRule } from "@allurereport/plugin-api";
2
+ export declare const maxFailuresRule: QualityGateRule<number>;
3
+ export declare const minTestsCountRule: QualityGateRule<number>;
4
+ export declare const successRateRule: QualityGateRule<number>;
5
+ export declare const maxDurationRule: QualityGateRule<number>;
6
+ export declare const qualityGateDefaultRules: QualityGateRule<number>[];
@@ -0,0 +1,55 @@
1
+ import { filterSuccessful, filterUnsuccessful } from "@allurereport/core-api";
2
+ import { bold } from "yoctocolors";
3
+ export const maxFailuresRule = {
4
+ rule: "maxFailures",
5
+ message: ({ actual, expected }) => `The number of failed tests ${bold(String(actual))} exceeds the allowed threshold value ${bold(String(expected))}`,
6
+ validate: async ({ trs, knownIssues, expected, state }) => {
7
+ const knownIssuesHistoryIds = knownIssues.map(({ historyId }) => historyId);
8
+ const unknown = trs.filter((tr) => !tr.historyId || !knownIssuesHistoryIds.includes(tr.historyId));
9
+ const failedTrs = unknown.filter(filterUnsuccessful);
10
+ const actual = failedTrs.length + (state.getResult() ?? 0);
11
+ state.setResult(actual);
12
+ return {
13
+ success: actual <= expected,
14
+ actual,
15
+ };
16
+ },
17
+ };
18
+ export const minTestsCountRule = {
19
+ rule: "minTestsCount",
20
+ message: ({ actual, expected }) => `The total number of tests ${bold(String(actual))} is less than the expected threshold value ${bold(String(expected))}`,
21
+ validate: async ({ trs, expected, state }) => {
22
+ const actual = trs.length + (state.getResult() ?? 0);
23
+ state.setResult(actual);
24
+ return {
25
+ success: actual >= expected,
26
+ actual,
27
+ };
28
+ },
29
+ };
30
+ export const successRateRule = {
31
+ rule: "successRate",
32
+ message: ({ actual, expected }) => `Success rate ${bold(String(actual))} is less, than expected ${bold(String(expected))}`,
33
+ validate: async ({ trs, knownIssues, expected }) => {
34
+ const knownIssuesHistoryIds = knownIssues.map(({ historyId }) => historyId);
35
+ const unknown = trs.filter((tr) => !tr.historyId || !knownIssuesHistoryIds.includes(tr.historyId));
36
+ const passedTrs = unknown.filter(filterSuccessful);
37
+ const rate = passedTrs.length === 0 ? 0 : passedTrs.length / unknown.length;
38
+ return {
39
+ success: rate >= expected,
40
+ actual: rate,
41
+ };
42
+ },
43
+ };
44
+ export const maxDurationRule = {
45
+ rule: "maxDuration",
46
+ message: ({ actual, expected }) => `Maximum duration of some tests exceed the defined limit; actual ${bold(String(actual))}, expected ${bold(String(expected))}`,
47
+ validate: async ({ trs, expected }) => {
48
+ const actual = Math.max(...trs.map((tr) => tr.duration ?? 0));
49
+ return {
50
+ success: actual <= expected,
51
+ actual,
52
+ };
53
+ },
54
+ };
55
+ export const qualityGateDefaultRules = [maxFailuresRule, minTestsCountRule, successRateRule, maxDurationRule];