allure 3.15.0 → 3.16.1

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.
Files changed (44) hide show
  1. package/README.md +16 -7
  2. package/dist/commands/agent-run.d.ts +1 -0
  3. package/dist/commands/agent-run.js +11 -8
  4. package/dist/commands/agent.d.ts +1 -0
  5. package/dist/commands/agent.js +19 -8
  6. package/dist/commands/allure2.js +10 -10
  7. package/dist/commands/awesome.js +10 -10
  8. package/dist/commands/check.js +1 -1
  9. package/dist/commands/classic.js +10 -10
  10. package/dist/commands/commons/generate.d.ts +1 -1
  11. package/dist/commands/commons/generate.js +7 -3
  12. package/dist/commands/commons/resultsDiscovery.d.ts +5 -0
  13. package/dist/commands/commons/resultsDiscovery.js +17 -0
  14. package/dist/commands/commons/run.d.ts +2 -3
  15. package/dist/commands/commons/run.js +37 -26
  16. package/dist/commands/csv.js +10 -10
  17. package/dist/commands/dashboard.js +9 -9
  18. package/dist/commands/generate.js +5 -4
  19. package/dist/commands/history.js +6 -4
  20. package/dist/commands/index.d.ts +1 -0
  21. package/dist/commands/index.js +1 -0
  22. package/dist/commands/jira/clear.js +1 -1
  23. package/dist/commands/log.js +9 -9
  24. package/dist/commands/open.js +9 -5
  25. package/dist/commands/qualityGate.js +5 -8
  26. package/dist/commands/results/pack.d.ts +1 -0
  27. package/dist/commands/results/pack.js +12 -14
  28. package/dist/commands/run.d.ts +1 -0
  29. package/dist/commands/run.js +20 -10
  30. package/dist/commands/slack.js +9 -9
  31. package/dist/commands/testopsPlan.d.ts +7 -0
  32. package/dist/commands/testopsPlan.js +65 -0
  33. package/dist/commands/testplan.js +7 -5
  34. package/dist/commands/watch.d.ts +1 -0
  35. package/dist/commands/watch.js +71 -25
  36. package/dist/index.js +2 -1
  37. package/dist/utils/index.d.ts +2 -0
  38. package/dist/utils/index.js +2 -0
  39. package/dist/utils/process.js +1 -0
  40. package/dist/utils/resultsPatterns.d.ts +12 -0
  41. package/dist/utils/resultsPatterns.js +37 -0
  42. package/dist/utils/signals.d.ts +18 -0
  43. package/dist/utils/signals.js +52 -0
  44. package/package.json +21 -21
@@ -2,25 +2,26 @@ import * as console from "node:console";
2
2
  import { realpath } from "node:fs/promises";
3
3
  import { rm } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
- import process, { exit } from "node:process";
5
+ import process from "node:process";
6
6
  import { AllureReport, isFileNotFoundError, readConfig } from "@allurereport/core";
7
- import { newFilesInDirectoryWatcher } from "@allurereport/directory-watcher";
7
+ import { allureResultsDirectoriesWatcher, newFilesInDirectoryWatcher } from "@allurereport/directory-watcher";
8
8
  import Awesome from "@allurereport/plugin-awesome";
9
9
  import ProgressPlugin from "@allurereport/plugin-progress";
10
10
  import ServerReloadPlugin from "@allurereport/plugin-server-reload";
11
11
  import { PathResultFile } from "@allurereport/reader-api";
12
12
  import { serve } from "@allurereport/static-server";
13
13
  import { Command, Option } from "clipanion";
