@allurereport/plugin-testops 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.
- package/README.md +2 -0
- package/dist/client.d.ts +7 -3
- package/dist/client.js +182 -109
- package/dist/errors.d.ts +23 -0
- package/dist/errors.js +134 -0
- package/dist/gitFlow/options.d.ts +1 -0
- package/dist/gitFlow/options.js +1 -1
- package/dist/logger.d.ts +3 -1
- package/dist/logger.js +6 -2
- package/dist/model.d.ts +26 -1
- package/dist/plugin.js +175 -160
- package/dist/uploadPacer.d.ts +12 -0
- package/dist/uploadPacer.js +76 -0
- package/dist/uploadQueue.d.ts +15 -0
- package/dist/uploadQueue.js +61 -0
- package/dist/utils/ciOverrides.d.ts +2 -0
- package/dist/utils/ciOverrides.js +20 -0
- package/dist/utils/httpRetry.d.ts +4 -0
- package/dist/utils/httpRetry.js +62 -0
- package/dist/utils/jobParameters.d.ts +15 -0
- package/dist/utils/jobParameters.js +39 -0
- package/dist/utils/launchCategories.d.ts +7 -0
- package/dist/utils/launchCategories.js +97 -0
- package/dist/utils/options.d.ts +1 -0
- package/dist/utils/options.js +14 -2
- package/dist/utils/resolvers.js +1 -0
- package/dist/utils/testResults.js +0 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -80,6 +80,8 @@ The plugin accepts the following options:
|
|
|
80
80
|
| `autocloseLaunch` | When `true` (default), the launch is closed automatically when the plugin finishes; set to `false` to keep the launch open | `boolean` | `true` |
|
|
81
81
|
| `gitFlow` | When `true`, collect Git metadata for 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 TestOps | `number` | `100` |
|
|
83
|
+
| `uploadRateLimit` | Caps how fast uploads are sent to 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 |
|
|
84
|
+
| `reopenClosedLaunch` | When `true`, a launch that TestOps reports as closed is reopened automatically instead of failing the upload | `boolean` | `false` |
|
|
83
85
|
|
|
84
86
|
### Using options from environment variables
|
|
85
87
|
|
package/dist/client.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ClientRequest } from "http";
|
|
|
2
2
|
import type { AttachmentLink, CiDescriptor, EnvironmentIdentity, TestError, TestResult, TestStatus } from "@allurereport/core-api";
|
|
3
3
|
import type { QualityGateValidationResult } from "@allurereport/plugin-api";
|
|
4
4
|
import { AxiosError, type AxiosResponse } from "axios";
|
|
5
|
+
import type { RetryOptions } from "./errors.js";
|
|
5
6
|
import type { LaunchGitContextDto } from "./gitFlow/index.js";
|
|
6
7
|
import type { AttachmentForUpload, AttachmentsResolver, FixtureResolver, LaunchCategoryBulkItem, LaunchCategoryBulkResult, TestOpsClientParams, TestOpsNamedEnv, TestOpsPluginTestResult } from "./model.js";
|
|
7
8
|
declare class TestOpsClientError extends AxiosError<{
|
|
@@ -23,13 +24,14 @@ export declare class TestOpsClient {
|
|
|
23
24
|
get launchUrl(): string | undefined;
|
|
24
25
|
get launchId(): number | undefined;
|
|
25
26
|
closeLaunch(launchId: number): Promise<void>;
|
|
27
|
+
reopenLaunch(launchId: number): Promise<void>;
|
|
26
28
|
createLaunchCategoriesBulk(launchId: number, items: LaunchCategoryBulkItem[]): Promise<LaunchCategoryBulkResult[]>;
|
|
27
|
-
startUpload(ci: CiDescriptor): Promise<void>;
|
|
29
|
+
startUpload(ci: CiDescriptor, jobRunId?: number): Promise<void>;
|
|
28
30
|
stopUpload(ci: CiDescriptor, status: TestStatus): Promise<void>;
|
|
29
31
|
createLaunch(launchName: string, launchTags: string[], gitContext?: LaunchGitContextDto): Promise<void>;
|
|
32
|
+
attachToLaunch(launchId: number): void;
|
|
30
33
|
checkLaunchProgress(): Promise<boolean>;
|
|
31
|
-
createSession(environment?: Record<string, unknown
|
|
32
|
-
get namedEnvs(): MapIterator<TestOpsNamedEnv>;
|
|
34
|
+
createSession(environment?: Record<string, unknown>, reopenClosedLaunch?: boolean): Promise<void>;
|
|
33
35
|
getNamedEnvFor(id: string): TestOpsNamedEnv | undefined;
|
|
34
36
|
createNamedEnvs(environments: EnvironmentIdentity[], onProgress?: (percent: number, total: number) => void): Promise<void>;
|
|
35
37
|
uploadGlobalAttachments(params: {
|
|
@@ -44,6 +46,8 @@ export declare class TestOpsClient {
|
|
|
44
46
|
attachmentsResolver: AttachmentsResolver;
|
|
45
47
|
fixturesResolver: FixtureResolver;
|
|
46
48
|
onProgress?: () => void;
|
|
49
|
+
onRetry?: RetryOptions["onRetry"];
|
|
50
|
+
onChunkUploaded?: (uploadedChunkTrs: TestResult[]) => void;
|
|
47
51
|
}): Promise<TestResult[]>;
|
|
48
52
|
uploadQualityGateResults(results: QualityGateValidationResult[], onProgress?: (percent: number, total: number) => void): Promise<void>;
|
|
49
53
|
}
|
package/dist/client.js
CHANGED
|
@@ -9,20 +9,27 @@ 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_session, _TestOpsClient_uploadInProgress, _TestOpsClient_uploadLimit, _TestOpsClient_namedEnvsIdsByEnv, _TestOpsClient_postTestResultsChunk, _TestOpsClient_uploadChunkAttachmentsAndFixtures, _TestOpsClient_uploadAttachmentsForResult, _TestOpsClient_uploadFixturesForResult;
|
|
13
|
-
import {
|
|
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;
|
|
13
|
+
import { isLocalCiDescriptor } from "@allurereport/ci";
|
|
14
|
+
import { KnownError, createServiceHttpClient } from "@allurereport/service";
|
|
14
15
|
import { AxiosError, isAxiosError } from "axios";
|
|
15
16
|
import FormData from "form-data";
|
|
16
17
|
import { chunk } from "lodash-es";
|
|
17
18
|
import pLimit from "p-limit";
|
|
18
19
|
import { bold } from "yoctocolors";
|
|
20
|
+
import { withUploadRetry } from "./errors.js";
|
|
19
21
|
import { Logger } from "./logger.js";
|
|
22
|
+
import { UploadPacer } from "./uploadPacer.js";
|
|
20
23
|
import { toUploadFixturesResultsDto } from "./utils/fixtures.js";
|
|
24
|
+
import { attachmentByteLength, retryRequest } from "./utils/httpRetry.js";
|
|
25
|
+
import { TESTOPS_CI_TYPE, ciEndpoint, externalParameters } from "./utils/jobParameters.js";
|
|
21
26
|
import { testStatusToLaunchStatus } from "./utils/launches.js";
|
|
22
27
|
import { normalizeTestStepsResults, toUploadResultsDto } from "./utils/testResults.js";
|
|
23
28
|
import { validateExecutableName } from "./utils/validation.js";
|
|
24
29
|
class TestOpsClientError extends AxiosError {
|
|
25
30
|
}
|
|
31
|
+
const UNKNOWN_ERROR_MESSAGE = "Unknown error";
|
|
32
|
+
const MAX_LAUNCH_NAME_LENGTH = 255;
|
|
26
33
|
const CHUNK_SIZE = 100;
|
|
27
34
|
const BULK_UPLOAD_CHUNK_SIZE = 1000;
|
|
28
35
|
export class TestOpsClient {
|
|
@@ -34,9 +41,11 @@ export class TestOpsClient {
|
|
|
34
41
|
_TestOpsClient_projectId.set(this, void 0);
|
|
35
42
|
_TestOpsClient_client.set(this, void 0);
|
|
36
43
|
_TestOpsClient_launch.set(this, void 0);
|
|
44
|
+
_TestOpsClient_jobRunId.set(this, void 0);
|
|
37
45
|
_TestOpsClient_session.set(this, void 0);
|
|
38
46
|
_TestOpsClient_uploadInProgress.set(this, false);
|
|
39
47
|
_TestOpsClient_uploadLimit.set(this, 1);
|
|
48
|
+
_TestOpsClient_uploadPacer.set(this, void 0);
|
|
40
49
|
_TestOpsClient_namedEnvsIdsByEnv.set(this, new Map());
|
|
41
50
|
if (!params.accessToken) {
|
|
42
51
|
throw new Error("accessToken is required");
|
|
@@ -59,6 +68,7 @@ export class TestOpsClient {
|
|
|
59
68
|
if (params.limit) {
|
|
60
69
|
__classPrivateFieldSet(this, _TestOpsClient_uploadLimit, params.limit, "f");
|
|
61
70
|
}
|
|
71
|
+
__classPrivateFieldSet(this, _TestOpsClient_uploadPacer, new UploadPacer(params.uploadRateLimit), "f");
|
|
62
72
|
}
|
|
63
73
|
isTestOpsClientError(error) {
|
|
64
74
|
return (isAxiosError(error) &&
|
|
@@ -79,6 +89,11 @@ export class TestOpsClient {
|
|
|
79
89
|
await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post(`/api/launch/${launchId}/close`);
|
|
80
90
|
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose("Launch closed");
|
|
81
91
|
}
|
|
92
|
+
async reopenLaunch(launchId) {
|
|
93
|
+
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose(`Reopening launch ${launchId}…`);
|
|
94
|
+
await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post(`/api/launch/${launchId}/reopen`);
|
|
95
|
+
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose("Launch reopened");
|
|
96
|
+
}
|
|
82
97
|
async createLaunchCategoriesBulk(launchId, items) {
|
|
83
98
|
if (items.length === 0) {
|
|
84
99
|
return [];
|
|
@@ -114,29 +129,52 @@ export class TestOpsClient {
|
|
|
114
129
|
}
|
|
115
130
|
return results;
|
|
116
131
|
}
|
|
117
|
-
async startUpload(ci) {
|
|
118
|
-
if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
|
|
132
|
+
async startUpload(ci, jobRunId) {
|
|
133
|
+
if (!jobRunId && !__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
|
|
119
134
|
throw new Error("Launch isn't created! Call createLaunch first");
|
|
120
135
|
}
|
|
136
|
+
if (!jobRunId && isLocalCiDescriptor(ci)) {
|
|
137
|
+
__classPrivateFieldSet(this, _TestOpsClient_uploadInProgress, true, "f");
|
|
138
|
+
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose("CI upload started (manual, no CI detected)");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const endpoint = ciEndpoint(ci);
|
|
142
|
+
const existingJob = await __classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_getExistingJob).call(this, ci);
|
|
143
|
+
const parameters = externalParameters(ci, existingJob?.parameters);
|
|
144
|
+
const jobPayload = {
|
|
145
|
+
name: ci.jobName || ci.jobUid,
|
|
146
|
+
uid: ci.jobUid,
|
|
147
|
+
...(existingJob?.id ? { id: existingJob.id } : {}),
|
|
148
|
+
...(ci.jobUrl ? { url: ci.jobUrl } : {}),
|
|
149
|
+
...(parameters.job.length ? { parameters: parameters.job } : {}),
|
|
150
|
+
};
|
|
151
|
+
const jobRunPayload = {
|
|
152
|
+
...(jobRunId ? { id: jobRunId } : {}),
|
|
153
|
+
uid: ci.jobRunUid,
|
|
154
|
+
...(ci.jobRunName ? { name: ci.jobRunName } : {}),
|
|
155
|
+
...(ci.jobRunUrl ? { url: ci.jobRunUrl } : {}),
|
|
156
|
+
...(parameters.jobRun.length ? { parameters: parameters.jobRun } : {}),
|
|
157
|
+
};
|
|
121
158
|
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose(`Starting CI upload (${ci.type})…`);
|
|
122
|
-
await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/upload/start", {
|
|
159
|
+
const data = await retryRequest(() => __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/upload/start", {
|
|
123
160
|
body: {
|
|
124
161
|
projectId: __classPrivateFieldGet(this, _TestOpsClient_projectId, "f"),
|
|
125
162
|
ci: {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
job: {
|
|
129
|
-
name: ci.jobUid,
|
|
130
|
-
uid: ci.jobUid,
|
|
131
|
-
},
|
|
132
|
-
jobRun: {
|
|
133
|
-
uid: ci.jobRunUid,
|
|
134
|
-
},
|
|
135
|
-
launch: {
|
|
136
|
-
id: __classPrivateFieldGet(this, _TestOpsClient_launch, "f").id,
|
|
163
|
+
type: TESTOPS_CI_TYPE[ci.type] ?? ci.type,
|
|
164
|
+
...(endpoint ? { endpoint } : {}),
|
|
137
165
|
},
|
|
166
|
+
job: jobPayload,
|
|
167
|
+
jobRun: jobRunPayload,
|
|
168
|
+
...(__classPrivateFieldGet(this, _TestOpsClient_launch, "f") ? { launch: { id: __classPrivateFieldGet(this, _TestOpsClient_launch, "f").id } } : {}),
|
|
138
169
|
},
|
|
139
|
-
});
|
|
170
|
+
}));
|
|
171
|
+
__classPrivateFieldSet(this, _TestOpsClient_jobRunId, data?.jobRunId ?? jobRunId, "f");
|
|
172
|
+
if (jobRunId) {
|
|
173
|
+
if (typeof data?.launchId !== "number") {
|
|
174
|
+
throw new Error("TestOps didn't return a launch id for the job run");
|
|
175
|
+
}
|
|
176
|
+
__classPrivateFieldSet(this, _TestOpsClient_launch, { id: data.launchId }, "f");
|
|
177
|
+
}
|
|
140
178
|
__classPrivateFieldSet(this, _TestOpsClient_uploadInProgress, true, "f");
|
|
141
179
|
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose("CI upload started");
|
|
142
180
|
}
|
|
@@ -144,14 +182,16 @@ export class TestOpsClient {
|
|
|
144
182
|
if (!__classPrivateFieldGet(this, _TestOpsClient_uploadInProgress, "f")) {
|
|
145
183
|
throw new Error("Upload isn't started! Call startUpload first");
|
|
146
184
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
185
|
+
if (!isLocalCiDescriptor(ci)) {
|
|
186
|
+
await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/upload/stop", {
|
|
187
|
+
body: {
|
|
188
|
+
jobRunUid: ci.jobRunUid,
|
|
189
|
+
jobUid: ci.jobUid,
|
|
190
|
+
projectId: __classPrivateFieldGet(this, _TestOpsClient_projectId, "f"),
|
|
191
|
+
status: testStatusToLaunchStatus(status),
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
155
195
|
__classPrivateFieldSet(this, _TestOpsClient_uploadInProgress, false, "f");
|
|
156
196
|
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose(`CI upload stopped (status: ${status})`);
|
|
157
197
|
}
|
|
@@ -159,7 +199,7 @@ export class TestOpsClient {
|
|
|
159
199
|
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose("Creating launch…");
|
|
160
200
|
const data = await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/launch", {
|
|
161
201
|
body: {
|
|
162
|
-
name: launchName,
|
|
202
|
+
name: launchName.slice(0, MAX_LAUNCH_NAME_LENGTH),
|
|
163
203
|
projectId: __classPrivateFieldGet(this, _TestOpsClient_projectId, "f"),
|
|
164
204
|
autoclose: true,
|
|
165
205
|
external: true,
|
|
@@ -170,42 +210,44 @@ export class TestOpsClient {
|
|
|
170
210
|
__classPrivateFieldSet(this, _TestOpsClient_launch, data, "f");
|
|
171
211
|
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").debug(`Launch created: id=${bold(data.id.toString())}`);
|
|
172
212
|
}
|
|
213
|
+
attachToLaunch(launchId) {
|
|
214
|
+
__classPrivateFieldSet(this, _TestOpsClient_launch, { id: launchId }, "f");
|
|
215
|
+
}
|
|
173
216
|
async checkLaunchProgress() {
|
|
174
217
|
if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
|
|
175
218
|
throw new Error("Launch isn't created! Call createLaunch first");
|
|
176
219
|
}
|
|
177
220
|
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose("Retrieving launch progress status…");
|
|
178
|
-
const data = await __classPrivateFieldGet(this, _TestOpsClient_client, "f").get(`/api/launch/${__classPrivateFieldGet(this, _TestOpsClient_launch, "f").id}/progress`);
|
|
221
|
+
const data = await retryRequest(() => __classPrivateFieldGet(this, _TestOpsClient_client, "f").get(`/api/launch/${__classPrivateFieldGet(this, _TestOpsClient_launch, "f").id}/progress`));
|
|
179
222
|
return data.ready;
|
|
180
223
|
}
|
|
181
|
-
async createSession(environment = {}) {
|
|
224
|
+
async createSession(environment = {}, reopenClosedLaunch = false) {
|
|
182
225
|
if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
|
|
183
226
|
throw new Error("Launch isn't created! Call createLaunch first");
|
|
184
227
|
}
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
228
|
+
const currentLaunch = await retryRequest(() => __classPrivateFieldGet(this, _TestOpsClient_client, "f").get(`/api/launch/${__classPrivateFieldGet(this, _TestOpsClient_launch, "f").id}`));
|
|
229
|
+
if (currentLaunch.closed) {
|
|
230
|
+
if (!reopenClosedLaunch) {
|
|
231
|
+
throw new Error(`Launch ${__classPrivateFieldGet(this, _TestOpsClient_launch, "f").id} is closed`);
|
|
232
|
+
}
|
|
233
|
+
await this.reopenLaunch(__classPrivateFieldGet(this, _TestOpsClient_launch, "f").id);
|
|
234
|
+
}
|
|
235
|
+
const sessionEnvironment = Object.entries(environment).map(([key, value]) => ({
|
|
236
|
+
key,
|
|
237
|
+
value: String(value),
|
|
238
|
+
}));
|
|
239
|
+
const data = __classPrivateFieldGet(this, _TestOpsClient_jobRunId, "f")
|
|
240
|
+
? await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/upload/session", {
|
|
241
|
+
body: { jobRunId: __classPrivateFieldGet(this, _TestOpsClient_jobRunId, "f"), environment: sessionEnvironment },
|
|
242
|
+
})
|
|
243
|
+
: await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/upload/session", {
|
|
244
|
+
body: { launchId: __classPrivateFieldGet(this, _TestOpsClient_launch, "f").id, environment: sessionEnvironment },
|
|
245
|
+
params: { manual: "true" },
|
|
246
|
+
});
|
|
197
247
|
__classPrivateFieldSet(this, _TestOpsClient_session, data, "f");
|
|
198
248
|
}
|
|
199
|
-
get namedEnvs() {
|
|
200
|
-
return __classPrivateFieldGet(this, _TestOpsClient_namedEnvsIdsByEnv, "f").values();
|
|
201
|
-
}
|
|
202
249
|
getNamedEnvFor(id) {
|
|
203
|
-
|
|
204
|
-
if (env.externalId === id) {
|
|
205
|
-
return env;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return undefined;
|
|
250
|
+
return __classPrivateFieldGet(this, _TestOpsClient_namedEnvsIdsByEnv, "f").get(id);
|
|
209
251
|
}
|
|
210
252
|
async createNamedEnvs(environments, onProgress) {
|
|
211
253
|
if (!__classPrivateFieldGet(this, _TestOpsClient_session, "f")) {
|
|
@@ -247,17 +289,26 @@ export class TestOpsClient {
|
|
|
247
289
|
if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
|
|
248
290
|
throw new Error("Launch isn't created! Call createLaunch first");
|
|
249
291
|
}
|
|
292
|
+
const launchId = __classPrivateFieldGet(this, _TestOpsClient_launch, "f").id;
|
|
250
293
|
const formData = new FormData();
|
|
294
|
+
let hasResolvedAttachments = false;
|
|
295
|
+
let totalBytes = 0;
|
|
251
296
|
for (const attachmentLink of attachments) {
|
|
252
297
|
const attachment = await attachmentsResolver(attachmentLink);
|
|
253
298
|
if (!attachment) {
|
|
254
299
|
continue;
|
|
255
300
|
}
|
|
301
|
+
totalBytes += attachmentByteLength(attachment);
|
|
256
302
|
formData.append("file", attachment.content, {
|
|
257
303
|
filename: attachment.originalFileName,
|
|
258
304
|
contentType: attachment.contentType,
|
|
259
305
|
});
|
|
306
|
+
hasResolvedAttachments = true;
|
|
260
307
|
}
|
|
308
|
+
if (!hasResolvedAttachments) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
await __classPrivateFieldGet(this, _TestOpsClient_uploadPacer, "f").wait({ requests: 1, files: attachments.length, bytes: totalBytes });
|
|
261
312
|
await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/launch/attachment", {
|
|
262
313
|
body: formData,
|
|
263
314
|
onUploadProgress(progressEvent) {
|
|
@@ -265,7 +316,7 @@ export class TestOpsClient {
|
|
|
265
316
|
const percent = total > 0 ? Math.min(100, Math.max(0, (progressEvent.loaded / total) * 100)) : 0;
|
|
266
317
|
onProgress?.(percent, total);
|
|
267
318
|
},
|
|
268
|
-
params: { launchId: __classPrivateFieldGet(this,
|
|
319
|
+
params: { launchId, ...(__classPrivateFieldGet(this, _TestOpsClient_session, "f").jobRunId ? { jobRunId: __classPrivateFieldGet(this, _TestOpsClient_session, "f").jobRunId } : {}) },
|
|
269
320
|
headers: formData.getHeaders(),
|
|
270
321
|
});
|
|
271
322
|
}
|
|
@@ -276,10 +327,13 @@ export class TestOpsClient {
|
|
|
276
327
|
if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
|
|
277
328
|
throw new Error("Launch isn't created! Call createLaunch first");
|
|
278
329
|
}
|
|
330
|
+
const launchId = __classPrivateFieldGet(this, _TestOpsClient_launch, "f").id;
|
|
331
|
+
await __classPrivateFieldGet(this, _TestOpsClient_uploadPacer, "f").wait({ requests: 1 });
|
|
279
332
|
await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/launch/error/bulk", {
|
|
280
333
|
body: {
|
|
281
|
-
launchId
|
|
282
|
-
|
|
334
|
+
launchId,
|
|
335
|
+
...(__classPrivateFieldGet(this, _TestOpsClient_session, "f").jobRunId ? { jobRunId: __classPrivateFieldGet(this, _TestOpsClient_session, "f").jobRunId } : {}),
|
|
336
|
+
items: errors.map(({ message, trace }) => ({ message: message?.trim() || UNKNOWN_ERROR_MESSAGE, trace })),
|
|
283
337
|
},
|
|
284
338
|
onUploadProgress(progressEvent) {
|
|
285
339
|
const total = progressEvent.total ?? 100;
|
|
@@ -292,7 +346,7 @@ export class TestOpsClient {
|
|
|
292
346
|
if (!__classPrivateFieldGet(this, _TestOpsClient_session, "f")) {
|
|
293
347
|
throw new Error("Session isn't created! Call createSession first");
|
|
294
348
|
}
|
|
295
|
-
const { trs, environments, attachmentsResolver, fixturesResolver, onProgress } = params;
|
|
349
|
+
const { trs, environments, attachmentsResolver, fixturesResolver, onProgress, onRetry, onChunkUploaded } = params;
|
|
296
350
|
const projectedTrs = trs.map((tr) => ({
|
|
297
351
|
...tr,
|
|
298
352
|
...(tr.steps ? { steps: normalizeTestStepsResults(tr.steps) } : {}),
|
|
@@ -301,39 +355,31 @@ export class TestOpsClient {
|
|
|
301
355
|
const uploadLimitFn = pLimit(__classPrivateFieldGet(this, _TestOpsClient_uploadLimit, "f"));
|
|
302
356
|
const uploadedTrs = [];
|
|
303
357
|
const envNamesById = new Map(environments.map(({ id, name }) => [id, name]));
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
});
|
|
314
|
-
}
|
|
358
|
+
for (const trsChunk of trsChunks) {
|
|
359
|
+
const chunkEnvs = new Map();
|
|
360
|
+
for (const tr of trsChunk) {
|
|
361
|
+
const environmentId = tr.environment;
|
|
362
|
+
if (environmentId && !__classPrivateFieldGet(this, _TestOpsClient_namedEnvsIdsByEnv, "f").has(environmentId)) {
|
|
363
|
+
chunkEnvs.set(environmentId, {
|
|
364
|
+
id: environmentId,
|
|
365
|
+
name: envNamesById.get(environmentId) ?? environmentId,
|
|
366
|
+
});
|
|
315
367
|
}
|
|
316
|
-
if (chunkEnvs.size > 0) {
|
|
317
|
-
await this.createNamedEnvs(Array.from(chunkEnvs.values()));
|
|
318
|
-
}
|
|
319
|
-
const reportIdsToTestOpsIds = await __classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_postTestResultsChunk).call(this, trsChunk);
|
|
320
|
-
uploadedTrs.push(...trsChunk.filter((tr) => typeof reportIdsToTestOpsIds[tr.id] === "number"));
|
|
321
|
-
await __classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_uploadChunkAttachmentsAndFixtures).call(this, trsChunk, reportIdsToTestOpsIds, attachmentsResolver, fixturesResolver, uploadLimitFn, onProgress);
|
|
322
368
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
__classPrivateFieldGet(this,
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
369
|
+
const reportIdsToTestOpsIds = await withUploadRetry(async () => {
|
|
370
|
+
const envsToCreate = Array.from(chunkEnvs.values()).filter((environment) => !__classPrivateFieldGet(this, _TestOpsClient_namedEnvsIdsByEnv, "f").has(environment.id));
|
|
371
|
+
if (envsToCreate.length > 0) {
|
|
372
|
+
await this.createNamedEnvs(envsToCreate);
|
|
373
|
+
}
|
|
374
|
+
await __classPrivateFieldGet(this, _TestOpsClient_uploadPacer, "f").wait({ requests: 1, files: trsChunk.length });
|
|
375
|
+
return __classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_postTestResultsChunk).call(this, trsChunk);
|
|
376
|
+
}, { onRetry });
|
|
377
|
+
const uploadedChunkTrs = trsChunk.filter((tr) => typeof reportIdsToTestOpsIds[tr.id] === "number");
|
|
378
|
+
uploadedTrs.push(...uploadedChunkTrs);
|
|
379
|
+
onChunkUploaded?.(uploadedChunkTrs);
|
|
380
|
+
await __classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_uploadChunkAttachmentsAndFixtures).call(this, trsChunk, reportIdsToTestOpsIds, attachmentsResolver, fixturesResolver, uploadLimitFn, onProgress);
|
|
381
|
+
}
|
|
382
|
+
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").verbose("Test results upload completed");
|
|
337
383
|
return uploadedTrs;
|
|
338
384
|
}
|
|
339
385
|
async uploadQualityGateResults(results, onProgress) {
|
|
@@ -343,6 +389,7 @@ export class TestOpsClient {
|
|
|
343
389
|
if (!__classPrivateFieldGet(this, _TestOpsClient_launch, "f")) {
|
|
344
390
|
throw new Error("Launch isn't created! Call createLaunch first");
|
|
345
391
|
}
|
|
392
|
+
const launchId = __classPrivateFieldGet(this, _TestOpsClient_launch, "f").id;
|
|
346
393
|
const items = results.map((result) => {
|
|
347
394
|
const item = {
|
|
348
395
|
name: result.rule,
|
|
@@ -354,9 +401,10 @@ export class TestOpsClient {
|
|
|
354
401
|
}
|
|
355
402
|
return item;
|
|
356
403
|
});
|
|
357
|
-
await __classPrivateFieldGet(this,
|
|
404
|
+
await __classPrivateFieldGet(this, _TestOpsClient_uploadPacer, "f").wait({ requests: 1 });
|
|
405
|
+
await retryRequest(() => __classPrivateFieldGet(this, _TestOpsClient_client, "f").post("/api/launch/quality-gate/bulk", {
|
|
358
406
|
body: {
|
|
359
|
-
launchId
|
|
407
|
+
launchId,
|
|
360
408
|
items,
|
|
361
409
|
},
|
|
362
410
|
onUploadProgress(progressEvent) {
|
|
@@ -364,10 +412,22 @@ export class TestOpsClient {
|
|
|
364
412
|
const percent = total > 0 ? Math.min(100, Math.max(0, (progressEvent.loaded / total) * 100)) : 0;
|
|
365
413
|
onProgress?.(percent, total);
|
|
366
414
|
},
|
|
367
|
-
});
|
|
415
|
+
}));
|
|
368
416
|
}
|
|
369
417
|
}
|
|
370
|
-
_TestOpsClient_baseUrl = new WeakMap(), _TestOpsClient_logger = new WeakMap(), _TestOpsClient_accessToken = new WeakMap(), _TestOpsClient_projectId = new WeakMap(), _TestOpsClient_client = new WeakMap(), _TestOpsClient_launch = new WeakMap(), _TestOpsClient_session = new WeakMap(), _TestOpsClient_uploadInProgress = new WeakMap(), _TestOpsClient_uploadLimit = new WeakMap(), _TestOpsClient_namedEnvsIdsByEnv = new WeakMap(), _TestOpsClient_instances = new WeakSet(),
|
|
418
|
+
_TestOpsClient_baseUrl = new WeakMap(), _TestOpsClient_logger = new WeakMap(), _TestOpsClient_accessToken = new WeakMap(), _TestOpsClient_projectId = new WeakMap(), _TestOpsClient_client = new WeakMap(), _TestOpsClient_launch = new WeakMap(), _TestOpsClient_jobRunId = new WeakMap(), _TestOpsClient_session = new WeakMap(), _TestOpsClient_uploadInProgress = new WeakMap(), _TestOpsClient_uploadLimit = new WeakMap(), _TestOpsClient_uploadPacer = new WeakMap(), _TestOpsClient_namedEnvsIdsByEnv = new WeakMap(), _TestOpsClient_instances = new WeakSet(), _TestOpsClient_getExistingJob = async function _TestOpsClient_getExistingJob(ci) {
|
|
419
|
+
try {
|
|
420
|
+
return await retryRequest(() => __classPrivateFieldGet(this, _TestOpsClient_client, "f").get("/api/rs/job", {
|
|
421
|
+
params: { projectId: __classPrivateFieldGet(this, _TestOpsClient_projectId, "f"), externalId: ci.jobUid },
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
if (error instanceof KnownError && error.status === 404) {
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
}, _TestOpsClient_postTestResultsChunk = async function _TestOpsClient_postTestResultsChunk(trsChunk) {
|
|
371
431
|
const extendedChunk = trsChunk.map((testResult) => {
|
|
372
432
|
const extendedTestResult = {
|
|
373
433
|
...testResult,
|
|
@@ -404,16 +464,23 @@ _TestOpsClient_baseUrl = new WeakMap(), _TestOpsClient_logger = new WeakMap(), _
|
|
|
404
464
|
if (typeof testOpsId !== "number") {
|
|
405
465
|
return;
|
|
406
466
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
467
|
+
try {
|
|
468
|
+
const attachments = await attachmentsResolver(tr);
|
|
469
|
+
const fixtures = (await fixturesResolver(tr))
|
|
470
|
+
.filter((fixture) => validateExecutableName(fixture.name))
|
|
471
|
+
.map((fixture) => ({
|
|
472
|
+
...fixture,
|
|
473
|
+
...(fixture.steps ? { steps: normalizeTestStepsResults(fixture.steps) } : {}),
|
|
474
|
+
}));
|
|
475
|
+
await __classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_uploadAttachmentsForResult).call(this, testOpsId, attachments);
|
|
476
|
+
await __classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_uploadFixturesForResult).call(this, testOpsId, fixtures);
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
__classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_logResultUploadFailure).call(this, "fixtures", testOpsId, error);
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
onProgress?.();
|
|
483
|
+
}
|
|
417
484
|
})));
|
|
418
485
|
}, _TestOpsClient_uploadAttachmentsForResult = async function _TestOpsClient_uploadAttachmentsForResult(testOpsResultId, attachments) {
|
|
419
486
|
if (attachments.length === 0) {
|
|
@@ -422,36 +489,42 @@ _TestOpsClient_baseUrl = new WeakMap(), _TestOpsClient_logger = new WeakMap(), _
|
|
|
422
489
|
const attachmentsChunks = chunk(attachments, 100);
|
|
423
490
|
for (const attachmentsChunk of attachmentsChunks) {
|
|
424
491
|
const formData = new FormData();
|
|
492
|
+
let chunkBytes = 0;
|
|
425
493
|
for (const att of attachmentsChunk) {
|
|
494
|
+
chunkBytes += attachmentByteLength(att);
|
|
426
495
|
formData.append("file", att.content, {
|
|
427
496
|
filename: att.originalFileName,
|
|
428
497
|
contentType: att.contentType,
|
|
429
498
|
});
|
|
430
499
|
}
|
|
431
500
|
try {
|
|
501
|
+
await __classPrivateFieldGet(this, _TestOpsClient_uploadPacer, "f").wait({ requests: 1, files: attachmentsChunk.length, bytes: chunkBytes });
|
|
432
502
|
await __classPrivateFieldGet(this, _TestOpsClient_client, "f").post(`/api/upload/test-result/${testOpsResultId}/attachment`, {
|
|
433
503
|
body: formData,
|
|
434
504
|
headers: formData.getHeaders(),
|
|
435
505
|
});
|
|
436
506
|
}
|
|
437
507
|
catch (error) {
|
|
438
|
-
|
|
439
|
-
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").error(`Failed to upload attachments for result ${testOpsResultId}: ${error.response?.data.message}`);
|
|
440
|
-
}
|
|
441
|
-
else if (error instanceof Error) {
|
|
442
|
-
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").error(`Failed to upload attachments for result ${testOpsResultId}: ${error.message}`);
|
|
443
|
-
}
|
|
444
|
-
else {
|
|
445
|
-
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").error(`Failed to upload attachments for result ${testOpsResultId}`);
|
|
446
|
-
}
|
|
508
|
+
__classPrivateFieldGet(this, _TestOpsClient_instances, "m", _TestOpsClient_logResultUploadFailure).call(this, "attachments", testOpsResultId, error);
|
|
447
509
|
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").inspect(formData);
|
|
448
510
|
}
|
|
449
511
|
}
|
|
512
|
+
}, _TestOpsClient_logResultUploadFailure = function _TestOpsClient_logResultUploadFailure(label, testOpsResultId, error) {
|
|
513
|
+
if (this.isTestOpsClientError(error)) {
|
|
514
|
+
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").error(`Failed to upload ${label} for result ${testOpsResultId}: ${error.response?.data.message}`);
|
|
515
|
+
}
|
|
516
|
+
else if (error instanceof Error) {
|
|
517
|
+
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").error(`Failed to upload ${label} for result ${testOpsResultId}: ${error.message}`);
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
__classPrivateFieldGet(this, _TestOpsClient_logger, "f").error(`Failed to upload ${label} for result ${testOpsResultId}`);
|
|
521
|
+
}
|
|
450
522
|
}, _TestOpsClient_uploadFixturesForResult = async function _TestOpsClient_uploadFixturesForResult(testOpsResultId, fixtures) {
|
|
451
523
|
if (fixtures.length === 0)
|
|
452
524
|
return;
|
|
453
525
|
const body = toUploadFixturesResultsDto(fixtures);
|
|
454
|
-
await __classPrivateFieldGet(this,
|
|
526
|
+
await __classPrivateFieldGet(this, _TestOpsClient_uploadPacer, "f").wait({ requests: 1 });
|
|
527
|
+
await retryRequest(() => __classPrivateFieldGet(this, _TestOpsClient_client, "f").post(`/api/upload/test-result/${testOpsResultId}/test-fixture-result`, {
|
|
455
528
|
body,
|
|
456
|
-
});
|
|
529
|
+
}));
|
|
457
530
|
};
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare enum ErrorKind {
|
|
2
|
+
None = "none",
|
|
3
|
+
ServiceTransient = "service_transient",
|
|
4
|
+
ResourceRecoverable = "resource_recoverable",
|
|
5
|
+
AuthTerminal = "auth_terminal",
|
|
6
|
+
NotFoundTerminal = "not_found_terminal",
|
|
7
|
+
PayloadTerminal = "payload_terminal",
|
|
8
|
+
ValidationTerminal = "validation_terminal",
|
|
9
|
+
ConflictTerminal = "conflict_terminal",
|
|
10
|
+
Unknown = "unknown"
|
|
11
|
+
}
|
|
12
|
+
export declare const isClosedLaunchError: (error: unknown) => boolean;
|
|
13
|
+
export declare const classifyError: (error: unknown) => ErrorKind;
|
|
14
|
+
export declare const shouldRetryUpload: (error: unknown) => boolean;
|
|
15
|
+
export declare const isPermanentUploadError: (error: unknown) => boolean;
|
|
16
|
+
export declare const isTerminalUploadError: (error: unknown, retries: number, maxRetries: number) => boolean;
|
|
17
|
+
export type RetryOptions = {
|
|
18
|
+
maxRetries?: number;
|
|
19
|
+
baseDelayMs?: number;
|
|
20
|
+
maxDelayMs?: number;
|
|
21
|
+
onRetry?: (error: unknown, attempt: number) => void | Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
export declare const withUploadRetry: <T>(operation: () => Promise<T>, options?: RetryOptions) => Promise<T>;
|