@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.
@@ -1,14 +1,19 @@
1
1
  // src/formatter/formatter.ts
2
- import { randomUUID as randomUUID2 } from "crypto";
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-uploader.ts
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 = `${randomUUID()}${resolved.extension}`;
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.2.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, `${randomUUID2()}.json`);
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.`