14
- import { red } from "yoctocolors";
15
- import { findAllureResultDirectories } from "../utils/fileSystem.js";
14
+ import { resolveResultsPatterns } from "../utils/resultsPatterns.js";
15
+ import { boundedTerminationSignal, notifySignals, waitForAbort } from "../utils/signals.js";
16
+ import { allureResultsDirectoriesGlobWatcher } from "./commons/resultsDiscovery.js";
16
17
  export class WatchCommand extends Command {
17
18
  constructor() {
18
19
  super(...arguments);
19
20
  this.resultsDir = Option.Rest({
20
- name: "Patterns to match test results directories in the current working directory (default: ./**/allure-results)",
21
+ name: "Patterns to match test results directories. Overrides config.resultsDir. Empty patterns use name-based discovery.",
21
22
  });
22
23
  this.config = Option.String("--config,-c", {
23
- description: "The path Allure config file",
24
+ description: "The path to Allure config file",
24
25
  });
25
26
  this.cwd = Option.String("--cwd", {
26
27
  description: "The working directory for the command to run (default: current working directory)",
@@ -40,15 +41,12 @@ export class WatchCommand extends Command {
40
41
  this.preserve = Option.Boolean("--preserve", {
41
42
  description: "Don't clear terminal output on the data refresh",
42
43
  });
44
+ this.newOnly = Option.Boolean("--new-only", true, {
45
+ description: "Skip whatever test results already exist on disk at startup and only react to results written after the watch has started, instead of ingesting the existing backlog first (default: true). Pass --no-new-only to ingest the existing backlog too",
46
+ });
43
47
  }
44
48
  async execute() {
45
49
  const cwd = await realpath(this.cwd ?? process.cwd());
46
- const { resultDirectories, patterns } = await findAllureResultDirectories(cwd, this.resultsDir);
47
- if (!resultDirectories.length) {
48
- console.error(red(`No test results directories found matching pattern: ${patterns}`));
49
- exit(1);
50
- return;
51
- }
52
50
  const before = new Date().getTime();
53
51
  process.on("exit", (code) => {
54
52
  const after = new Date().getTime();
@@ -60,6 +58,8 @@ export class WatchCommand extends Command {
60
58
  open: this.open,
61
59
  port: this.port,
62
60
  });
61
+ const resultsPatterns = resolveResultsPatterns(this.resultsDir ?? [], config.resultsDir);
62
+ const useDynamicNameDiscovery = resultsPatterns.length === 0;
63
63
  try {
64
64
  await rm(config.output, { recursive: true });
65
65
  }
@@ -110,39 +110,85 @@ export class WatchCommand extends Command {
110
110
  });
111
111
  await allureReport.start();
112
112
  const abortFunctions = [];
113
- for (const directory of resultDirectories) {
114
- const { abort } = newFilesInDirectoryWatcher(directory, async (path) => {
115
- await allureReport.readResult(new PathResultFile(path));
116
- });
117
- abortFunctions.push(abort);
118
- }
113
+ const perDirectoryWatchers = new Map();
114
+ let isInitialDiscovery = true;
115
+ const onDiscoveryUpdate = async (newDirectories, deletedDirectories) => {
116
+ for (const deletedDir of deletedDirectories) {
117
+ const watcher = perDirectoryWatchers.get(deletedDir);
118
+ if (watcher) {
119
+ await watcher.abort();
120
+ }
121
+ perDirectoryWatchers.delete(deletedDir);
122
+ }
123
+ for (const newDir of newDirectories) {
124
+ if (perDirectoryWatchers.has(newDir)) {
125
+ continue;
126
+ }
127
+ const watcher = newFilesInDirectoryWatcher(newDir, async (path) => {
128
+ await allureReport.readResult(new PathResultFile(path));
129
+ }, { ignoreInitial: this.newOnly && isInitialDiscovery });
130
+ perDirectoryWatchers.set(newDir, watcher);
131
+ await watcher.initialScan();
132
+ }
133
+ isInitialDiscovery = false;
134
+ };
135
+ const discoveryWatcher = useDynamicNameDiscovery
136
+ ? allureResultsDirectoriesWatcher(cwd, onDiscoveryUpdate)
137
+ : allureResultsDirectoriesGlobWatcher(cwd, resultsPatterns, onDiscoveryUpdate, { indexDelay: 600 });
138
+ await discoveryWatcher.initialScan();
139
+ abortFunctions.push(discoveryWatcher.abort);
140
+ abortFunctions.push(async (immediately) => {
141
+ for (const watcher of perDirectoryWatchers.values()) {
142
+ await watcher.abort(immediately);
143
+ }
144
+ perDirectoryWatchers.clear();
145
+ });
119
146
  const pluginIdToOpen = config.plugins?.find((plugin) => !!plugin.options.open)?.id;
120
147
  if (pluginIdToOpen) {
121
148
  await server.open(join(server.url, pluginIdToOpen));
122
149
  }
123
150
  console.info("Press Ctrl+C to exit");
124
- process.on("SIGINT", async () => {
125
- console.log("");
151
+ const notifier = notifySignals(["SIGINT", "SIGTERM"], (signal) => {
152
+ console.log(`\nreceived another ${signal}, force exiting...`);
153
+ process.exit(130);
154
+ });
155
+ await waitForAbort(notifier.signal);
156
+ const signalInfo = notifier.info();
157
+ console.log(`\nreceived ${signalInfo?.signal}, stopping (press again to force exit)...`);
158
+ const terminationSignal = boundedTerminationSignal(signalInfo, 5000);
159
+ const cleanup = (async () => {
126
160
  for (const abort of abortFunctions) {
127
- await abort();
161
+ await abort(true);
128
162
  }
129
163
  await server.stop();
130
164
  await allureReport.done();
131
- process.exit(0);
132
- });
165
+ })();
166
+ const timedOut = await Promise.race([cleanup.then(() => false), waitForAbort(terminationSignal).then(() => true)]);
167
+ if (timedOut) {
168
+ console.log("shutdown is taking too long, force exiting...");
169
+ process.exit(130);
170
+ }
171
+ notifier.dispose();
172
+ process.exit(signalInfo?.code ?? 0);
133
173
  }
134
174
  }
135
175
  WatchCommand.paths = [["watch"]];
136
176
  WatchCommand.usage = Command.Usage({
137
177
  description: "Watches Allure Results changes in Real-time",
138
- details: "This command watches for changes in the Allure Results directory and updates the report in real-time.",
178
+ details: "This command watches for changes in Allure Results directories and updates the report in real-time. " +
179
+ "CLI patterns and config.resultsDir use live re-glob discovery. " +
180
+ "When both are empty, directories named `allure-results` are discovered dynamically (unlike generate, which defaults to `./**/allure-results`). " +
181
+ "Quote globs in the shell so they are not expanded early.",
139
182
  examples: [
140
183
  ["watch ./allure-results", "Watch for changes in the ./allure-results directory"],
141
184
  [
142
185
  "watch ./allure-results --port 8080",
143
186
  "Watch for changes in the ./allure-results directory and serve the report on port 8080",
144
187
  ],
145
- ["watch ./packages/*/allure-results", "Watch for changes in all Allure result directories matching the pattern"],
188
+ [
189
+ "watch './packages/*/allure-results'",
190
+ "Watch for changes in all Allure result directories matching the pattern",
191
+ ],
146
192
  [
147
193
  "watch ./packages/foo/allure-results ./packages/bar/allure-results",
148
194
  "Watch for changes in two Allure result directories",
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import process, { argv } from "node:process";
3
3
  import { Builtins, Cli } from "clipanion";
4
- import { AgentCommand, AGENT_TASK_MAP_HELP, AgentCapabilitiesCommand, AgentInspectCommand, AgentLatestCommand, AgentQueryCommand, AgentSelectCommand, AgentStateDirCommand, Allure2Command, AwesomeCommand, CheckCommand, ClassicCommand, CsvCommand, DashboardCommand, GenerateCommand, HistoryCommand, JiraClearCommand, LogCommand, OpenCommand, QualityGateCommand, ResultsPackCommand, ResultsUnpackCommand, RunCommand, SlackCommand, TestPlanCommand, WatchCommand, isAgentTaskMapHelpRequest, } from "./commands/index.js";
4
+ import { AgentCommand, AGENT_TASK_MAP_HELP, AgentCapabilitiesCommand, AgentInspectCommand, AgentLatestCommand, AgentQueryCommand, AgentSelectCommand, AgentStateDirCommand, Allure2Command, AwesomeCommand, CheckCommand, ClassicCommand, CsvCommand, DashboardCommand, GenerateCommand, HistoryCommand, JiraClearCommand, LogCommand, OpenCommand, QualityGateCommand, ResultsPackCommand, ResultsUnpackCommand, RunCommand, SlackCommand, TestOpsPlanCommand, TestPlanCommand, WatchCommand, isAgentTaskMapHelpRequest, } from "./commands/index.js";
5
5
  const [node, app, ...args] = argv;
6
6
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
7
7
  const cli = new Cli({
@@ -30,6 +30,7 @@ cli.register(OpenCommand);
30
30
  cli.register(QualityGateCommand);
31
31
  cli.register(RunCommand);
32
32
  cli.register(SlackCommand);
33
+ cli.register(TestOpsPlanCommand);
33
34
  cli.register(TestPlanCommand);
34
35
  cli.register(WatchCommand);
35
36
  cli.register(ResultsPackCommand);
@@ -3,3 +3,5 @@ export * from "./terminal.js";
3
3
  export * from "./logs.js";
4
4
  export * from "./execution-context.js";
5
5
  export * from "./fileSystem.js";
6
+ export * from "./resultsPatterns.js";
7
+ export * from "./signals.js";
@@ -3,3 +3,5 @@ export * from "./terminal.js";
3
3
  export * from "./logs.js";
4
4
  export * from "./execution-context.js";
5
5
  export * from "./fileSystem.js";
6
+ export * from "./resultsPatterns.js";
7
+ export * from "./signals.js";
@@ -74,6 +74,7 @@ export const runProcess = (params) => {
74
74
  };
75
75
  export const terminationOf = (testProcess) => new Promise((resolve) => {
76
76
  testProcess.on("exit", (code) => resolve(code));
77
+ testProcess.on("error", () => resolve(null));
77
78
  });
78
79
  export const stopProcessTree = async (pid, { signal = "SIGTERM" } = {}) => {
79
80
  const tree = new Map();
@@ -0,0 +1,12 @@
1
+ export declare const normalizeResultsDir: (value?: string | readonly string[]) => string[];
2
+ export declare const resolveResultsPatterns: (cliPatterns?: string | readonly string[] | null, configResultsDir?: string | readonly string[]) => string[];
3
+ export declare const normalizeResultsDirectoryPath: (path: string) => string;
4
+ export type ParsedRunCommand = {
5
+ command: string | undefined;
6
+ commandArgs: string[];
7
+ };
8
+ export declare const parseRunCommand: (args: string[]) => ParsedRunCommand;
9
+ export declare const resolveAndFindResultsDirs: (cwd: string, cliPatterns: readonly string[], configResultsDir?: string | string[]) => Promise<{
10
+ patterns: readonly string[];
11
+ resultDirectories: string[];
12
+ }>;
@@ -0,0 +1,37 @@
1
+ import { resolve } from "node:path";
2
+ import { findAllureResultDirectories } from "./fileSystem.js";
3
+ export const normalizeResultsDir = (value) => {
4
+ if (value === undefined) {
5
+ return [];
6
+ }
7
+ const values = typeof value === "string" ? [value] : [...value];
8
+ return values.filter((entry) => entry.length > 0);
9
+ };
10
+ export const resolveResultsPatterns = (cliPatterns, configResultsDir) => {
11
+ const fromCli = normalizeResultsDir(cliPatterns ?? undefined);
12
+ if (fromCli.length) {
13
+ return fromCli;
14
+ }
15
+ return normalizeResultsDir(configResultsDir);
16
+ };
17
+ export const normalizeResultsDirectoryPath = (path) => {
18
+ return resolve(path.replace(/[/\\]+$/, ""));
19
+ };
20
+ export const parseRunCommand = (args) => {
21
+ let start = 0;
22
+ while (start < args.length && args[start] === "--") {
23
+ start += 1;
24
+ }
25
+ const commandParts = args.slice(start);
26
+ if (!commandParts.length) {
27
+ return { command: undefined, commandArgs: [] };
28
+ }
29
+ return {
30
+ command: commandParts[0],
31
+ commandArgs: commandParts.slice(1),
32
+ };
33
+ };
34
+ export const resolveAndFindResultsDirs = async (cwd, cliPatterns, configResultsDir) => {
35
+ const patterns = resolveResultsPatterns(cliPatterns, configResultsDir);
36
+ return findAllureResultDirectories(cwd, patterns);
37
+ };
@@ -0,0 +1,18 @@
1
+ export declare const SIGNAL_EXIT_CODES: Partial<Record<NodeJS.Signals, number>>;
2
+ export declare const GRACEFUL_SHUTDOWN_TIMEOUT_MS = 60000;
3
+ export declare const COMMAND_TERMINATION_GRACE_MS = 5000;
4
+ export type SignalInfo = {
5
+ signal: NodeJS.Signals;
6
+ code: number;
7
+ receivedAt: number;
8
+ deadline: number;
9
+ };
10
+ export type SignalNotifier = {
11
+ signal: AbortSignal;
12
+ info: () => SignalInfo | undefined;
13
+ dispose: () => void;
14
+ };
15
+ export declare const notifySignals: (signals?: NodeJS.Signals[], onRepeat?: (signal: NodeJS.Signals) => void) => SignalNotifier;
16
+ export declare const gracefulShutdownRemaining: (info: SignalInfo | undefined) => number | undefined;
17
+ export declare const boundedTerminationSignal: (info: SignalInfo | undefined, graceMs?: number) => AbortSignal;
18
+ export declare const waitForAbort: (signal: AbortSignal) => Promise<void>;
@@ -0,0 +1,52 @@
1
+ import process from "node:process";
2
+ export const SIGNAL_EXIT_CODES = {
3
+ SIGINT: 130,
4
+ SIGTERM: 143,
5
+ };
6
+ export const GRACEFUL_SHUTDOWN_TIMEOUT_MS = 60000;
7
+ export const COMMAND_TERMINATION_GRACE_MS = 5000;
8
+ const exitCodeForSignal = (signal) => SIGNAL_EXIT_CODES[signal] ?? 1;
9
+ export const notifySignals = (signals = ["SIGINT", "SIGTERM"], onRepeat) => {
10
+ const controller = new AbortController();
11
+ let info;
12
+ const handler = (signal) => {
13
+ if (info) {
14
+ onRepeat?.(signal);
15
+ return;
16
+ }
17
+ const receivedAt = Date.now();
18
+ info = {
19
+ signal,
20
+ code: exitCodeForSignal(signal),
21
+ receivedAt,
22
+ deadline: receivedAt + GRACEFUL_SHUTDOWN_TIMEOUT_MS,
23
+ };
24
+ controller.abort();
25
+ };
26
+ for (const signal of signals) {
27
+ process.on(signal, handler);
28
+ }
29
+ return {
30
+ signal: controller.signal,
31
+ info: () => info,
32
+ dispose: () => {
33
+ for (const signal of signals) {
34
+ process.off(signal, handler);
35
+ }
36
+ },
37
+ };
38
+ };
39
+ export const gracefulShutdownRemaining = (info) => {
40
+ if (!info) {
41
+ return undefined;
42
+ }
43
+ return Math.max(0, info.deadline - Date.now());
44
+ };
45
+ export const boundedTerminationSignal = (info, graceMs = COMMAND_TERMINATION_GRACE_MS) => {
46
+ const remaining = gracefulShutdownRemaining(info);
47
+ const timeoutMs = remaining === undefined ? graceMs : Math.min(remaining, graceMs);
48
+ return AbortSignal.timeout(timeoutMs);
49
+ };
50
+ export const waitForAbort = (signal) => signal.aborted
51
+ ? Promise.resolve()
52
+ : new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "allure",
3
- "version": "3.15.0",
3
+ "version": "3.16.1",
4
4
  "description": "Allure Commandline Tool",
5
5
  "keywords": [
6
6
  "allure",
@@ -32,26 +32,26 @@
32
32
  "lint:fix": "oxlint --import-plugin --fix src test features stories"
33
33
  },
34
34
  "dependencies": {
35
- "@allurereport/charts-api": "3.15.0",
36
- "@allurereport/ci": "3.15.0",
37
- "@allurereport/core": "3.15.0",
38
- "@allurereport/core-api": "3.15.0",
39
- "@allurereport/directory-watcher": "3.15.0",
40
- "@allurereport/plugin-agent": "3.15.0",
41
- "@allurereport/plugin-allure2": "3.15.0",
42
- "@allurereport/plugin-api": "3.15.0",
43
- "@allurereport/plugin-awesome": "3.15.0",
44
- "@allurereport/plugin-classic": "3.15.0",
45
- "@allurereport/plugin-csv": "3.15.0",
46
- "@allurereport/plugin-dashboard": "3.15.0",
47
- "@allurereport/plugin-jira": "3.15.0",
48
- "@allurereport/plugin-log": "3.15.0",
49
- "@allurereport/plugin-progress": "3.15.0",
50
- "@allurereport/plugin-server-reload": "3.15.0",
51
- "@allurereport/plugin-slack": "3.15.0",
52
- "@allurereport/reader-api": "3.15.0",
53
- "@allurereport/service": "3.15.0",
54
- "@allurereport/static-server": "3.15.0",
35
+ "@allurereport/charts-api": "3.16.1",
36
+ "@allurereport/ci": "3.16.1",
37
+ "@allurereport/core": "3.16.1",
38
+ "@allurereport/core-api": "3.16.1",
39
+ "@allurereport/directory-watcher": "3.16.1",
40
+ "@allurereport/plugin-agent": "3.16.1",
41
+ "@allurereport/plugin-allure2": "3.16.1",
42
+ "@allurereport/plugin-api": "3.16.1",
43
+ "@allurereport/plugin-awesome": "3.16.1",
44
+ "@allurereport/plugin-classic": "3.16.1",
45
+ "@allurereport/plugin-csv": "3.16.1",
46
+ "@allurereport/plugin-dashboard": "3.16.1",
47
+ "@allurereport/plugin-jira": "3.16.1",
48
+ "@allurereport/plugin-log": "3.16.1",
49
+ "@allurereport/plugin-progress": "3.16.1",
50
+ "@allurereport/plugin-server-reload": "3.16.1",
51
+ "@allurereport/plugin-slack": "3.16.1",
52
+ "@allurereport/reader-api": "3.16.1",
53
+ "@allurereport/service": "3.16.1",
54
+ "@allurereport/static-server": "3.16.1",
55
55
  "adm-zip": "^0.6.0",
56
56
  "clipanion": "^4.0.0-rc.4",
57
57
  "glob": "^13.0.6",