@qualflare/cucumberjs 0.1.0 → 0.3.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 +55 -14
- package/dist/formatter/index.cjs +230 -303
- package/dist/formatter/index.cjs.map +1 -1
- package/dist/formatter/index.d.cts +14 -1
- package/dist/formatter/index.d.ts +14 -1
- package/dist/formatter/index.js +230 -303
- package/dist/formatter/index.js.map +1 -1
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -2
- package/dist/index.d.ts +33 -2
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/formatter/index.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
// src/formatter/formatter.ts
|
|
2
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3
|
+
import * as fs3 from "fs";
|
|
4
|
+
import * as path4 from "path";
|
|
2
5
|
import { Formatter } from "@cucumber/cucumber";
|
|
3
6
|
|
|
7
|
+
// src/config/resolve-config.ts
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
|
|
10
|
+
// src/shared/constants.ts
|
|
11
|
+
var RESERVED_MESSAGE_MEDIA_TYPE = "application/vnd.qualflare.message+json";
|
|
12
|
+
var MAX_SUITES_PER_LAUNCH = 2e3;
|
|
13
|
+
var MAX_CASES_PER_SUITE = 5e3;
|
|
14
|
+
var MAX_TAGS_PER_CASE = 64;
|
|
15
|
+
var MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
16
|
+
var MAX_STEPS_PER_TEST_ATTEMPT = 300;
|
|
17
|
+
|
|
4
18
|
// src/config/ci-detect.ts
|
|
5
19
|
import * as ciInfo from "ci-info";
|
|
6
20
|
function parsePositiveInt(raw) {
|
|
@@ -18,6 +32,7 @@ var PROVIDERS = [
|
|
|
18
32
|
detect: (env) => env.GITHUB_ACTIONS === "true",
|
|
19
33
|
providerName: "GitHub Actions",
|
|
20
34
|
buildNumber: (env) => nonEmpty(env.GITHUB_RUN_NUMBER),
|
|
35
|
+
runId: (env) => nonEmpty(env.GITHUB_RUN_ID),
|
|
21
36
|
runUrl: (env) => env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` : void 0,
|
|
22
37
|
prNumber: (env) => {
|
|
23
38
|
const match = /^refs\/pull\/(\d+)\/merge$/.exec(env.GITHUB_REF ?? "");
|
|
@@ -28,6 +43,7 @@ var PROVIDERS = [
|
|
|
28
43
|
detect: (env) => env.GITLAB_CI === "true",
|
|
29
44
|
providerName: "GitLab CI",
|
|
30
45
|
buildNumber: (env) => nonEmpty(env.CI_PIPELINE_IID),
|
|
46
|
+
runId: (env) => nonEmpty(env.CI_PIPELINE_ID),
|
|
31
47
|
runUrl: (env) => nonEmpty(env.CI_PIPELINE_URL),
|
|
32
48
|
prNumber: (env) => parsePositiveInt(env.CI_MERGE_REQUEST_IID)
|
|
33
49
|
},
|
|
@@ -35,6 +51,7 @@ var PROVIDERS = [
|
|
|
35
51
|
detect: (env) => env.CIRCLECI === "true",
|
|
36
52
|
providerName: "CircleCI",
|
|
37
53
|
buildNumber: (env) => nonEmpty(env.CIRCLE_BUILD_NUM),
|
|
54
|
+
runId: (env) => nonEmpty(env.CIRCLE_WORKFLOW_ID ?? env.CIRCLE_BUILD_NUM),
|
|
38
55
|
runUrl: (env) => nonEmpty(env.CIRCLE_BUILD_URL),
|
|
39
56
|
prNumber: (env) => parsePositiveInt(env.CIRCLE_PR_NUMBER)
|
|
40
57
|
},
|
|
@@ -42,6 +59,7 @@ var PROVIDERS = [
|
|
|
42
59
|
detect: (env) => env.BUILDKITE === "true",
|
|
43
60
|
providerName: "Buildkite",
|
|
44
61
|
buildNumber: (env) => nonEmpty(env.BUILDKITE_BUILD_NUMBER),
|
|
62
|
+
runId: (env) => nonEmpty(env.BUILDKITE_BUILD_ID),
|
|
45
63
|
runUrl: (env) => nonEmpty(env.BUILDKITE_BUILD_URL),
|
|
46
64
|
prNumber: (env) => {
|
|
47
65
|
const raw = env.BUILDKITE_PULL_REQUEST;
|
|
@@ -57,6 +75,7 @@ var PROVIDERS = [
|
|
|
57
75
|
detect: (env) => Boolean(env.JENKINS_URL),
|
|
58
76
|
providerName: "Jenkins",
|
|
59
77
|
buildNumber: (env) => nonEmpty(env.BUILD_NUMBER),
|
|
78
|
+
runId: (env) => nonEmpty(env.BUILD_TAG ?? env.BUILD_NUMBER),
|
|
60
79
|
runUrl: (env) => nonEmpty(env.BUILD_URL)
|
|
61
80
|
// Jenkins has no standardized PR-number env var across its many PR
|
|
62
81
|
// plugins (Multibranch, GitHub Branch Source, etc.) — deliberately
|
|
@@ -66,6 +85,7 @@ var PROVIDERS = [
|
|
|
66
85
|
detect: (env) => env.TF_BUILD === "True" || env.TF_BUILD === "true",
|
|
67
86
|
providerName: "Azure Pipelines",
|
|
68
87
|
buildNumber: (env) => nonEmpty(env.BUILD_BUILDID),
|
|
88
|
+
runId: (env) => nonEmpty(env.BUILD_BUILDID),
|
|
69
89
|
runUrl: (env) => {
|
|
70
90
|
const collectionUri = env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
|
|
71
91
|
const project = env.SYSTEM_TEAMPROJECT;
|
|
@@ -81,6 +101,7 @@ var PROVIDERS = [
|
|
|
81
101
|
detect: (env) => Boolean(env.BITBUCKET_BUILD_NUMBER),
|
|
82
102
|
providerName: "Bitbucket Pipelines",
|
|
83
103
|
buildNumber: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),
|
|
104
|
+
runId: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),
|
|
84
105
|
runUrl: (env) => {
|
|
85
106
|
const origin = env.BITBUCKET_GIT_HTTP_ORIGIN;
|
|
86
107
|
if (!origin) {
|
|
@@ -102,6 +123,8 @@ function detectCi(env = process.env) {
|
|
|
102
123
|
if (runUrl !== void 0) result.ciRunUrl = runUrl;
|
|
103
124
|
const prNumber = provider.prNumber?.(env);
|
|
104
125
|
if (prNumber !== void 0) result.ciPrNumber = prNumber;
|
|
126
|
+
const runId = provider.runId?.(env);
|
|
127
|
+
if (runId !== void 0) result.ciRunId = runId;
|
|
105
128
|
return result;
|
|
106
129
|
}
|
|
107
130
|
if (ciInfo.name) {
|
|
@@ -172,22 +195,30 @@ function envInt(...names) {
|
|
|
172
195
|
const parsed = Number.parseInt(raw, 10);
|
|
173
196
|
return Number.isFinite(parsed) ? parsed : void 0;
|
|
174
197
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
198
|
+
function argvShardIndex(argv = process.argv) {
|
|
199
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
200
|
+
const arg = argv[i];
|
|
201
|
+
if (arg === void 0) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const raw = arg === "--shard" ? argv[i + 1] : arg.startsWith("--shard=") ? arg.slice("--shard=".length) : void 0;
|
|
205
|
+
if (raw === void 0) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (!/^\d+\/\d+$/.test(raw)) {
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
const oneBased = Number.parseInt(raw.split("/")[0] ?? "", 10);
|
|
212
|
+
return Number.isFinite(oneBased) && oneBased >= 1 ? oneBased - 1 : void 0;
|
|
179
213
|
}
|
|
180
|
-
|
|
214
|
+
return void 0;
|
|
215
|
+
}
|
|
181
216
|
function resolveConfig(options, deps = {}) {
|
|
182
217
|
const doDetectGit = deps.detectGit ?? detectGit;
|
|
183
218
|
const doDetectCi = deps.detectCi ?? detectCi;
|
|
184
219
|
const enabled = options.enabled ?? envBool("QUALFLARE_ENABLED") ?? true;
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
throw new QualflareConfigError(
|
|
188
|
-
"qualflare-cucumberjs: no token configured. Set the `token` format option or the QUALFLARE_TOKEN (or QF_TOKEN) environment variable, or pass `enabled: false` to disable this formatter."
|
|
189
|
-
);
|
|
190
|
-
}
|
|
220
|
+
const outputDir = options.outputDir || firstEnv2("QUALFLARE_OUTPUT_DIR") || "./qualflare-results";
|
|
221
|
+
const shardIndex = options.shardIndex ?? envInt("QUALFLARE_SHARD_INDEX") ?? argvShardIndex();
|
|
191
222
|
const milestoneRaw = options.milestone !== void 0 ? options.milestone : envInt("QUALFLARE_MILESTONE", "QF_MILESTONE");
|
|
192
223
|
const milestone = milestoneRaw !== void 0 && milestoneRaw !== null && milestoneRaw >= 1 ? milestoneRaw : null;
|
|
193
224
|
const envBranch = firstEnv2("QUALFLARE_BRANCH", "QF_BRANCH");
|
|
@@ -201,14 +232,11 @@ function resolveConfig(options, deps = {}) {
|
|
|
201
232
|
const ciBuildNumber = options.ciBuildNumber ?? detectedCi.ciBuildNumber;
|
|
202
233
|
const ciRunUrl = options.ciRunUrl ?? detectedCi.ciRunUrl;
|
|
203
234
|
const ciPrNumber = options.ciPrNumber ?? detectedCi.ciPrNumber;
|
|
235
|
+
const runId = options.runId ?? firstEnv2("QUALFLARE_RUN_ID") ?? detectedCi.ciRunId ?? randomUUID();
|
|
204
236
|
return {
|
|
205
|
-
token,
|
|
206
|
-
apiEndpoint: options.apiEndpoint ?? firstEnv2("QUALFLARE_API_ENDPOINT") ?? "https://api.qualflare.com",
|
|
207
237
|
// `||` (truthy check), not `??`, for these three REQUIRED-non-empty wire
|
|
208
238
|
// fields — an explicit `''` option must not silently win over the
|
|
209
|
-
// default (the server rejects an empty `environment
|
|
210
|
-
// `failOnUploadError` defaults `false`, that would fail the entire
|
|
211
|
-
// upload with no visible error by default). Ported verbatim from
|
|
239
|
+
// default (the server rejects an empty `environment`). Ported verbatim from
|
|
212
240
|
// qualflare-cypress, where this was found via deep adversarial review.
|
|
213
241
|
environment: (options.environment || void 0) ?? firstEnv2("QUALFLARE_ENVIRONMENT", "QF_ENVIRONMENT") ?? "development",
|
|
214
242
|
language: (options.language || void 0) ?? firstEnv2("QUALFLARE_LANGUAGE", "QF_LANGUAGE") ?? "en-US",
|
|
@@ -224,38 +252,19 @@ function resolveConfig(options, deps = {}) {
|
|
|
224
252
|
ciBuildNumber,
|
|
225
253
|
ciRunUrl,
|
|
226
254
|
ciPrNumber,
|
|
227
|
-
|
|
228
|
-
retry: {
|
|
229
|
-
max: options.retry?.max ?? envInt("QUALFLARE_RETRY_MAX", "QF_RETRY_MAX") ?? 3,
|
|
230
|
-
baseDelayMs: options.retry?.baseDelayMs ?? envInt("QUALFLARE_RETRY_BASE_DELAY_MS") ?? 1e3,
|
|
231
|
-
maxDelayMs: options.retry?.maxDelayMs ?? envInt("QUALFLARE_RETRY_MAX_DELAY_MS") ?? 3e4
|
|
232
|
-
},
|
|
233
|
-
failOnUploadError: options.failOnUploadError ?? envBool("QUALFLARE_FAIL_ON_UPLOAD_ERROR") ?? false,
|
|
255
|
+
runId,
|
|
234
256
|
attachScreenshots: options.attachScreenshots ?? envBool("QUALFLARE_ATTACH_SCREENSHOTS") ?? true,
|
|
235
257
|
includeStepHooks: options.includeStepHooks ?? envBool("QUALFLARE_INCLUDE_STEP_HOOKS") ?? false,
|
|
236
258
|
maxAttachmentBytes: options.maxAttachmentBytes ?? envInt("QUALFLARE_MAX_ATTACHMENT_BYTES") ?? 15e5,
|
|
237
259
|
maxTotalAttachmentBytes: options.maxTotalAttachmentBytes ?? envInt("QUALFLARE_MAX_TOTAL_ATTACHMENT_BYTES") ?? 75e4,
|
|
260
|
+
maxVideoBytes: options.maxVideoBytes ?? envInt("QUALFLARE_MAX_VIDEO_BYTES") ?? MAX_VIDEO_UPLOAD_BYTES,
|
|
238
261
|
debug: options.debug ?? envBool("QUALFLARE_DEBUG", "QF_DEBUG") ?? false,
|
|
239
|
-
enabled
|
|
262
|
+
enabled,
|
|
263
|
+
outputDir,
|
|
264
|
+
shardIndex
|
|
240
265
|
};
|
|
241
266
|
}
|
|
242
267
|
|
|
243
|
-
// src/http/client.ts
|
|
244
|
-
import { request } from "undici";
|
|
245
|
-
|
|
246
|
-
// src/shared/constants.ts
|
|
247
|
-
var RESERVED_MESSAGE_MEDIA_TYPE = "application/vnd.qualflare.message+json";
|
|
248
|
-
var HEADER_TOKEN = "QF_TOKEN";
|
|
249
|
-
var HEADER_IDEMPOTENCY_KEY = "Idempotency-Key";
|
|
250
|
-
var HEADER_CONTENT_TYPE = "Content-Type";
|
|
251
|
-
var HEADER_ACCEPT = "Accept";
|
|
252
|
-
var HEADER_USER_AGENT = "User-Agent";
|
|
253
|
-
var MAX_SUITES_PER_LAUNCH = 2e3;
|
|
254
|
-
var MAX_CASES_PER_SUITE = 5e3;
|
|
255
|
-
var MAX_TAGS_PER_CASE = 64;
|
|
256
|
-
var MAX_IDEMPOTENCY_KEY_CHARS = 255;
|
|
257
|
-
var MAX_STEPS_PER_TEST_ATTEMPT = 300;
|
|
258
|
-
|
|
259
268
|
// src/shared/logger.ts
|
|
260
269
|
var PREFIX = "[qualflare-cucumberjs]";
|
|
261
270
|
var logger = {
|
|
@@ -273,220 +282,86 @@ var logger = {
|
|
|
273
282
|
}
|
|
274
283
|
};
|
|
275
284
|
|
|
276
|
-
// src/
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const jittered = Math.random() * Math.min(exponential, maxDelayMs);
|
|
280
|
-
const floor = retryAfterMs !== void 0 ? retryAfterMs : 0;
|
|
281
|
-
return Math.min(Math.max(jittered, floor), maxDelayMs);
|
|
282
|
-
}
|
|
285
|
+
// src/formatter/attachment-budget.ts
|
|
286
|
+
import * as fs2 from "fs";
|
|
287
|
+
import * as path2 from "path";
|
|
283
288
|
|
|
284
|
-
// src/
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
289
|
+
// src/formatter/video-writer.ts
|
|
290
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
291
|
+
import * as fs from "fs";
|
|
292
|
+
import * as path from "path";
|
|
293
|
+
var VIDEO_MIME_TYPES_BY_EXTENSION = {
|
|
294
|
+
".mp4": "video/mp4",
|
|
295
|
+
".webm": "video/webm",
|
|
296
|
+
".mov": "video/quicktime"
|
|
297
|
+
};
|
|
298
|
+
var EXTENSION_BY_VIDEO_MIME_TYPE = {
|
|
299
|
+
"video/mp4": ".mp4",
|
|
300
|
+
"video/webm": ".webm",
|
|
301
|
+
"video/quicktime": ".mov"
|
|
302
|
+
};
|
|
303
|
+
function resolveVideoMimeType(mimeType, filePath) {
|
|
304
|
+
if (filePath) {
|
|
305
|
+
const extension2 = path.extname(filePath).toLowerCase();
|
|
306
|
+
const resolvedMimeType = VIDEO_MIME_TYPES_BY_EXTENSION[extension2];
|
|
307
|
+
return resolvedMimeType ? { mimeType: resolvedMimeType, extension: extension2 } : void 0;
|
|
308
|
+
}
|
|
309
|
+
const normalized = mimeType?.toLowerCase();
|
|
310
|
+
const extension = normalized ? EXTENSION_BY_VIDEO_MIME_TYPE[normalized] : void 0;
|
|
311
|
+
return normalized && extension ? { mimeType: normalized, extension } : void 0;
|
|
308
312
|
}
|
|
309
|
-
function
|
|
310
|
-
|
|
313
|
+
function writeVideoAttachment(pending, outputDir, maxVideoBytes) {
|
|
314
|
+
const resolved = resolveVideoMimeType(pending.mimeType, pending.path);
|
|
315
|
+
if (!resolved) {
|
|
316
|
+
logger.warn(`skipping video attachment "${pending.name}": unsupported video format.`);
|
|
311
317
|
return void 0;
|
|
312
318
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
requestId;
|
|
323
|
-
fields;
|
|
324
|
-
constructor(init) {
|
|
325
|
-
const parts = [init.message];
|
|
326
|
-
const fieldsRendered = renderFields(init.fields);
|
|
327
|
-
if (fieldsRendered) {
|
|
328
|
-
parts.push(`fields: ${fieldsRendered}`);
|
|
319
|
+
const localVideoPath = `${randomUUID2()}${resolved.extension}`;
|
|
320
|
+
const destination = path.join(outputDir, localVideoPath);
|
|
321
|
+
if (pending.path !== void 0) {
|
|
322
|
+
let fileSize;
|
|
323
|
+
try {
|
|
324
|
+
fileSize = fs.statSync(pending.path).size;
|
|
325
|
+
} catch (err) {
|
|
326
|
+
logger.warn(`skipping video attachment "${pending.path}": could not stat file: ${err.message}`);
|
|
327
|
+
return void 0;
|
|
329
328
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
329
|
+
if (fileSize > maxVideoBytes) {
|
|
330
|
+
logger.warn(
|
|
331
|
+
`skipping video attachment "${pending.path}": ${fileSize} bytes exceeds the configured maxVideoBytes cap of ${maxVideoBytes} bytes.`
|
|
332
|
+
);
|
|
333
|
+
return void 0;
|
|
333
334
|
}
|
|
334
|
-
|
|
335
|
-
|
|
335
|
+
try {
|
|
336
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
337
|
+
fs.copyFileSync(pending.path, destination);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
logger.warn(`skipping video attachment "${pending.path}": could not copy file: ${err.message}`);
|
|
340
|
+
return void 0;
|
|
336
341
|
}
|
|
337
|
-
|
|
338
|
-
this.name = "QualflareApiError";
|
|
339
|
-
this.code = init.code;
|
|
340
|
-
this.statusCode = init.statusCode;
|
|
341
|
-
this.requestId = init.requestId;
|
|
342
|
-
this.fields = init.fields;
|
|
342
|
+
return { localVideoPath, fileSize, mimeType: resolved.mimeType };
|
|
343
343
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
message,
|
|
350
|
-
code,
|
|
351
|
-
statusCode,
|
|
352
|
-
requestId: body?.request_id,
|
|
353
|
-
fields: body?.fields
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// src/http/idempotency.ts
|
|
358
|
-
import { randomUUID } from "crypto";
|
|
359
|
-
function newIdempotencyKey() {
|
|
360
|
-
const key = randomUUID();
|
|
361
|
-
if (key.length > MAX_IDEMPOTENCY_KEY_CHARS) {
|
|
362
|
-
return key.slice(0, MAX_IDEMPOTENCY_KEY_CHARS);
|
|
363
|
-
}
|
|
364
|
-
return key;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// src/http/client.ts
|
|
368
|
-
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
369
|
-
function redactToken(token) {
|
|
370
|
-
return token.length > 0 ? "***REDACTED***" : "(none)";
|
|
371
|
-
}
|
|
372
|
-
var QualflareHttpClient = class {
|
|
373
|
-
constructor(opts) {
|
|
374
|
-
this.opts = opts;
|
|
375
|
-
}
|
|
376
|
-
opts;
|
|
377
|
-
async send(collect) {
|
|
378
|
-
const url = `${this.opts.endpoint.replace(/\/+$/, "")}/api/v1/collect`;
|
|
379
|
-
const idempotencyKey = newIdempotencyKey();
|
|
380
|
-
const body = JSON.stringify(collect);
|
|
381
|
-
const maxAttempts = Math.max(1, this.opts.retry.max + 1);
|
|
382
|
-
let lastError;
|
|
383
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
384
|
-
if (this.opts.debug) {
|
|
385
|
-
logger.debug(
|
|
386
|
-
`POST ${url} (attempt ${attempt}/${maxAttempts}, QF_TOKEN: ${redactToken(this.opts.token)})`
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
let statusCode;
|
|
390
|
-
let responseBody;
|
|
391
|
-
let responseHeaders;
|
|
392
|
-
try {
|
|
393
|
-
const res = await request(url, {
|
|
394
|
-
method: "POST",
|
|
395
|
-
headers: {
|
|
396
|
-
[HEADER_TOKEN]: this.opts.token,
|
|
397
|
-
[HEADER_CONTENT_TYPE]: "application/json",
|
|
398
|
-
[HEADER_ACCEPT]: "application/json",
|
|
399
|
-
[HEADER_USER_AGENT]: this.opts.userAgent,
|
|
400
|
-
[HEADER_IDEMPOTENCY_KEY]: idempotencyKey
|
|
401
|
-
},
|
|
402
|
-
body,
|
|
403
|
-
maxRedirections: 0,
|
|
404
|
-
signal: AbortSignal.timeout(this.opts.timeoutMs)
|
|
405
|
-
});
|
|
406
|
-
statusCode = res.statusCode;
|
|
407
|
-
responseHeaders = res.headers;
|
|
408
|
-
responseBody = await res.body.text();
|
|
409
|
-
} catch (err) {
|
|
410
|
-
lastError = err;
|
|
411
|
-
if (this.opts.debug) {
|
|
412
|
-
logger.debug(`attempt ${attempt} transport error: ${err?.message ?? err}`);
|
|
413
|
-
}
|
|
414
|
-
if (attempt < maxAttempts) {
|
|
415
|
-
await sleep(computeDelay(attempt, this.opts.retry.baseDelayMs, this.opts.retry.maxDelayMs));
|
|
416
|
-
continue;
|
|
417
|
-
}
|
|
418
|
-
throw new QualflareApiError({
|
|
419
|
-
message: `failed to send request to ${url}`,
|
|
420
|
-
cause: err
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
if (this.opts.debug) {
|
|
424
|
-
logger.debug(`attempt ${attempt} response: ${statusCode}`);
|
|
425
|
-
}
|
|
426
|
-
if (statusCode >= 200 && statusCode < 300) {
|
|
427
|
-
return parseSuccess(responseBody);
|
|
428
|
-
}
|
|
429
|
-
const parsedError = parseErrorBody(responseBody);
|
|
430
|
-
if (!RETRYABLE_STATUS_CODES.has(statusCode) || attempt === maxAttempts) {
|
|
431
|
-
throw buildApiError(statusCode, parsedError);
|
|
432
|
-
}
|
|
433
|
-
const retryAfterMs = parseRetryAfter(responseHeaders["retry-after"]);
|
|
434
|
-
const delay = computeDelay(
|
|
435
|
-
attempt,
|
|
436
|
-
this.opts.retry.baseDelayMs,
|
|
437
|
-
this.opts.retry.maxDelayMs,
|
|
438
|
-
retryAfterMs
|
|
344
|
+
if (pending.content !== void 0) {
|
|
345
|
+
const fileSize = Buffer.byteLength(pending.content, "base64");
|
|
346
|
+
if (fileSize > maxVideoBytes) {
|
|
347
|
+
logger.warn(
|
|
348
|
+
`skipping video attachment "${pending.name}": ${fileSize} bytes exceeds the configured maxVideoBytes cap of ${maxVideoBytes} bytes.`
|
|
439
349
|
);
|
|
440
|
-
|
|
441
|
-
logger.debug(`retrying after ${Math.round(delay)}ms (status ${statusCode})`);
|
|
442
|
-
}
|
|
443
|
-
await sleep(delay);
|
|
350
|
+
return void 0;
|
|
444
351
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (typeof parsed.seq !== "number") {
|
|
452
|
-
throw new Error('response body missing numeric "seq"');
|
|
352
|
+
try {
|
|
353
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
354
|
+
fs.writeFileSync(destination, Buffer.from(pending.content, "base64"));
|
|
355
|
+
} catch (err) {
|
|
356
|
+
logger.warn(`skipping video attachment "${pending.name}": could not write file: ${err.message}`);
|
|
357
|
+
return void 0;
|
|
453
358
|
}
|
|
454
|
-
return
|
|
455
|
-
} catch (err) {
|
|
456
|
-
throw new QualflareApiError({
|
|
457
|
-
message: "server returned a success status but an unparseable body",
|
|
458
|
-
cause: err
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
function parseErrorBody(responseBody) {
|
|
463
|
-
if (!responseBody) {
|
|
464
|
-
return void 0;
|
|
465
|
-
}
|
|
466
|
-
try {
|
|
467
|
-
return JSON.parse(responseBody);
|
|
468
|
-
} catch {
|
|
469
|
-
return void 0;
|
|
359
|
+
return { localVideoPath, fileSize, mimeType: resolved.mimeType };
|
|
470
360
|
}
|
|
361
|
+
return void 0;
|
|
471
362
|
}
|
|
472
|
-
function parseRetryAfter(value) {
|
|
473
|
-
const raw = Array.isArray(value) ? value[0] : value;
|
|
474
|
-
if (!raw) {
|
|
475
|
-
return void 0;
|
|
476
|
-
}
|
|
477
|
-
const seconds = Number(raw);
|
|
478
|
-
return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : void 0;
|
|
479
|
-
}
|
|
480
|
-
function sleep(ms) {
|
|
481
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// src/config/version.ts
|
|
485
|
-
var PACKAGE_VERSION = "0.1.0";
|
|
486
363
|
|
|
487
364
|
// src/formatter/attachment-budget.ts
|
|
488
|
-
import * as fs from "fs";
|
|
489
|
-
import * as path from "path";
|
|
490
365
|
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".webm", ".mov", ".avi", ".mkv"]);
|
|
491
366
|
var AttachmentBudget = class {
|
|
492
367
|
constructor(maxTotalBytes) {
|
|
@@ -511,7 +386,7 @@ function isVideoLike(mimeType, filePath) {
|
|
|
511
386
|
if (mimeType?.toLowerCase().startsWith("video/")) {
|
|
512
387
|
return true;
|
|
513
388
|
}
|
|
514
|
-
if (filePath && VIDEO_EXTENSIONS.has(
|
|
389
|
+
if (filePath && VIDEO_EXTENSIONS.has(path2.extname(filePath).toLowerCase())) {
|
|
515
390
|
return true;
|
|
516
391
|
}
|
|
517
392
|
return false;
|
|
@@ -519,7 +394,7 @@ function isVideoLike(mimeType, filePath) {
|
|
|
519
394
|
function readAttachmentFile(filePath, maxAttachmentBytes, budget) {
|
|
520
395
|
let size;
|
|
521
396
|
try {
|
|
522
|
-
size =
|
|
397
|
+
size = fs2.statSync(filePath).size;
|
|
523
398
|
} catch (err) {
|
|
524
399
|
return { skipped: true, reason: `could not stat file: ${err.message}` };
|
|
525
400
|
}
|
|
@@ -536,7 +411,7 @@ function readAttachmentFile(filePath, maxAttachmentBytes, budget) {
|
|
|
536
411
|
};
|
|
537
412
|
}
|
|
538
413
|
try {
|
|
539
|
-
const content =
|
|
414
|
+
const content = fs2.readFileSync(filePath).toString("base64");
|
|
540
415
|
return { skipped: false, content };
|
|
541
416
|
} catch (err) {
|
|
542
417
|
return { skipped: true, reason: `could not read file: ${err.message}` };
|
|
@@ -546,12 +421,6 @@ function resolvePendingAttachment(pending, config, budget) {
|
|
|
546
421
|
if (!config.attachScreenshots) {
|
|
547
422
|
return void 0;
|
|
548
423
|
}
|
|
549
|
-
if (isVideoLike(pending.mimeType, pending.path)) {
|
|
550
|
-
logger.warn(
|
|
551
|
-
`refusing to attach "${pending.name}": video attachments are not supported yet (${pending.path ?? pending.mimeType ?? "unknown"}).`
|
|
552
|
-
);
|
|
553
|
-
return void 0;
|
|
554
|
-
}
|
|
555
424
|
if (pending.content !== void 0) {
|
|
556
425
|
const bytes = Buffer.byteLength(pending.content, "base64");
|
|
557
426
|
if (bytes > config.maxAttachmentBytes) {
|
|
@@ -584,6 +453,22 @@ function resolvePendingAttachment(pending, config, budget) {
|
|
|
584
453
|
}
|
|
585
454
|
return void 0;
|
|
586
455
|
}
|
|
456
|
+
async function resolveVideoAttachment(pending, config) {
|
|
457
|
+
if (!config.attachScreenshots) {
|
|
458
|
+
return void 0;
|
|
459
|
+
}
|
|
460
|
+
const written = writeVideoAttachment(pending, config.outputDir, config.maxVideoBytes);
|
|
461
|
+
if (!written) {
|
|
462
|
+
return void 0;
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
name: pending.name,
|
|
466
|
+
mimeType: written.mimeType,
|
|
467
|
+
localVideoPath: written.localVideoPath,
|
|
468
|
+
fileSize: written.fileSize,
|
|
469
|
+
stepIndex: pending.stepIndex
|
|
470
|
+
};
|
|
471
|
+
}
|
|
587
472
|
|
|
588
473
|
// src/formatter/attempt-tracker.ts
|
|
589
474
|
import {
|
|
@@ -832,7 +717,8 @@ var AttemptTracker = class {
|
|
|
832
717
|
tags: [],
|
|
833
718
|
properties: {},
|
|
834
719
|
attachments: [],
|
|
835
|
-
stepCapWarned: false
|
|
720
|
+
stepCapWarned: false,
|
|
721
|
+
pendingVideoWrites: []
|
|
836
722
|
};
|
|
837
723
|
this.byTestCaseStartedId.set(e.id, record);
|
|
838
724
|
const attempts = this.byTestCaseId.get(testCase.id) ?? [];
|
|
@@ -910,11 +796,25 @@ var AttemptTracker = class {
|
|
|
910
796
|
}
|
|
911
797
|
const stepIndex = this.resolveStepIndex(record, e.testStepId);
|
|
912
798
|
const content = e.contentEncoding === "BASE64" ? e.body : Buffer.from(e.body, "utf8").toString("base64");
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
799
|
+
this.resolveAttachment(record, { name: e.fileName || "attachment", mimeType: e.mediaType, content, stepIndex });
|
|
800
|
+
}
|
|
801
|
+
/** Resolves one pending attachment, routing a video-like one through the
|
|
802
|
+
* write-to-`outputDir` flow (tracked in `record.pendingVideoWrites` so
|
|
803
|
+
* `finish()` can wait for it) and everything else through the synchronous
|
|
804
|
+
* inline path — shared by the real `World.attach()` handler above and both
|
|
805
|
+
* `qualflare.attachment()`/`attachmentFromFile()` runtime-message cases
|
|
806
|
+
* below. */
|
|
807
|
+
resolveAttachment(record, pending) {
|
|
808
|
+
if (isVideoLike(pending.mimeType, pending.path)) {
|
|
809
|
+
const write = resolveVideoAttachment(pending, this.config).then((resolved2) => {
|
|
810
|
+
if (resolved2) {
|
|
811
|
+
record.attachments.push(resolved2);
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
record.pendingVideoWrites.push(write);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const resolved = resolvePendingAttachment(pending, this.config, this.attachmentBudget);
|
|
918
818
|
if (resolved) {
|
|
919
819
|
record.attachments.push(resolved);
|
|
920
820
|
}
|
|
@@ -971,26 +871,12 @@ var AttemptTracker = class {
|
|
|
971
871
|
}
|
|
972
872
|
case "attachment": {
|
|
973
873
|
const stepIndex = testStepId !== void 0 ? record.stepIndexByTestStepId.get(testStepId) : void 0;
|
|
974
|
-
|
|
975
|
-
{ name: message.name, mimeType: message.mimeType, content: message.contentBase64, stepIndex },
|
|
976
|
-
this.config,
|
|
977
|
-
this.attachmentBudget
|
|
978
|
-
);
|
|
979
|
-
if (resolved) {
|
|
980
|
-
record.attachments.push(resolved);
|
|
981
|
-
}
|
|
874
|
+
this.resolveAttachment(record, { name: message.name, mimeType: message.mimeType, content: message.contentBase64, stepIndex });
|
|
982
875
|
return;
|
|
983
876
|
}
|
|
984
877
|
case "attachment_from_file": {
|
|
985
878
|
const stepIndex = testStepId !== void 0 ? record.stepIndexByTestStepId.get(testStepId) : void 0;
|
|
986
|
-
|
|
987
|
-
{ name: message.name, mimeType: message.mimeType, path: message.path, stepIndex },
|
|
988
|
-
this.config,
|
|
989
|
-
this.attachmentBudget
|
|
990
|
-
);
|
|
991
|
-
if (resolved) {
|
|
992
|
-
record.attachments.push(resolved);
|
|
993
|
-
}
|
|
879
|
+
this.resolveAttachment(record, { name: message.name, mimeType: message.mimeType, path: message.path, stepIndex });
|
|
994
880
|
return;
|
|
995
881
|
}
|
|
996
882
|
case "step_start": {
|
|
@@ -1015,8 +901,21 @@ var AttemptTracker = class {
|
|
|
1015
901
|
}
|
|
1016
902
|
/** Returns the collapsed result once all attempts of this logical
|
|
1017
903
|
* scenario have arrived, or `undefined` if more attempts are coming
|
|
1018
|
-
* (`willBeRetried === true`).
|
|
1019
|
-
|
|
904
|
+
* (`willBeRetried === true`).
|
|
905
|
+
*
|
|
906
|
+
* Async because it must first await every attempt's own
|
|
907
|
+
* `pendingVideoWrites` (any video attached anywhere across every retry
|
|
908
|
+
* of this scenario). This is load-bearing, not just tidiness:
|
|
909
|
+
* `collapseAttempts`/`buildCase` read `attachments` by REFERENCE, not by
|
|
910
|
+
* copy, and `buildCase` runs synchronously right after this resolves — a
|
|
911
|
+
* scenario whose ONLY attachment is a still-uploading video would have an
|
|
912
|
+
* EMPTY `attachments` array at that instant, and `buildCase` captures
|
|
913
|
+
* `attachments.length > 0 ? attachments : undefined` as a plain
|
|
914
|
+
* `undefined` VALUE right then, permanently — a later push onto the
|
|
915
|
+
* (still-live) array reference would no longer be visible through
|
|
916
|
+
* `undefined`. Awaiting here first guarantees `attachments` is complete
|
|
917
|
+
* before `buildCase` ever reads it. */
|
|
918
|
+
async finish(e) {
|
|
1020
919
|
const record = this.byTestCaseStartedId.get(e.testCaseStartedId);
|
|
1021
920
|
this.byTestCaseStartedId.delete(e.testCaseStartedId);
|
|
1022
921
|
if (!record) {
|
|
@@ -1028,6 +927,10 @@ var AttemptTracker = class {
|
|
|
1028
927
|
const testCaseId = record.testCase.id;
|
|
1029
928
|
const attempts = this.byTestCaseId.get(testCaseId) ?? [record];
|
|
1030
929
|
this.byTestCaseId.delete(testCaseId);
|
|
930
|
+
const pendingWrites = attempts.flatMap((a) => a.pendingVideoWrites);
|
|
931
|
+
if (pendingWrites.length > 0) {
|
|
932
|
+
await Promise.all(pendingWrites);
|
|
933
|
+
}
|
|
1031
934
|
const snapshots = attempts.map((a) => {
|
|
1032
935
|
const worst = a.stepResults.length > 0 ? getWorstTestStepResult(a.stepResults) : void 0;
|
|
1033
936
|
const duration = a.stepResults.reduce((sum, r) => sum + messageDurationToNs(r.duration), 0);
|
|
@@ -1062,6 +965,11 @@ function timestampMs(ts) {
|
|
|
1062
965
|
|
|
1063
966
|
// src/formatter/collect-builder.ts
|
|
1064
967
|
import * as os from "os";
|
|
968
|
+
|
|
969
|
+
// src/config/version.ts
|
|
970
|
+
var PACKAGE_VERSION = "0.3.0";
|
|
971
|
+
|
|
972
|
+
// src/formatter/collect-builder.ts
|
|
1065
973
|
function resolveOs(config) {
|
|
1066
974
|
if (config.os) {
|
|
1067
975
|
return config.os;
|
|
@@ -1082,7 +990,8 @@ function buildCollectPayload(suites, config) {
|
|
|
1082
990
|
metadata: {
|
|
1083
991
|
version: PACKAGE_VERSION,
|
|
1084
992
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1085
|
-
cliName: "qualflare-cucumberjs"
|
|
993
|
+
cliName: "qualflare-cucumberjs",
|
|
994
|
+
runId: config.runId
|
|
1086
995
|
},
|
|
1087
996
|
properties: config.properties,
|
|
1088
997
|
suites,
|
|
@@ -1221,7 +1130,7 @@ var RunHookTracker = class {
|
|
|
1221
1130
|
}
|
|
1222
1131
|
return {
|
|
1223
1132
|
name: "(global hooks)",
|
|
1224
|
-
category: "
|
|
1133
|
+
category: "cucumber",
|
|
1225
1134
|
duration: this.failed.reduce((sum, c) => sum + c.duration, 0),
|
|
1226
1135
|
cases: this.failed
|
|
1227
1136
|
};
|
|
@@ -1229,7 +1138,7 @@ var RunHookTracker = class {
|
|
|
1229
1138
|
};
|
|
1230
1139
|
|
|
1231
1140
|
// src/formatter/suite-builder.ts
|
|
1232
|
-
import * as
|
|
1141
|
+
import * as path3 from "path";
|
|
1233
1142
|
function groupIntoSuites(cases, cwd, extraSuite) {
|
|
1234
1143
|
const byUri = /* @__PURE__ */ new Map();
|
|
1235
1144
|
for (const { uri, case: kase } of cases) {
|
|
@@ -1249,7 +1158,7 @@ function groupIntoSuites(cases, cwd, extraSuite) {
|
|
|
1249
1158
|
}
|
|
1250
1159
|
suites.push({
|
|
1251
1160
|
name: relativizeUri(uri, cwd),
|
|
1252
|
-
category: "
|
|
1161
|
+
category: "cucumber",
|
|
1253
1162
|
duration: kases.reduce((sum, c) => sum + c.duration, 0),
|
|
1254
1163
|
cases: kases.slice(0, MAX_CASES_PER_SUITE)
|
|
1255
1164
|
});
|
|
@@ -1269,10 +1178,10 @@ function relativizeUri(uri, cwd) {
|
|
|
1269
1178
|
if (normalized.startsWith("file://")) {
|
|
1270
1179
|
normalized = new URL(normalized).pathname;
|
|
1271
1180
|
}
|
|
1272
|
-
if (
|
|
1273
|
-
normalized =
|
|
1181
|
+
if (path3.isAbsolute(normalized)) {
|
|
1182
|
+
normalized = path3.relative(cwd, normalized);
|
|
1274
1183
|
}
|
|
1275
|
-
return normalized.split(
|
|
1184
|
+
return normalized.split(path3.sep).join("/");
|
|
1276
1185
|
}
|
|
1277
1186
|
|
|
1278
1187
|
// src/formatter/formatter.ts
|
|
@@ -1286,12 +1195,25 @@ var QualflareCucumberFormatter = class extends Formatter {
|
|
|
1286
1195
|
attemptTracker;
|
|
1287
1196
|
runHookTracker = new RunHookTracker();
|
|
1288
1197
|
finishedCases = [];
|
|
1198
|
+
/** One promise per `testCaseFinished` envelope, resolving once that
|
|
1199
|
+
* scenario's `AttemptTracker.finish()` (which itself awaits any pending
|
|
1200
|
+
* video uploads — see its doc comment) has settled and, if it produced a
|
|
1201
|
+
* result, been pushed into `finishedCases`. `finished()` awaits all of
|
|
1202
|
+
* these before building/uploading the Collect payload, so a scenario
|
|
1203
|
+
* whose only attachment is a still-uploading video is never silently
|
|
1204
|
+
* dropped from the report. */
|
|
1205
|
+
pendingCaseBuilds = [];
|
|
1289
1206
|
constructor(options) {
|
|
1290
1207
|
super(options);
|
|
1291
1208
|
this.config = resolveConfig(options.parsedArgvOptions);
|
|
1292
1209
|
this.hookIndex = buildHookIndex(options.supportCodeLibrary);
|
|
1293
1210
|
this.attachmentBudget = new AttachmentBudget(this.config.maxTotalAttachmentBytes);
|
|
1294
|
-
this.attemptTracker = new AttemptTracker(
|
|
1211
|
+
this.attemptTracker = new AttemptTracker(
|
|
1212
|
+
this.hookIndex,
|
|
1213
|
+
this.gherkin,
|
|
1214
|
+
this.config,
|
|
1215
|
+
this.attachmentBudget
|
|
1216
|
+
);
|
|
1295
1217
|
if (!this.config.enabled) {
|
|
1296
1218
|
return;
|
|
1297
1219
|
}
|
|
@@ -1342,15 +1264,20 @@ var QualflareCucumberFormatter = class extends Formatter {
|
|
|
1342
1264
|
return;
|
|
1343
1265
|
}
|
|
1344
1266
|
if (envelope.testCaseFinished) {
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1267
|
+
const pending = this.attemptTracker.finish(envelope.testCaseFinished).then((finished) => {
|
|
1268
|
+
if (!finished) {
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1347
1271
|
const pickle = this.pickleIndex.get(finished.pickleId);
|
|
1348
1272
|
if (pickle) {
|
|
1349
1273
|
this.finishedCases.push(buildCase(finished.uri, pickle, finished.collapsed, this.gherkin));
|
|
1350
1274
|
} else {
|
|
1351
1275
|
logger.warn(`could not resolve pickle "${finished.pickleId}" for a finished scenario \u2014 it will not be uploaded.`);
|
|
1352
1276
|
}
|
|
1353
|
-
}
|
|
1277
|
+
}).catch((err) => {
|
|
1278
|
+
logger.error("failed to process a cucumber-js event:", err);
|
|
1279
|
+
});
|
|
1280
|
+
this.pendingCaseBuilds.push(pending);
|
|
1354
1281
|
return;
|
|
1355
1282
|
}
|
|
1356
1283
|
if (envelope.testRunHookStarted) {
|
|
@@ -1367,41 +1294,41 @@ var QualflareCucumberFormatter = class extends Formatter {
|
|
|
1367
1294
|
}
|
|
1368
1295
|
async finished() {
|
|
1369
1296
|
try {
|
|
1297
|
+
await Promise.all(this.pendingCaseBuilds);
|
|
1370
1298
|
if (this.config.enabled) {
|
|
1371
|
-
|
|
1299
|
+
this.writeResults();
|
|
1372
1300
|
}
|
|
1373
1301
|
} finally {
|
|
1374
1302
|
await super.finished();
|
|
1375
1303
|
}
|
|
1376
1304
|
}
|
|
1377
|
-
|
|
1305
|
+
/** Writes this process's Collect payload into `outputDir` under a unique
|
|
1306
|
+
* filename. Never uploads: `qualflare-cli collect <outputDir>` does that,
|
|
1307
|
+
* merging every file it finds there into one Launch. Multiple shards can
|
|
1308
|
+
* therefore share one directory safely — the UUID filename is what keeps
|
|
1309
|
+
* them from overwriting each other. */
|
|
1310
|
+
writeResults() {
|
|
1378
1311
|
const suites = groupIntoSuites(this.finishedCases, this.cwd, this.runHookTracker.buildSuite());
|
|
1379
1312
|
if (suites.length === 0) {
|
|
1380
1313
|
if (this.config.debug) {
|
|
1381
|
-
logger.debug("no scenarios reported \u2014 skipping
|
|
1314
|
+
logger.debug("no scenarios reported \u2014 skipping file write.");
|
|
1382
1315
|
}
|
|
1383
1316
|
return;
|
|
1384
1317
|
}
|
|
1385
1318
|
const payload = buildCollectPayload(suites, this.config);
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
userAgent: `qualflare-cucumberjs/${PACKAGE_VERSION}`,
|
|
1392
|
-
debug: this.config.debug
|
|
1393
|
-
});
|
|
1394
|
-
try {
|
|
1395
|
-
const result = await client.send(payload);
|
|
1396
|
-
if (this.config.debug) {
|
|
1397
|
-
logger.debug(`uploaded launch #${result.seq}.`);
|
|
1398
|
-
}
|
|
1399
|
-
} catch (err) {
|
|
1400
|
-
if (this.config.failOnUploadError) {
|
|
1401
|
-
throw err;
|
|
1319
|
+
if (this.config.shardIndex !== void 0) {
|
|
1320
|
+
for (const suite of payload.suites) {
|
|
1321
|
+
for (const c of suite.cases) {
|
|
1322
|
+
c.shardIndex = this.config.shardIndex;
|
|
1323
|
+
}
|
|
1402
1324
|
}
|
|
1403
|
-
logger.error("failed to upload results to Qualflare:", err);
|
|
1404
1325
|
}
|
|
1326
|
+
fs3.mkdirSync(this.config.outputDir, { recursive: true });
|
|
1327
|
+
const outputPath = path4.join(this.config.outputDir, `${randomUUID3()}.json`);
|
|
1328
|
+
fs3.writeFileSync(outputPath, JSON.stringify(payload));
|
|
1329
|
+
logger.info(
|
|
1330
|
+
`wrote Collect payload to ${outputPath} \u2014 run \`qualflare-cli collect ${this.config.outputDir}\` to upload it.`
|
|
1331
|
+
);
|
|
1405
1332
|
}
|
|
1406
1333
|
};
|
|
1407
1334
|
export {
|