@allurereport/plugin-testops 3.21.0 → 3.22.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/README.md CHANGED
@@ -77,7 +77,7 @@ The plugin accepts the following options:
77
77
  | `accessToken` | Access token for Allure TestOps API | `string` | `undefined` |
78
78
  | `endpoint` | Allure TestOps API endpoint | `string` | `undefined` |
79
79
  | `projectId` | Allure TestOps project ID | `string` | `undefined` |
80
- | `autocloseLaunch` | When `true` (default), the launch is closed automatically when the plugin finishes; set to `false` to keep the launch open | `boolean` | `true` |
80
+ | `autocloseLaunch` | When `true` (default), the launch is closed automatically when the plugin finishes, unless some uploads failed; set to `false` to keep the launch open | `boolean` | `true` |
81
81
  | `gitFlow` | When `true`, collect Git metadata for Allure TestOps Git Flow on CI uploads (opt-in) | `boolean` | `false` |
82
82
  | `ancestorLimit` | How many ancestor commits to attach to the launch for history linking in Allure TestOps | `number` | `100` |
83
83
  | `uploadRateLimit` | Caps how fast uploads are sent to Allure TestOps within a rolling time window (requests/files/bytes per window); pass `false` to disable pacing entirely | `{ windowMs: number; maxRequestsPerWindow?: number; maxFilesPerWindow?: number; maxBytesPerWindow?: number } \| false` | `20` req/s, `1000` files/s, `1 GiB`/s |
@@ -97,4 +97,47 @@ The plugin automatically reads the following environment variables and uses them
97
97
  | `ALLURE_GIT_FLOW` | `gitFlow` |
98
98
  | `ALLURE_GIT_ANCESTOR_LIMIT` | `ancestorLimit` |
99
99
 
