@qualflare/cucumberjs 0.2.0 → 0.4.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 +9 -3
- package/dist/formatter/index.cjs +59 -7
- package/dist/formatter/index.cjs.map +1 -1
- package/dist/formatter/index.js +59 -7
- package/dist/formatter/index.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/formatter/index.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
// src/formatter/formatter.ts
|
|
2
|
-
import { randomUUID as
|
|
2
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3
3
|
import * as fs3 from "fs";
|
|
4
4
|
import * as path4 from "path";
|
|
5
5
|
import { Formatter } from "@cucumber/cucumber";
|
|
6
6
|
|
|
7
|
+
// src/config/resolve-config.ts
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
|
|
7
10
|
// src/shared/constants.ts
|
|
8
11
|
var RESERVED_MESSAGE_MEDIA_TYPE = "application/vnd.qualflare.message+json";
|
|
9
12
|
var MAX_SUITES_PER_LAUNCH = 2e3;
|
|
10
13
|
var MAX_CASES_PER_SUITE = 5e3;
|
|
11
14
|
var MAX_TAGS_PER_CASE = 64;
|
|
15
|
+
var MAX_ATTEMPTS_PER_CASE = 50;
|
|
16
|
+
var MAX_ATTEMPT_MESSAGE_RUNES = 8192;
|
|
12
17
|
var MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
13
18
|
var MAX_STEPS_PER_TEST_ATTEMPT = 300;
|
|
14
19
|
|
|
@@ -29,6 +34,7 @@ var PROVIDERS = [
|
|
|
29
34
|
detect: (env) => env.GITHUB_ACTIONS === "true",
|
|
30
35
|
providerName: "GitHub Actions",
|
|
31
36
|
buildNumber: (env) => nonEmpty(env.GITHUB_RUN_NUMBER),
|
|
37
|
+
runId: (env) => nonEmpty(env.GITHUB_RUN_ID),
|
|
32
38
|
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,
|
|
33
39
|
prNumber: (env) => {
|
|
34
40
|
const match = /^refs\/pull\/(\d+)\/merge$/.exec(env.GITHUB_REF ?? "");
|
|
@@ -39,6 +45,7 @@ var PROVIDERS = [
|
|
|
39
45
|
detect: (env) => env.GITLAB_CI === "true",
|
|
40
46
|
providerName: "GitLab CI",
|
|
41
47
|
buildNumber: (env) => nonEmpty(env.CI_PIPELINE_IID),
|
|
48
|
+
runId: (env) => nonEmpty(env.CI_PIPELINE_ID),
|
|
42
49
|
runUrl: (env) => nonEmpty(env.CI_PIPELINE_URL),
|
|
43
50
|
prNumber: (env) => parsePositiveInt(env.CI_MERGE_REQUEST_IID)
|
|
44
51
|
},
|
|
@@ -46,6 +53,7 @@ var PROVIDERS = [
|
|
|
46
53
|
detect: (env) => env.CIRCLECI === "true",
|
|
47
54
|
providerName: "CircleCI",
|
|
48
55
|
buildNumber: (env) => nonEmpty(env.CIRCLE_BUILD_NUM),
|
|
56
|
+
runId: (env) => nonEmpty(env.CIRCLE_WORKFLOW_ID ?? env.CIRCLE_BUILD_NUM),
|
|
49
57
|
runUrl: (env) => nonEmpty(env.CIRCLE_BUILD_URL),
|
|
50
58
|
prNumber: (env) => parsePositiveInt(env.CIRCLE_PR_NUMBER)
|
|
51
59
|
},
|
|
@@ -53,6 +61,7 @@ var PROVIDERS = [
|
|
|
53
61
|
detect: (env) => env.BUILDKITE === "true",
|
|
54
62
|
providerName: "Buildkite",
|
|
55
63
|
buildNumber: (env) => nonEmpty(env.BUILDKITE_BUILD_NUMBER),
|
|
64
|
+
runId: (env) => nonEmpty(env.BUILDKITE_BUILD_ID),
|
|
56
65
|
runUrl: (env) => nonEmpty(env.BUILDKITE_BUILD_URL),
|
|
57
66
|
prNumber: (env) => {
|
|
58
67
|
const raw = env.BUILDKITE_PULL_REQUEST;
|
|
@@ -68,6 +77,7 @@ var PROVIDERS = [
|
|
|
68
77
|
detect: (env) => Boolean(env.JENKINS_URL),
|
|
69
78
|
providerName: "Jenkins",
|
|
70
79
|
buildNumber: (env) => nonEmpty(env.BUILD_NUMBER),
|
|
80
|
+
runId: (env) => nonEmpty(env.BUILD_TAG ?? env.BUILD_NUMBER),
|
|
71
81
|
runUrl: (env) => nonEmpty(env.BUILD_URL)
|
|
72
82
|
// Jenkins has no standardized PR-number env var across its many PR
|
|
73
83
|
// plugins (Multibranch, GitHub Branch Source, etc.) — deliberately
|
|
@@ -77,6 +87,7 @@ var PROVIDERS = [
|
|
|
77
87
|
detect: (env) => env.TF_BUILD === "True" || env.TF_BUILD === "true",
|
|
78
88
|
providerName: "Azure Pipelines",
|
|
79
89
|
buildNumber: (env) => nonEmpty(env.BUILD_BUILDID),
|
|
90
|
+
runId: (env) => nonEmpty(env.BUILD_BUILDID),
|
|
80
91
|
runUrl: (env) => {
|
|
81
92
|
const collectionUri = env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;
|
|
82
93
|
const project = env.SYSTEM_TEAMPROJECT;
|
|
@@ -92,6 +103,7 @@ var PROVIDERS = [
|
|
|
92
103
|
detect: (env) => Boolean(env.BITBUCKET_BUILD_NUMBER),
|
|
93
104
|
providerName: "Bitbucket Pipelines",
|
|
94
105
|
buildNumber: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),
|
|
106
|
+
runId: (env) => nonEmpty(env.BITBUCKET_BUILD_NUMBER),
|
|
95
107
|
runUrl: (env) => {
|
|
96
108
|
const origin = env.BITBUCKET_GIT_HTTP_ORIGIN;
|
|
97
109
|
if (!origin) {
|
|
@@ -113,6 +125,8 @@ function detectCi(env = process.env) {
|
|
|
113
125
|
if (runUrl !== void 0) result.ciRunUrl = runUrl;
|
|
114
126
|
const prNumber = provider.prNumber?.(env);
|
|
115
127
|
if (prNumber !== void 0) result.ciPrNumber = prNumber;
|
|
128
|
+
const runId = provider.runId?.(env);
|
|
129
|
+
if (runId !== void 0) result.ciRunId = runId;
|
|
116
130
|
return result;
|
|
117
131
|
}
|
|
118
132
|
if (ciInfo.name) {
|
|
@@ -220,6 +234,7 @@ function resolveConfig(options, deps = {}) {
|
|
|
220
234
|
const ciBuildNumber = options.ciBuildNumber ?? detectedCi.ciBuildNumber;
|
|
221
235
|
const ciRunUrl = options.ciRunUrl ?? detectedCi.ciRunUrl;
|
|
222
236
|
const ciPrNumber = options.ciPrNumber ?? detectedCi.ciPrNumber;
|
|
237
|
+
const runId = options.runId ?? firstEnv2("QUALFLARE_RUN_ID") ?? detectedCi.ciRunId ?? randomUUID();
|
|
223
238
|
return {
|
|
224
239
|
// `||` (truthy check), not `??`, for these three REQUIRED-non-empty wire
|
|
225
240
|
// fields — an explicit `''` option must not silently win over the
|
|
@@ -239,6 +254,7 @@ function resolveConfig(options, deps = {}) {
|
|
|
239
254
|
ciBuildNumber,
|
|
240
255
|
ciRunUrl,
|
|
241
256
|
ciPrNumber,
|
|
257
|
+
runId,
|
|
242
258
|
attachScreenshots: options.attachScreenshots ?? envBool("QUALFLARE_ATTACH_SCREENSHOTS") ?? true,
|
|
243
259
|
includeStepHooks: options.includeStepHooks ?? envBool("QUALFLARE_INCLUDE_STEP_HOOKS") ?? false,
|
|
244
260
|
maxAttachmentBytes: options.maxAttachmentBytes ?? envInt("QUALFLARE_MAX_ATTACHMENT_BYTES") ?? 15e5,
|
|
@@ -272,8 +288,8 @@ var logger = {
|
|
|
272
288
|
import * as fs2 from "fs";
|
|
273
289
|
import * as path2 from "path";
|
|
274
290
|
|
|
275
|
-
// src/formatter/video-
|
|
276
|
-
import { randomUUID } from "crypto";
|
|
291
|
+
// src/formatter/video-writer.ts
|
|
292
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
277
293
|
import * as fs from "fs";
|
|
278
294
|
import * as path from "path";
|
|
279
295
|
var VIDEO_MIME_TYPES_BY_EXTENSION = {
|
|
@@ -302,7 +318,7 @@ function writeVideoAttachment(pending, outputDir, maxVideoBytes) {
|
|
|
302
318
|
logger.warn(`skipping video attachment "${pending.name}": unsupported video format.`);
|
|
303
319
|
return void 0;
|
|
304
320
|
}
|
|
305
|
-
const localVideoPath = `${
|
|
321
|
+
const localVideoPath = `${randomUUID2()}${resolved.extension}`;
|
|
306
322
|
const destination = path.join(outputDir, localVideoPath);
|
|
307
323
|
if (pending.path !== void 0) {
|
|
308
324
|
let fileSize;
|
|
@@ -478,6 +494,18 @@ function messageDurationToNs(duration) {
|
|
|
478
494
|
return total > 0 ? Math.round(total) : 0;
|
|
479
495
|
}
|
|
480
496
|
|
|
497
|
+
// src/shared/text.ts
|
|
498
|
+
function truncateRunes(value, maxRunes) {
|
|
499
|
+
if (value.length <= maxRunes) {
|
|
500
|
+
return value;
|
|
501
|
+
}
|
|
502
|
+
const runes = Array.from(value);
|
|
503
|
+
if (runes.length <= maxRunes) {
|
|
504
|
+
return value;
|
|
505
|
+
}
|
|
506
|
+
return runes.slice(0, maxRunes).join("");
|
|
507
|
+
}
|
|
508
|
+
|
|
481
509
|
// src/formatter/case-builder.ts
|
|
482
510
|
function combineSteps(realSteps, manualSteps) {
|
|
483
511
|
if (realSteps.length === 0 && manualSteps.length === 0) {
|
|
@@ -496,6 +524,26 @@ function combineSteps(realSteps, manualSteps) {
|
|
|
496
524
|
});
|
|
497
525
|
return [...realSteps, ...offsetManual];
|
|
498
526
|
}
|
|
527
|
+
function buildAttempts(attempts) {
|
|
528
|
+
if (attempts.length < 2) {
|
|
529
|
+
return void 0;
|
|
530
|
+
}
|
|
531
|
+
let kept = attempts;
|
|
532
|
+
if (attempts.length > MAX_ATTEMPTS_PER_CASE) {
|
|
533
|
+
kept = [...attempts.slice(0, MAX_ATTEMPTS_PER_CASE - 1), attempts[attempts.length - 1]];
|
|
534
|
+
}
|
|
535
|
+
return kept.map((a, i) => {
|
|
536
|
+
const attempt = {
|
|
537
|
+
attempt: i + 1,
|
|
538
|
+
status: a.status,
|
|
539
|
+
duration: a.duration
|
|
540
|
+
};
|
|
541
|
+
if (a.error) {
|
|
542
|
+
attempt.message = truncateRunes(a.error, MAX_ATTEMPT_MESSAGE_RUNES);
|
|
543
|
+
}
|
|
544
|
+
return attempt;
|
|
545
|
+
});
|
|
546
|
+
}
|
|
499
547
|
function collapseAttempts(attempts) {
|
|
500
548
|
if (attempts.length === 0) {
|
|
501
549
|
throw new Error("collapseAttempts: at least one attempt is required");
|
|
@@ -504,11 +552,13 @@ function collapseAttempts(attempts) {
|
|
|
504
552
|
const retryCount = attempts.length - 1;
|
|
505
553
|
const isFlaky = retryCount > 0 && final.status === "passed" && attempts.some((a) => a.status !== "passed");
|
|
506
554
|
const duration = attempts.reduce((sum, a) => sum + a.duration, 0);
|
|
555
|
+
const attemptHistory = buildAttempts(attempts);
|
|
507
556
|
return {
|
|
508
557
|
status: final.status,
|
|
509
558
|
duration,
|
|
510
559
|
retryCount,
|
|
511
560
|
isFlaky,
|
|
561
|
+
...attemptHistory ? { attempts: attemptHistory } : {},
|
|
512
562
|
error: final.status === "passed" ? void 0 : final.error,
|
|
513
563
|
steps: combineSteps(final.steps, final.manualSteps),
|
|
514
564
|
labels: final.labels,
|
|
@@ -548,6 +598,7 @@ function buildCase(uri, pickle, collapsed, gherkin) {
|
|
|
548
598
|
duration: collapsed.duration,
|
|
549
599
|
retryCount: collapsed.retryCount || void 0,
|
|
550
600
|
isFlaky: collapsed.isFlaky || void 0,
|
|
601
|
+
attempts: collapsed.attempts,
|
|
551
602
|
error: collapsed.error,
|
|
552
603
|
tags: tags.length > 0 ? tags : void 0,
|
|
553
604
|
steps: collapsed.steps,
|
|
@@ -953,7 +1004,7 @@ function timestampMs(ts) {
|
|
|
953
1004
|
import * as os from "os";
|
|
954
1005
|
|
|
955
1006
|
// src/config/version.ts
|
|
956
|
-
var PACKAGE_VERSION = "0.
|
|
1007
|
+
var PACKAGE_VERSION = "0.4.0";
|
|
957
1008
|
|
|
958
1009
|
// src/formatter/collect-builder.ts
|
|
959
1010
|
function resolveOs(config) {
|
|
@@ -976,7 +1027,8 @@ function buildCollectPayload(suites, config) {
|
|
|
976
1027
|
metadata: {
|
|
977
1028
|
version: PACKAGE_VERSION,
|
|
978
1029
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
979
|
-
cliName: "qualflare-cucumberjs"
|
|
1030
|
+
cliName: "qualflare-cucumberjs",
|
|
1031
|
+
runId: config.runId
|
|
980
1032
|
},
|
|
981
1033
|
properties: config.properties,
|
|
982
1034
|
suites,
|
|
@@ -1309,7 +1361,7 @@ var QualflareCucumberFormatter = class extends Formatter {
|
|
|
1309
1361
|
}
|
|
1310
1362
|
}
|
|
1311
1363
|
fs3.mkdirSync(this.config.outputDir, { recursive: true });
|
|
1312
|
-
const outputPath = path4.join(this.config.outputDir, `${
|
|
1364
|
+
const outputPath = path4.join(this.config.outputDir, `${randomUUID3()}.json`);
|
|
1313
1365
|
fs3.writeFileSync(outputPath, JSON.stringify(payload));
|
|
1314
1366
|
logger.info(
|
|
1315
1367
|
`wrote Collect payload to ${outputPath} \u2014 run \`qualflare-cli collect ${this.config.outputDir}\` to upload it.`
|