100
- `ALLURE_TESTOPS_ENABLED` and `CI` are not configuration options: they only control whether upload runs when no CI is detected. See [CI and local runs](#ci-and-local-runs).
100
+ `ALLURE_TESTOPS_ENABLED` and `CI` are not configuration options: they only control whether upload runs when no CI is detected. See [CI and local runs](#ci-and-local-runs).
101
+
102
+ ## Environment variables
103
+
104
+ Every upload session sends environment variables to Allure TestOps, which maps them to the project's env vars. A variable shows up on test results only if its key matches a key of the project's env var schema in Allure TestOps exactly, including case: a schema key `browser` does not match a variable named `Browser`. Other keys are ignored.
105
+
106
+ The plugin sends, from weakest to strongest:
107
+
108
+ 1. the environment variables of the process (for example, the ones set by your CI);
109
+ 2. the report-level `variables` from the Allure configuration;
110
+ 3. the `variables` of the environment (from `environments`) the test result belongs to.
111
+
112
+ A stronger source overrides a weaker one when the keys are the same. Keys that differ only in case are different variables and are all sent. Configuration variables without a value (for example, `process.env.STAND` when `STAND` is unset, or an empty string) and values that aren't strings, numbers or booleans are not sent, so they don't override anything: an environment variable without a value keeps the report-level value.
113
+
114
+ ```js
115
+ import { defineConfig } from "allure";
116
+
117
+ export default defineConfig({
118
+ variables: {
119
+ Stand: process.env.STAND ?? "staging",
120
+ },
121
+ environments: {
122
+ chrome: {
123
+ matcher: ({ labels }) => labels.some(({ name, value }) => name === "browser" && value === "chrome"),
124
+ variables: { browser: "chrome" },
125
+ },
126
+ firefox: {
127
+ matcher: ({ labels }) => labels.some(({ name, value }) => name === "browser" && value === "firefox"),
128
+ variables: { browser: "firefox" },
129
+ },
130
+ },
131
+ plugins: {
132
+ testops: {
133
+ options: {},
134
+ },
135
+ },
136
+ });
137
+ ```
138
+
139
+ Test results of an environment that defines its own `variables` are uploaded in a separate upload session carrying those variables; the plugin creates it once per environment and reuses it for the whole run. Other test results go into the upload session the plugin creates for each upload, together with global attachments and errors.
140
+
141
+ Report-level `variables` are the same for every test result. When a report is generated from several dumps (`allure generate --dump …`), they are merged and the last dump wins. Environment `variables` come from the configuration used to generate the report, so values that differ between dumps, such as the browser of a matrix job, belong in the `variables` of environments defined in that configuration rather than in report-level `variables`.
142
+
143
+ Env vars are part of a test result's identity in Allure TestOps. If your project's env var schema contains keys that you now set in `variables`, the history of the affected tests starts anew once after upgrading the plugin.
package/dist/client.d.ts CHANGED
@@ -4,7 +4,7 @@ import type { QualityGateValidationResult } from "@allurereport/plugin-api";
4
4
  import { AxiosError, type AxiosResponse } from "axios";
5
5
  import type { RetryOptions } from "./errors.js";
6
6
  import type { LaunchGitContextDto } from "./gitFlow/index.js";
7
- import type { AttachmentForUpload, AttachmentsResolver, FixtureResolver, LaunchCategoryBulkItem, LaunchCategoryBulkResult, TestOpsClientParams, TestOpsNamedEnv, TestOpsPluginTestResult } from "./model.js";
7
+ import type { AttachmentForUpload, AttachmentsResolver, FixtureResolver, LaunchCategoryBulkItem, LaunchCategoryBulkResult, TestOpsClientParams, TestOpsNamedEnv, TestOpsPluginTestResult, TestOpsSession } from "./model.js";
8
8
  declare class TestOpsClientError extends AxiosError<{
9
9
  message: string;
10
10
  timestamp: number;
@@ -31,7 +31,9 @@ export declare class TestOpsClient {
31
31
  createLaunch(launchName: string, launchTags: string[], gitContext?: LaunchGitContextDto): Promise<void>;
32
32
  attachToLaunch(launchId: number): void;
33
33
  checkLaunchProgress(): Promise<boolean>;
34
- createSession(environment?: Record<string, unknown>, reopenClosedLaunch?: boolean): Promise<void>;
34
+ ensureLaunchOpen(reopenClosedLaunch?: boolean): Promise<void>;
35
+ createSession(environment?: Record<string, unknown>, reopenClosedLaunch?: boolean): Promise<TestOpsSession>;
36
+ createSessionInOpenLaunch(environment?: Record<string, unknown>): Promise<TestOpsSession>;
35
37
  getNamedEnvFor(id: string): TestOpsNamedEnv | undefined;
36
38
  createNamedEnvs(environments: EnvironmentIdentity[], onProgress?: (percent: number, total: number) => void): Promise<void>;
37
39
  uploadGlobalAttachments(params: {
@@ -48,6 +50,7 @@ export declare class TestOpsClient {
48
50
  onProgress?: () => void;
49
51
  onRetry?: RetryOptions["onRetry"];
50
52
  onChunkUploaded?: (uploadedChunkTrs: TestResult[]) => void;
53
+ session: TestOpsSession;
51
54
  }): Promise<TestResult[]>;
52
55
  uploadQualityGateResults(results: QualityGateValidationResult[], onProgress?: (percent: number, total: number) => void): Promise<void>;
53
56
  }
package/dist/client.js CHANGED
@@ -9,7 +9,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  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");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _TestOpsClient_instances, _TestOpsClient_baseUrl, _TestOpsClient_logger, _TestOpsClient_accessToken, _TestOpsClient_projectId, _TestOpsClient_client, _TestOpsClient_launch, _TestOpsClient_jobRunId, _TestOpsClient_session, _TestOpsClient_uploadInProgress, _TestOpsClient_uploadLimit, _TestOpsClient_uploadPacer, _TestOpsClient_namedEnvsIdsByEnv, _TestOpsClient_getExistingJob, _TestOpsClient_postTestResultsChunk, _TestOpsClient_uploadChunkAttachmentsAndFixtures, _TestOpsClient_uploadAttachmentsForResult, _TestOpsClient_logResultUploadFailure, _TestOpsClient_uploadFixturesForResult;
12
+ var _TestOpsClient_instances, _TestOpsClient_baseUrl, _TestOpsClient_logger, _TestOpsClient_accessToken, _TestOpsClient_projectId, _TestOpsClient_client, _TestOpsClient_launch, _TestOpsClient_jobRunId, _TestOpsClient_session, _TestOpsClient_uploadInProgress, _TestOpsClient_uploadLimit, _TestOpsClient_uploadPacer, _TestOpsClient_namedEnvsIdsByEnv, _TestOpsClient_getExistingJob, _TestOpsClient_namedEnvByIdOrName, _TestOpsClient_postTestResultsChunk, _TestOpsClient_uploadChunkAttachmentsAndFixtures, _TestOpsClient_uploadAttachmentsForResult, _TestOpsClient_logResultUploadFailure, _TestOpsClient_uploadFixturesForResult;
13
13
  import { isLocalCiDescriptor } from "@allurereport/ci";
14
14
  import { KnownError, createServiceHttpClient } from "@allurereport/service";
15
15
  import { AxiosError, isAxiosError } from "axios";
@@ -221,7 +221,7 @@ export class TestOpsClient {
221
221
  const data = await retryRequest(() => __classPrivateFieldGet(this, _TestOpsClient_client, "f").get(`/api/launch/${__classPrivateFieldGet(this, _TestOpsClient_launch, "f").id}/progress`));
222
222
  return data.ready;
223
223
  }
224
- async createSession(environment = {}, reopenClosedLaunch = false) {
224
+ async ensureLaunchOpen(reopenClosedLaunch = false) {
225
225
  if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
226
226
  throw new Error("Launch isn't created! Call createLaunch first");
227
227
  }
@@ -232,6 +232,15 @@ export class TestOpsClient {
232
232
  }
233
233
  await this.reopenLaunch(__classPrivateFieldGet(this, _TestOpsClient_launch, "f").id);
234
234
  }
235
+ }
236
+ async createSession(environment = {}, reopenClosedLaunch = false) {
237
+ await this.ensureLaunchOpen(reopenClosedLaunch);
238
+ return this.createSessionInOpenLaunch(environment);
239
+ }
240
+ async createSessionInOpenLaunch(environment = {}) {
241
+ if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
242
+ throw new Error("Launch isn't created! Call createLaunch first");
243
+ }
235
244
  const sessionEnvironment = Object.entries(environment).map(([key, value]) => ({
236
245
  key,
237
246
  value: String(value),
@@ -245,6 +254,7 @@ export class TestOpsClient {
245
254
  params: { manual: "true" },
246
255
  });
247
256
  __classPrivateFieldSet(this, _TestOpsClient_session, data, "f");
257
+ return data;
248
258
  }
249
259
  getNamedEnvFor(id) {
250
260
  return __classPrivateFieldGet(this, _TestOpsClient_namedEnvsIdsByEnv, "f").get(id);
@@ -343,10 +353,7 @@ export class TestOpsClient {
343
353
  });
344
354
  }
345
355
  async uploadTestResults(params) {
346
- if (!__classPrivateFieldGet(this, _TestOpsClient_session, "f")) {
347
- throw new Error("Session isn't created! Call createSession first");
348
- }
349
- const { trs, environments, attachmentsResolver, fixturesResolver, onProgress, onRetry, onChunkUploaded } = params;
356
+ const { session, trs, environments, attachmentsResolver, fixturesResolver, onProgress, onRetry, onChunkUploaded } = params;
350
357
  const projectedTrs = trs.map((tr) => ({
351
358
  ...tr,
352
359
  ...(tr.steps ? { steps: normalizeTestStepsResults(tr.steps) } : {}),
@@ -372,7 +379,7 @@ export class TestOpsClient {
372
379
  await this.createNamedEnvs(envsToCreate);
373
380
  }
374
381
  await __classPrivateFieldGet(this, _TestOpsClient_uploadPacer, "f").wait({ requests: 1, files: trsChunk.length });
375
- return __classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_postTestResultsChunk).call(this, trsChunk);
382
+ return __classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_postTestResultsChunk).call(this, trsChunk, session);
376
383
  }, { onRetry });
377
384
  const uploadedChunkTrs = trsChunk.filter((tr) => typeof reportIdsToTestOpsIds[tr.id] === "number");
378
385
  uploadedTrs.push(...uploadedChunkTrs);
@@ -383,9 +390,6 @@ export class TestOpsClient {
383
390
  return uploadedTrs;
384
391
  }
385
392
  async uploadQualityGateResults(results, onProgress) {
386
- if (!__classPrivateFieldGet(this, _TestOpsClient_session, "f")) {
387
- throw new Error("Session isn't created! Call createSession first");
388
- }
389
393
  if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
390
394
  throw new Error("Launch isn't created! Call createLaunch first");
391
395
  }
@@ -395,7 +399,7 @@ export class TestOpsClient {
395
399
  name: result.rule,
396
400
  message: result.message,
397
401
  };
398
- const namedEnvId = !!result.environment && this.getNamedEnvFor(result.environment)?.id;
402
+ const namedEnvId = !!result.environment && __classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_namedEnvByIdOrName).call(this, result.environment)?.id;
399
403
  if (typeof namedEnvId === "number") {
400
404
  item.namedEnvId = namedEnvId;
401
405
  }
@@ -427,7 +431,10 @@ _TestOpsClient_baseUrl = new WeakMap(), _TestOpsClient_logger = new WeakMap(), _
427
431
  }
428
432
  throw error;
429
433
  }
430
- }, _TestOpsClient_postTestResultsChunk = async function _TestOpsClient_postTestResultsChunk(trsChunk) {
434
+ }, _TestOpsClient_namedEnvByIdOrName = function _TestOpsClient_namedEnvByIdOrName(environment) {
435
+ return (__classPrivateFieldGet(this, _TestOpsClient_namedEnvsIdsByEnv, "f").get(environment) ??
436
+ [...__classPrivateFieldGet(this, _TestOpsClient_namedEnvsIdsByEnv, "f").values()].find(({ name }) => name === environment));
437
+ }, _TestOpsClient_postTestResultsChunk = async function _TestOpsClient_postTestResultsChunk(trsChunk, session) {
431
438
  const extendedChunk = trsChunk.map((testResult) => {
432
439
  const extendedTestResult = {
433
440
  ...testResult,
@@ -446,7 +453,7 @@ _TestOpsClient_baseUrl = new WeakMap(), _TestOpsClient_logger = new WeakMap(), _
446
453
  }
447
454
  return extendedTestResult;
448
455
  });
449
- const body = toUploadResultsDto(__classPrivateFieldGet(this, _TestOpsClient_session, "f").id, extendedChunk);
456
+ const body = toUploadResultsDto(session.id, extendedChunk);
450
457
  const data = await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/upload/test-result", {
451
458
  body,
452
459
  headers: { "Content-Type": "application/json" },
package/dist/model.d.ts CHANGED
@@ -212,7 +212,6 @@ export type TestOpsUploaderOptions = {
212
212
  export interface TestOpsFixtureResult extends Omit<TestFixtureResult, "type"> {
213
213
  type: "BEFORE" | "AFTER";
214
214
  }
215
- export type TemplateManifest = Record<string, string>;
216
215
  export type TestOpsPluginOptions = TestOpsUploaderOptions;
217
216
  export type TestOpsSession = {
218
217
  id: number;
package/dist/plugin.js CHANGED
@@ -9,13 +9,13 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  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");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _TestOpsPlugin_instances, _TestOpsPlugin_logger, _TestOpsPlugin_ci, _TestOpsPlugin_client, _TestOpsPlugin_launchName, _TestOpsPlugin_launchTags, _TestOpsPlugin_uploadedTestResultsIds, _TestOpsPlugin_uploadedGlobalAttachmentIds, _TestOpsPlugin_uploadedGlobalErrors, _TestOpsPlugin_autocloseLaunch, _TestOpsPlugin_launchStarted, _TestOpsPlugin_reopenClosedLaunch, _TestOpsPlugin_skippedUploadCycle, _TestOpsPlugin_launchId, _TestOpsPlugin_gitFlow, _TestOpsPlugin_enabledByConfig, _TestOpsPlugin_uploadQueue, _TestOpsPlugin_reopenLaunchIfClosed, _TestOpsPlugin_logDeferredUpload, _TestOpsPlugin_retryHooks, _TestOpsPlugin_logUploadFailure, _TestOpsPlugin_uploadQualityGateResults, _TestOpsPlugin_uploadGlobalErrors, _TestOpsPlugin_uploadGlobalAttachments, _TestOpsPlugin_uploadTestResults, _TestOpsPlugin_upload, _TestOpsPlugin_trsToUpload, _TestOpsPlugin_startUpload, _TestOpsPlugin_stopUpload;
12
+ var _TestOpsPlugin_instances, _TestOpsPlugin_logger, _TestOpsPlugin_ci, _TestOpsPlugin_client, _TestOpsPlugin_launchName, _TestOpsPlugin_launchTags, _TestOpsPlugin_uploadedTestResultsIds, _TestOpsPlugin_uploadedGlobalAttachmentIds, _TestOpsPlugin_uploadedGlobalErrors, _TestOpsPlugin_autocloseLaunch, _TestOpsPlugin_launchStarted, _TestOpsPlugin_reopenClosedLaunch, _TestOpsPlugin_skippedUploadCycle, _TestOpsPlugin_environmentSessions, _TestOpsPlugin_launchId, _TestOpsPlugin_gitFlow, _TestOpsPlugin_enabledByConfig, _TestOpsPlugin_uploadQueue, _TestOpsPlugin_reopenLaunchIfClosed, _TestOpsPlugin_logDeferredUpload, _TestOpsPlugin_retryHooks, _TestOpsPlugin_logUploadFailure, _TestOpsPlugin_uploadQualityGateResults, _TestOpsPlugin_uploadGlobalErrors, _TestOpsPlugin_uploadGlobalAttachments, _TestOpsPlugin_uploadTestResults, _TestOpsPlugin_environmentSession, _TestOpsPlugin_upload, _TestOpsPlugin_groupByEnvironmentVariables, _TestOpsPlugin_environmentIdOf, _TestOpsPlugin_environmentVariables, _TestOpsPlugin_trsToUpload, _TestOpsPlugin_startUpload, _TestOpsPlugin_stopUpload;
13
13
  import { env } from "node:process";
14
14
  import { applyAllureCiEnv, detect, isLocalCiDescriptor } from "@allurereport/ci";
15
15
  import { createProgressLogger, getGlobalLogLevel } from "@allurereport/cli-commons";
16
- import { getWorstStatus } from "@allurereport/core-api";
16
+ import { DEFAULT_ENVIRONMENT, getWorstStatus } from "@allurereport/core-api";
17
17
  import { createPluginSummary, } from "@allurereport/plugin-api";
18
- import { uniqBy, stubTrue } from "lodash-es";
18
+ import { groupBy, isEqual, uniqBy, stubTrue } from "lodash-es";
19
19
  import { bold } from "yoctocolors";
20
20
  import { TestOpsClient } from "./client.js";
21
21
  import { isClosedLaunchError } from "./errors.js";
@@ -27,6 +27,7 @@ import { applyCiOverrides } from "./utils/ciOverrides.js";
27
27
  import { enrichWithCategories, syncLaunchCategories } from "./utils/launchCategories.js";
28
28
  import { resolvePluginOptions, toPositiveInteger } from "./utils/options.js";
29
29
  import { attachmentsResolverFactory, fixturesResolverFactory } from "./utils/resolvers.js";
30
+ import { toSessionVariables } from "./utils/sessionEnvironment.js";
30
31
  import { validateExecutableName } from "./utils/validation.js";
31
32
  const LAUNCH_PROGRESS_POLL_DELAY_MS = 500;
32
33
  const LAUNCH_PROGRESS_ATTEMPTS_LIMIT = 10;
@@ -47,6 +48,7 @@ export class TestOpsPlugin {
47
48
  _TestOpsPlugin_launchStarted.set(this, false);
48
49
  _TestOpsPlugin_reopenClosedLaunch.set(this, false);
49
50
  _TestOpsPlugin_skippedUploadCycle.set(this, false);
51
+ _TestOpsPlugin_environmentSessions.set(this, new Map());
50
52
  _TestOpsPlugin_launchId.set(this, void 0);
51
53
  _TestOpsPlugin_gitFlow.set(this, void 0);
52
54
  _TestOpsPlugin_enabledByConfig.set(this, false);
@@ -188,7 +190,11 @@ export class TestOpsPlugin {
188
190
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").info(`Upload finished. Allure TestOps Launch: ${__classPrivateFieldGet(this, _TestOpsPlugin_client, "f").launchUrl}`);
189
191
  return;
190
192
  }
191
- if (stillPending.length > 0 || __classPrivateFieldGet(this, _TestOpsPlugin_skippedUploadCycle, "f")) {
193
+ const failedUploads = __classPrivateFieldGet(this, _TestOpsPlugin_uploadQueue, "f").failedNames;
194
+ if (failedUploads.length > 0) {
195
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`${failedUploads.length} TestOps upload(s) failed: ${failedUploads.join(", ")}`);
196
+ }
197
+ if (stillPending.length > 0 || failedUploads.length > 0 || __classPrivateFieldGet(this, _TestOpsPlugin_skippedUploadCycle, "f")) {
192
198
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").warn(`Not closing launch ${launchId}: some uploads never made it to TestOps, closing now would report an incomplete launch as finished.`);
193
199
  return;
194
200
  }
@@ -239,7 +245,7 @@ export class TestOpsPlugin {
239
245
  return summary;
240
246
  }
241
247
  }
242
- _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestOpsPlugin_client = new WeakMap(), _TestOpsPlugin_launchName = new WeakMap(), _TestOpsPlugin_launchTags = new WeakMap(), _TestOpsPlugin_uploadedTestResultsIds = new WeakMap(), _TestOpsPlugin_uploadedGlobalAttachmentIds = new WeakMap(), _TestOpsPlugin_uploadedGlobalErrors = new WeakMap(), _TestOpsPlugin_autocloseLaunch = new WeakMap(), _TestOpsPlugin_launchStarted = new WeakMap(), _TestOpsPlugin_reopenClosedLaunch = new WeakMap(), _TestOpsPlugin_skippedUploadCycle = new WeakMap(), _TestOpsPlugin_launchId = new WeakMap(), _TestOpsPlugin_gitFlow = new WeakMap(), _TestOpsPlugin_enabledByConfig = new WeakMap(), _TestOpsPlugin_uploadQueue = new WeakMap(), _TestOpsPlugin_instances = new WeakSet(), _TestOpsPlugin_reopenLaunchIfClosed = async function _TestOpsPlugin_reopenLaunchIfClosed(error) {
248
+ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestOpsPlugin_client = new WeakMap(), _TestOpsPlugin_launchName = new WeakMap(), _TestOpsPlugin_launchTags = new WeakMap(), _TestOpsPlugin_uploadedTestResultsIds = new WeakMap(), _TestOpsPlugin_uploadedGlobalAttachmentIds = new WeakMap(), _TestOpsPlugin_uploadedGlobalErrors = new WeakMap(), _TestOpsPlugin_autocloseLaunch = new WeakMap(), _TestOpsPlugin_launchStarted = new WeakMap(), _TestOpsPlugin_reopenClosedLaunch = new WeakMap(), _TestOpsPlugin_skippedUploadCycle = new WeakMap(), _TestOpsPlugin_environmentSessions = new WeakMap(), _TestOpsPlugin_launchId = new WeakMap(), _TestOpsPlugin_gitFlow = new WeakMap(), _TestOpsPlugin_enabledByConfig = new WeakMap(), _TestOpsPlugin_uploadQueue = new WeakMap(), _TestOpsPlugin_instances = new WeakSet(), _TestOpsPlugin_reopenLaunchIfClosed = async function _TestOpsPlugin_reopenLaunchIfClosed(error) {
243
249
  if (!__classPrivateFieldGet(this, _TestOpsPlugin_reopenClosedLaunch, "f") || !isClosedLaunchError(error)) {
244
250
  return;
245
251
  }
@@ -406,18 +412,21 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
406
412
  }
407
413
  }, _TestOpsPlugin_uploadTestResults = async function _TestOpsPlugin_uploadTestResults(store, trsToUpload, environments, options) {
408
414
  const totalCount = trsToUpload.length;
409
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose(`Preparing to upload ${bold(totalCount.toString())} ${totalCount > 1 ? "test results" : "test result"}`);
415
+ const forEnvironment = options.environmentName ? ` for environment ${options.environmentName}` : "";
416
+ const label = `test results${forEnvironment}`;
417
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose(`Preparing to upload ${bold(totalCount.toString())} ${totalCount > 1 ? "test results" : "test result"}${forEnvironment}`);
410
418
  const progressLogger = createProgressLogger({
411
419
  total: totalCount,
412
- message: "Uploading test results",
420
+ message: `Uploading ${label}`,
413
421
  unitLabel: totalCount === 1 ? "test result uploaded" : "test results uploaded",
414
422
  prefix: "[TestOpsPlugin]",
415
- silent: !!options?.silent,
423
+ silent: !!options.silent,
416
424
  });
417
425
  try {
418
426
  progressLogger.log(true);
419
- const { onRetry, onDeferred } = __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_retryHooks).call(this, "test results");
420
- const result = await __classPrivateFieldGet(this, _TestOpsPlugin_uploadQueue, "f").run("test-results", () => __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").uploadTestResults({
427
+ const { onRetry, onDeferred } = __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_retryHooks).call(this, label);
428
+ const result = await __classPrivateFieldGet(this, _TestOpsPlugin_uploadQueue, "f").run(options.queueName, async () => __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").uploadTestResults({
429
+ session: await options.session(),
421
430
  attachmentsResolver: attachmentsResolverFactory(store),
422
431
  fixturesResolver: fixturesResolverFactory(store),
423
432
  environments,
@@ -441,14 +450,22 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
441
450
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose("No test results were uploaded");
442
451
  return;
443
452
  }
444
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").info(`Uploaded ${uploadedCount} ${uploadedCount > 1 ? "test results" : "test result"}`);
453
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").info(`Uploaded ${uploadedCount} ${uploadedCount > 1 ? "test results" : "test result"}${forEnvironment}`);
445
454
  }
446
455
  catch (error) {
447
- __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_logUploadFailure).call(this, "test results", error);
456
+ __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_logUploadFailure).call(this, label, error);
448
457
  }
449
458
  finally {
450
459
  progressLogger.cancel?.();
451
460
  }
461
+ }, _TestOpsPlugin_environmentSession = async function _TestOpsPlugin_environmentSession(environmentId, environmentVariables) {
462
+ const cached = __classPrivateFieldGet(this, _TestOpsPlugin_environmentSessions, "f").get(environmentId);
463
+ if (cached) {
464
+ return cached;
465
+ }
466
+ const session = await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").createSessionInOpenLaunch({ ...env, ...environmentVariables });
467
+ __classPrivateFieldGet(this, _TestOpsPlugin_environmentSessions, "f").set(environmentId, session);
468
+ return session;
452
469
  }, _TestOpsPlugin_upload = async function _TestOpsPlugin_upload(store, options = {}) {
453
470
  const { context, stage } = options;
454
471
  const trsToUpload = await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_trsToUpload).call(this, store);
@@ -477,8 +494,19 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
477
494
  if (stage === "update" && trsToUpload.length > 0) {
478
495
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose(`Found ${bold(trsToUpload.length.toString())} new test ${trsToUpload.length > 1 ? "results" : "result"}, uploading…`);
479
496
  }
497
+ const environments = await store.allEnvironmentIdentities();
498
+ const contextCategories = context?.categories ?? [];
499
+ const trsEnrichedWithCategories = await enrichWithCategories(store, trsToUpload, contextCategories, (environment) => __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_environmentIdOf).call(this, environment, environments));
500
+ const groups = await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_groupByEnvironmentVariables).call(this, store, trsEnrichedWithCategories);
501
+ const needsBaseSession = globalErrors.length > 0 || globalAttachments.length > 0 || groups.base.length > 0;
502
+ let baseSession;
480
503
  try {
481
- await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").createSession(env, __classPrivateFieldGet(this, _TestOpsPlugin_reopenClosedLaunch, "f"));
504
+ if (needsBaseSession) {
505
+ baseSession = await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").createSession({ ...env, ...groups.reportVariables }, __classPrivateFieldGet(this, _TestOpsPlugin_reopenClosedLaunch, "f"));
506
+ }
507
+ else {
508
+ await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").ensureLaunchOpen(__classPrivateFieldGet(this, _TestOpsPlugin_reopenClosedLaunch, "f"));
509
+ }
482
510
  }
483
511
  catch (error) {
484
512
  __classPrivateFieldSet(this, _TestOpsPlugin_skippedUploadCycle, true, "f");
@@ -500,17 +528,60 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
500
528
  if (trsToUpload.length === 0) {
501
529
  return;
502
530
  }
503
- await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadQualityGateResults).call(this, store);
504
- const environments = await store.allEnvironmentIdentities();
505
- const contextCategories = context?.categories ?? [];
506
- const trsEnrichedWithCategories = await enrichWithCategories(store, trsToUpload, contextCategories);
507
531
  const categoriesError = await syncLaunchCategories(__classPrivateFieldGet(this, _TestOpsPlugin_client, "f"), trsEnrichedWithCategories, contextCategories);
508
532
  if (categoriesError) {
509
533
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").warn(`Uploading test results without launch categories: ${categoriesError.message}`);
510
534
  }
511
- await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadTestResults).call(this, store, trsEnrichedWithCategories, environments, {
512
- silent: !!context?.realTime,
513
- });
535
+ const silent = !!context?.realTime;
536
+ if (groups.base.length > 0) {
537
+ await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadTestResults).call(this, store, groups.base, environments, {
538
+ silent,
539
+ queueName: "test-results",
540
+ session: async () => baseSession,
541
+ });
542
+ }
543
+ for (const { environmentId, environmentVariables, trs } of groups.withOwnVariables) {
544
+ await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadTestResults).call(this, store, trs, environments, {
545
+ silent,
546
+ queueName: `test-results:${environmentId}`,
547
+ session: () => __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_environmentSession).call(this, environmentId, environmentVariables),
548
+ environmentName: environments.find(({ id }) => id === environmentId)?.name ?? environmentId,
549
+ });
550
+ }
551
+ await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadQualityGateResults).call(this, store);
552
+ }, _TestOpsPlugin_groupByEnvironmentVariables = async function _TestOpsPlugin_groupByEnvironmentVariables(store, trs) {
553
+ const configReportVariables = await store.allVariables();
554
+ const reportVariables = toSessionVariables(configReportVariables);
555
+ const base = [];
556
+ const withOwnVariables = [];
557
+ const trsByEnvironment = groupBy(trs, (tr) => tr.environment ?? DEFAULT_ENVIRONMENT);
558
+ for (const [environmentId, environmentTrs] of Object.entries(trsByEnvironment)) {
559
+ const environmentVariables = await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_environmentVariables).call(this, store, environmentId, configReportVariables, reportVariables);
560
+ if (isEqual(environmentVariables, reportVariables)) {
561
+ for (const tr of environmentTrs) {
562
+ base.push(tr);
563
+ }
564
+ }
565
+ else {
566
+ withOwnVariables.push({ environmentId, environmentVariables, trs: environmentTrs });
567
+ }
568
+ }
569
+ return { reportVariables, base, withOwnVariables };
570
+ }, _TestOpsPlugin_environmentIdOf = function _TestOpsPlugin_environmentIdOf(environment, environments) {
571
+ if (environment === undefined || environments.some(({ id }) => id === environment)) {
572
+ return environment;
573
+ }
574
+ return environments.find(({ name }) => name === environment)?.id ?? environment;
575
+ }, _TestOpsPlugin_environmentVariables = async function _TestOpsPlugin_environmentVariables(store, environmentId, configReportVariables, reportVariables) {
576
+ try {
577
+ const merged = await store.envVariablesByEnvironmentId(environmentId);
578
+ const own = Object.fromEntries(Object.entries(merged).filter(([key, value]) => !Object.hasOwn(configReportVariables, key) || configReportVariables[key] !== value));
579
+ return { ...reportVariables, ...toSessionVariables(own) };
580
+ }
581
+ catch (error) {
582
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").warn(`Failed to resolve variables of environment ${environmentId}: ${error}`);
583
+ return reportVariables;
584
+ }
514
585
  }, _TestOpsPlugin_trsToUpload = async function _TestOpsPlugin_trsToUpload(store) {
515
586
  const filter = this.options.filter ?? stubTrue;
516
587
  const filteredTrs = await store.allTestResults({
@@ -8,6 +8,7 @@ export type UploadRunResult<T> = {
8
8
  export declare class UploadQueue {
9
9
  #private;
10
10
  get pendingNames(): string[];
11
+ get failedNames(): string[];
11
12
  run<T>(name: string, task: () => Promise<T>, options?: RetryOptions & {
12
13
  onDeferred?: (error?: unknown) => void;
13
14
  }): Promise<UploadRunResult<T>>;
@@ -9,34 +9,42 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
10
10
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
11
  };
12
- var _UploadQueue_pending, _UploadQueue_suspended;
12
+ var _UploadQueue_pending, _UploadQueue_failed, _UploadQueue_suspended;
13
13
  import { ErrorKind, classifyError, shouldRetryUpload, withUploadRetry } from "./errors.js";
14
14
  const isSuspendingError = (error) => classifyError(error) === ErrorKind.ResourceRecoverable;
15
15
  export class UploadQueue {
16
16
  constructor() {
17
17
  _UploadQueue_pending.set(this, new Map());
18
+ _UploadQueue_failed.set(this, new Set());
18
19
  _UploadQueue_suspended.set(this, false);
19
20
  }
20
21
  get pendingNames() {
21
22
  return [...__classPrivateFieldGet(this, _UploadQueue_pending, "f").keys()];
22
23
  }
24
+ get failedNames() {
25
+ return [...__classPrivateFieldGet(this, _UploadQueue_failed, "f")];
26
+ }
23
27
  async run(name, task, options = {}) {
24
28
  const { onDeferred, ...retryOptions } = options;
25
29
  if (__classPrivateFieldGet(this, _UploadQueue_suspended, "f")) {
26
30
  __classPrivateFieldGet(this, _UploadQueue_pending, "f").set(name, task);
31
+ __classPrivateFieldGet(this, _UploadQueue_failed, "f").delete(name);
27
32
  onDeferred?.();
28
33
  return { deferred: true };
29
34
  }
30
35
  try {
31
36
  const value = await withUploadRetry(task, retryOptions);
32
37
  __classPrivateFieldGet(this, _UploadQueue_pending, "f").delete(name);
38
+ __classPrivateFieldGet(this, _UploadQueue_failed, "f").delete(name);
33
39
  return { deferred: false, value };
34
40
  }
35
41
  catch (error) {
36
42
  if (!shouldRetryUpload(error)) {
43
+ __classPrivateFieldGet(this, _UploadQueue_failed, "f").add(name);
37
44
  throw error;
38
45
  }
39
46
  __classPrivateFieldGet(this, _UploadQueue_pending, "f").set(name, task);
47
+ __classPrivateFieldGet(this, _UploadQueue_failed, "f").delete(name);
40
48
  __classPrivateFieldSet(this, _UploadQueue_suspended, isSuspendingError(error), "f");
41
49
  onDeferred?.(error);
42
50
  return { deferred: true };
@@ -58,4 +66,4 @@ export class UploadQueue {
58
66
  return [...remaining.keys()];
59
67
  }
60
68
  }
61
- _UploadQueue_pending = new WeakMap(), _UploadQueue_suspended = new WeakMap();
69
+ _UploadQueue_pending = new WeakMap(), _UploadQueue_failed = new WeakMap(), _UploadQueue_suspended = new WeakMap();
@@ -3,5 +3,5 @@ import type { AllureStore } from "@allurereport/plugin-api";
3
3
  import type { TestOpsClient } from "../client.js";
4
4
  import type { TestOpsPluginTestResult, UploadCategory } from "../model.js";
5
5
  export declare const categoryDisplayName: (cat: UploadCategory) => string;
6
- export declare const enrichWithCategories: (store: AllureStore, trs: TestOpsPluginTestResult[], contextCategories: CategoryDefinition[]) => Promise<TestOpsPluginTestResult[]>;
6
+ export declare const enrichWithCategories: (store: AllureStore, trs: TestOpsPluginTestResult[], contextCategories: CategoryDefinition[], resolveEnvironmentId?: (environment: string | undefined) => string | undefined) => Promise<TestOpsPluginTestResult[]>;
7
7
  export declare const syncLaunchCategories: (client: TestOpsClient, trs: TestOpsPluginTestResult[], contextCategories: CategoryDefinition[]) => Promise<Error | undefined>;
@@ -1,9 +1,9 @@
1
1
  import { toUploadCategory } from "./categories.js";
2
2
  import { unwrapStepsAttachments } from "./resolvers.js";
3
3
  export const categoryDisplayName = (cat) => cat.name ?? cat.grouping?.[0]?.name ?? cat.grouping?.[0]?.value ?? cat.grouping?.[0]?.key ?? cat.externalId;
4
- export const enrichWithCategories = async (store, trs, contextCategories) => {
4
+ export const enrichWithCategories = async (store, trs, contextCategories, resolveEnvironmentId = (environment) => environment) => {
5
5
  return Promise.all(trs.map(async (tr) => {
6
- const environmentId = await store.environmentIdByTrId(tr.id);
6
+ const environmentId = resolveEnvironmentId((await store.environmentIdByTrId(tr.id)) ?? tr.environment);
7
7
  const base = {
8
8
  ...tr,
9
9
  ...(environmentId ? { environment: environmentId } : {}),
@@ -0,0 +1 @@
1
+ export declare const toSessionVariables: (variables: Record<string, unknown>) => Record<string, string>;
@@ -0,0 +1,4 @@
1
+ export const toSessionVariables = (variables) => Object.fromEntries(Object.entries(variables)
2
+ .filter(([, value]) => ["string", "number", "boolean"].includes(typeof value))
3
+ .map(([key, value]) => [key, String(value)])
4
+ .filter(([, value]) => value.trim() !== ""));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allurereport/plugin-testops",
3
- "version": "3.21.0",
3
+ "version": "3.22.0",
4
4
  "description": "Allure TestOps Plugin",
5
5
  "keywords": [
6
6
  "allure",
@@ -27,19 +27,19 @@
27
27
  "scripts": {
28
28
  "build": "run clean && tsc --project ./tsconfig.json",
29
29
  "clean": "rimraf ./dist",
30
- "test": "rimraf ./out && vitest run",
30
+ "test": "rimraf out && vp test run",
31
31
  "test:unit": "yarn test",
32
- "lint": "oxlint --import-plugin src test features stories",
33
- "lint:fix": "oxlint --import-plugin --fix src test features stories"
32
+ "lint": "vp lint --import-plugin src test features stories",
33
+ "lint:fix": "vp lint --import-plugin --fix src test features stories"
34
34
  },
35
35
  "dependencies": {
36
- "@allurereport/ci": "3.21.0",
37
- "@allurereport/cli-commons": "3.21.0",
38
- "@allurereport/core-api": "3.21.0",
39
- "@allurereport/git": "3.21.0",
40
- "@allurereport/plugin-api": "3.21.0",
41
- "@allurereport/reader-api": "3.21.0",
42
- "@allurereport/service": "3.21.0",
36
+ "@allurereport/ci": "3.22.0",
37
+ "@allurereport/cli-commons": "3.22.0",
38
+ "@allurereport/core-api": "3.22.0",
39
+ "@allurereport/git": "3.22.0",
40
+ "@allurereport/plugin-api": "3.22.0",
41
+ "@allurereport/reader-api": "3.22.0",
42
+ "@allurereport/service": "3.22.0",
43
43
  "axios": "^1.20.0",
44
44
  "form-data": "^4.0.6",
45
45
  "lodash-es": "^4.18.1",
@@ -47,17 +47,19 @@
47
47
  "yoctocolors": "^2"
48
48
  },
49
49
  "devDependencies": {
50
- "@allurereport/test-config": "3.21.0",
50
+ "@allurereport/common-config": "3.22.0",
51
51
  "@types/d3-shape": "^3.1.8",
52
52
  "@types/lodash-es": "^4",
53
53
  "@types/node": "^20",
54
- "@vitest/coverage-istanbul": "^4.1.0",
54
+ "@vitest/coverage-istanbul": "5.0.4",
55
55
  "@vitest/runner": "^2",
56
- "@vitest/snapshot": "^5.0.1",
56
+ "@vitest/snapshot": "5.0.4",
57
57
  "allure-js-commons": "^3",
58
58
  "allure-vitest": "^3",
59
59
  "rimraf": "^6",
60
60
  "typescript": "^5",
61
- "vitest": "^4.1.0"
61
+ "vite": "@voidzero-dev/vite-plus-core@1.2.0",
62
+ "vite-plus": "1.2.0",
63
+ "vitest": "5.0.4"
62
64
  }
63
65
  }
@@ -1,7 +0,0 @@
1
- export { toUploadAttachmentDto, uploadFilenameForLink } from "./attachments.js";
2
- export { toUploadCategory, buildUploadCategoryGrouping, toUploadCategoryFromContext } from "./categories.js";
3
- export { toUploadFixtureResultDto, toUploadFixturesResultsDto } from "./fixtures.js";
4
- export { resolvePluginOptions } from "./options.js";
5
- export { attachmentsResolverFactory, fixturesResolverFactory, unwrapStepsAttachments } from "./resolvers.js";
6
- export { normalizeTestStepsResults, toUploadResultsDto, toUploadTestResultDto } from "./testResults.js";
7
- export { validateExecutableName } from "./validation.js";
@@ -1,7 +0,0 @@
1
- export { toUploadAttachmentDto, uploadFilenameForLink } from "./attachments.js";
2
- export { toUploadCategory, buildUploadCategoryGrouping, toUploadCategoryFromContext } from "./categories.js";
3
- export { toUploadFixtureResultDto, toUploadFixturesResultsDto } from "./fixtures.js";
4
- export { resolvePluginOptions } from "./options.js";
5
- export { attachmentsResolverFactory, fixturesResolverFactory, unwrapStepsAttachments } from "./resolvers.js";
6
- export { normalizeTestStepsResults, toUploadResultsDto, toUploadTestResultDto } from "./testResults.js";
7
- export { validateExecutableName } from "./validation.js";