@red-hat-developer-hub/e2e-test-utils 1.1.33 → 1.1.34

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.
@@ -10,7 +10,8 @@ import type { Reporter, Suite, TestCase, TestResult } from "@playwright/test/rep
10
10
  * Falls back in onEnd() to clean up any projects that didn't complete naturally
11
11
  * (e.g., interrupted runs, maxFailures).
12
12
  *
13
- * Only active when process.env.CI === "true".
13
+ * Diagnostic log collection runs always (CI and local).
14
+ * Namespace deletion only runs when process.env.CI === "true".
14
15
  *
15
16
  * By default, deletes the namespace matching the project name.
16
17
  * For custom namespaces, consumers can register them via registerTeardownNamespace().
@@ -18,6 +19,7 @@ import type { Reporter, Suite, TestCase, TestResult } from "@playwright/test/rep
18
19
  export default class TeardownReporter implements Reporter {
19
20
  private _projectTestCounts;
20
21
  private _projectCompleted;
22
+ private _projectsWithFailures;
21
23
  private _pendingDeletions;
22
24
  onBegin(_config: unknown, suite: Suite): void;
23
25
  onTestEnd(test: TestCase, result: TestResult): void;
@@ -1 +1 @@
1
- {"version":3,"file":"teardown-reporter.d.ts","sourceRoot":"","sources":["../../src/playwright/teardown-reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAInC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,OAAO,OAAO,gBAAiB,YAAW,QAAQ;IACvD,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,iBAAiB,CAAoC;IAE7D,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI;IAa7C,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IA0B7C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAed,wBAAwB;CAuBvC"}
1
+ {"version":3,"file":"teardown-reporter.d.ts","sourceRoot":"","sources":["../../src/playwright/teardown-reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAKnC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,OAAO,OAAO,gBAAiB,YAAW,QAAQ;IACvD,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,qBAAqB,CAAqB;IAClD,OAAO,CAAC,iBAAiB,CAAoC;IAE7D,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI;IAa7C,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI;IA6B7C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAcd,wBAAwB;CAwCvC"}
@@ -1,3 +1,4 @@
1
+ import path from "path";
1
2
  import { KubernetesClientHelper } from "../utils/kubernetes-client.js";
2
3
  import { getTeardownNamespaces } from "./teardown-namespaces.js";
3
4
  /**
@@ -11,7 +12,8 @@ import { getTeardownNamespaces } from "./teardown-namespaces.js";
11
12
  * Falls back in onEnd() to clean up any projects that didn't complete naturally
12
13
  * (e.g., interrupted runs, maxFailures).
13
14
  *
14
- * Only active when process.env.CI === "true".
15
+ * Diagnostic log collection runs always (CI and local).
16
+ * Namespace deletion only runs when process.env.CI === "true".
15
17
  *
16
18
  * By default, deletes the namespace matching the project name.
17
19
  * For custom namespaces, consumers can register them via registerTeardownNamespace().
@@ -19,6 +21,7 @@ import { getTeardownNamespaces } from "./teardown-namespaces.js";
19
21
  export default class TeardownReporter {
20
22
  _projectTestCounts = new Map();
21
23
  _projectCompleted = new Map();
24
+ _projectsWithFailures = new Set();
22
25
  _pendingDeletions = new Map();
23
26
  onBegin(_config, suite) {
24
27
  for (const test of suite.allTests()) {
@@ -30,8 +33,6 @@ export default class TeardownReporter {
30
33
  }
31
34
  }
32
35
  onTestEnd(test, result) {
33
- if (process.env.CI !== "true")
34
- return;
35
36
  const project = test.parent.project();
36
37
  if (!project)
37
38
  return;
@@ -41,23 +42,25 @@ export default class TeardownReporter {
41
42
  if (!isDone)
42
43
  return;
43
44
  const name = project.name;
45
+ if (result.status !== "passed" && result.status !== "skipped") {
46
+ this._projectsWithFailures.add(name);
47
+ }
44
48
  const completed = (this._projectCompleted.get(name) ?? 0) + 1;
45
49
  this._projectCompleted.set(name, completed);
46
- // Start deletion immediately (fire-and-forget here, awaited in onEnd)
50
+ // Start cleanup immediately (fire-and-forget here, awaited in onEnd)
47
51
  if (completed === this._projectTestCounts.get(name) &&
48
52
  !this._pendingDeletions.has(name)) {
49
53
  this._pendingDeletions.set(name, this._deleteProjectNamespaces(name));
50
54
  }
51
55
  }
52
56
  async onEnd() {
53
- if (process.env.CI !== "true")
54
- return;
55
- // Await all in-flight deletions started from onTestEnd
57
+ // Await all in-flight cleanups started from onTestEnd
56
58
  await Promise.all(this._pendingDeletions.values());
57
59
  // Fallback: clean up projects that didn't complete naturally
58
- // (e.g., interrupted run, maxFailures hit)
60
+ // (e.g., interrupted run, maxFailures hit) — always collect diagnostics
59
61
  for (const [project] of this._projectTestCounts) {
60
62
  if (!this._pendingDeletions.has(project)) {
63
+ this._projectsWithFailures.add(project);
61
64
  await this._deleteProjectNamespaces(project);
62
65
  }
63
66
  }
@@ -68,14 +71,24 @@ export default class TeardownReporter {
68
71
  k8sClient = new KubernetesClientHelper();
69
72
  }
70
73
  catch (error) {
71
- console.error(`[TeardownReporter] Cannot connect to cluster, skipping teardown:`, error);
74
+ console.error(`[TeardownReporter] Cannot connect to cluster, skipping cleanup:`, error);
72
75
  return;
73
76
  }
74
77
  const customNamespaces = getTeardownNamespaces(projectName);
75
78
  const namespaces = customNamespaces.length > 0 ? customNamespaces : [projectName];
76
- for (const ns of namespaces) {
77
- console.log(`[TeardownReporter] Deleting namespace "${ns}" (project: ${projectName})`);
78
- await k8sClient.deleteNamespace(ns);
79
+ // Collect diagnostic logs on failure (always, regardless of CI)
80
+ if (this._projectsWithFailures.has(projectName)) {
81
+ for (const ns of namespaces) {
82
+ const outputDir = path.join("node_modules", ".cache", "e2e-test-results", "logs", projectName);
83
+ await k8sClient.collectDiagnosticLogs(ns, outputDir);
84
+ }
85
+ }
86
+ // Delete namespaces only in CI
87
+ if (process.env.CI === "true") {
88
+ for (const ns of namespaces) {
89
+ console.log(`[TeardownReporter] Deleting namespace "${ns}" (project: ${projectName})`);
90
+ await k8sClient.deleteNamespace(ns);
91
+ }
79
92
  }
80
93
  }
81
94
  }
@@ -88,6 +88,15 @@ declare class KubernetesClientHelper {
88
88
  * @param pollIntervalMs - How often to check pod status (default: 5000)
89
89
  */
90
90
  waitForPodsWithFailureDetection(namespace: string, labelSelector: string, timeoutSeconds?: number, pollIntervalMs?: number): Promise<void>;
91
+ /**
92
+ * Collects diagnostic logs for all resources in a namespace and saves them as files.
93
+ * Uses kubectl for cross-platform compatibility (works on OpenShift, EKS, GKE, etc.).
94
+ * OpenShift-specific resources (routes) are collected on a best-effort basis.
95
+ *
96
+ * @param namespace - Namespace to collect diagnostics from
97
+ * @param outputDir - Directory to write log files to (defaults to playwright-report/logs/<namespace>)
98
+ */
99
+ collectDiagnosticLogs(namespace: string, outputDir?: string): Promise<void>;
91
100
  /**
92
101
  * Check if a pod is in a failure state. Returns failure info or null if healthy.
93
102
  */
@@ -1 +1 @@
1
- {"version":3,"file":"kubernetes-client.d.ts","sourceRoot":"","sources":["../../src/utils/kubernetes-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,GAAG,MAAM,yBAAyB,CAAC;AAO/C;;GAEG;AACH,cAAM,sBAAsB;IAC1B,OAAO,CAAC,GAAG,CAAiB;IAC5B,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,iBAAiB,CAAuB;;IAqChD;;OAEG;IACG,uBAAuB,CAC3B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IA8C3B;;OAEG;IACG,0BAA0B,CAC9B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IA+B3B;;UAEM;IAmBN;;OAEG;IAsBH;;OAEG;YACW,YAAY;IA8B1B;;OAEG;IACG,wBAAwB,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC;IA0ChB;;OAEG;IACG,qBAAqB,CACzB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,EAC7C,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC;IAqChB;;OAEG;IACG,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,eAAe,GAAE,OAAc,EAC/B,cAAc,GAAE,MAAY,GAC3B,OAAO,CAAC,IAAI,CAAC;IAyBhB;;OAEG;YACW,yBAAyB;IAsCvC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA2BxB;;OAEG;IACG,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAc3E;;OAEG;IACG,uBAAuB,CAC3B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,cAAc,GAAE,MAAY,EAC5B,cAAc,GAAE,MAAa,GAC5B,OAAO,CAAC,OAAO,CAAC;IAiBnB;;;OAGG;IACG,uBAAuB,IAAI,OAAO,CAAC,MAAM,CAAC;IAuBhD;;;;;;OAMG;IACG,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBxE;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAmBxB;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAQnC;IAEH;;;;;;;;OAQG;IACG,+BAA+B,CACnC,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,cAAc,GAAE,MAAY,EAC5B,cAAc,GAAE,MAAa,GAC5B,OAAO,CAAC,IAAI,CAAC;IA2FhB;;OAEG;IACH,OAAO,CAAC,gBAAgB;CA8BzB;AAED,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
1
+ {"version":3,"file":"kubernetes-client.d.ts","sourceRoot":"","sources":["../../src/utils/kubernetes-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,GAAG,MAAM,yBAAyB,CAAC;AAO/C;;GAEG;AACH,cAAM,sBAAsB;IAC1B,OAAO,CAAC,GAAG,CAAiB;IAC5B,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,iBAAiB,CAAuB;;IAqChD;;OAEG;IACG,uBAAuB,CAC3B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IA8C3B;;OAEG;IACG,0BAA0B,CAC9B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IA+B3B;;UAEM;IAmBN;;OAEG;IAsBH;;OAEG;YACW,YAAY;IA8B1B;;OAEG;IACG,wBAAwB,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC;IA0ChB;;OAEG;IACG,qBAAqB,CACzB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,EAC7C,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC;IAqChB;;OAEG;IACG,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,eAAe,GAAE,OAAc,EAC/B,cAAc,GAAE,MAAY,GAC3B,OAAO,CAAC,IAAI,CAAC;IAyBhB;;OAEG;YACW,yBAAyB;IAsCvC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA2BxB;;OAEG;IACG,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAc3E;;OAEG;IACG,uBAAuB,CAC3B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,cAAc,GAAE,MAAY,EAC5B,cAAc,GAAE,MAAa,GAC5B,OAAO,CAAC,OAAO,CAAC;IAiBnB;;;OAGG;IACG,uBAAuB,IAAI,OAAO,CAAC,MAAM,CAAC;IAuBhD;;;;;;OAMG;IACG,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBxE;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAmBxB;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAQnC;IAEH;;;;;;;;OAQG;IACG,+BAA+B,CACnC,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,cAAc,GAAE,MAAY,EAC5B,cAAc,GAAE,MAAa,GAC5B,OAAO,CAAC,IAAI,CAAC;IAwFhB;;;;;;;OAOG;IACG,qBAAqB,CACzB,SAAS,EAAE,MAAM,EACjB,SAAS,GAAE,MAMV,GACA,OAAO,CAAC,IAAI,CAAC;IAiGhB;;OAEG;IACH,OAAO,CAAC,gBAAgB;CA8BzB;AAED,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
@@ -509,13 +509,11 @@ class KubernetesClientHelper {
509
509
  }
510
510
  await new Promise((r) => setTimeout(r, pollIntervalMs));
511
511
  }
512
- // Timeout reached - collect diagnostic info before throwing
512
+ // Timeout reached - print diagnostics to stdio before throwing
513
513
  console.log(`\n[K8sHelper] ═══ Pod Diagnostics (timeout reached) ═══`);
514
514
  try {
515
515
  console.log(`\n[K8sHelper] ─── Pod Status ───`);
516
516
  await $ `oc get pods -n ${namespace} -l ${labelSelector} -o wide`;
517
- console.log(`\n[K8sHelper] ─── Namespace Events ───`);
518
- await $ `oc get events -n ${namespace} --sort-by='.lastTimestamp'`;
519
517
  console.log(`\n[K8sHelper] ─── Pod Logs ───`);
520
518
  await $ `oc logs -n ${namespace} -l ${labelSelector} --all-containers --tail=100 2>&1 || true`;
521
519
  }
@@ -525,6 +523,75 @@ class KubernetesClientHelper {
525
523
  console.log(`\n[K8sHelper] ═══ End Pod Diagnostics ═══\n`);
526
524
  throw new Error(`Timeout waiting for pods (${labelSelector}) after ${timeoutSeconds}s`);
527
525
  }
526
+ /**
527
+ * Collects diagnostic logs for all resources in a namespace and saves them as files.
528
+ * Uses kubectl for cross-platform compatibility (works on OpenShift, EKS, GKE, etc.).
529
+ * OpenShift-specific resources (routes) are collected on a best-effort basis.
530
+ *
531
+ * @param namespace - Namespace to collect diagnostics from
532
+ * @param outputDir - Directory to write log files to (defaults to playwright-report/logs/<namespace>)
533
+ */
534
+ async collectDiagnosticLogs(namespace, outputDir = path.join("node_modules", ".cache", "e2e-test-results", "logs", namespace)) {
535
+ fs.mkdirSync(outputDir, { recursive: true });
536
+ console.log(`[K8sHelper] Collecting diagnostic logs for "${namespace}" → ${outputDir}`);
537
+ const quiet = $({
538
+ stdio: ["pipe", "pipe", "pipe"],
539
+ timeout: "20s",
540
+ });
541
+ const save = async (filePath, cmd) => {
542
+ try {
543
+ const result = await cmd;
544
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
545
+ fs.writeFileSync(filePath, result.stdout);
546
+ }
547
+ catch {
548
+ // ignore — resource type may not exist on this cluster
549
+ }
550
+ };
551
+ await Promise.allSettled([
552
+ save(path.join(outputDir, "events.txt"), quiet `kubectl get events -n ${namespace} --sort-by='.lastTimestamp'`),
553
+ save(path.join(outputDir, "pods.txt"), quiet `kubectl get pods -n ${namespace} -o wide`),
554
+ save(path.join(outputDir, "describe-pods.txt"), quiet `kubectl describe pods -n ${namespace}`),
555
+ save(path.join(outputDir, "deployments.txt"), quiet `kubectl get deployments -n ${namespace} -o wide`),
556
+ save(path.join(outputDir, "describe-deployments.txt"), quiet `kubectl describe deployments -n ${namespace}`),
557
+ save(path.join(outputDir, "statefulsets.txt"), quiet `kubectl get statefulsets -n ${namespace} -o wide`),
558
+ save(path.join(outputDir, "routes.txt"), quiet `kubectl get routes -n ${namespace} -o wide`),
559
+ ]);
560
+ try {
561
+ const pods = (await this._k8sApi.listNamespacedPod({ namespace })).items;
562
+ const saveLogs = async (filePath, cmd) => {
563
+ try {
564
+ const result = await cmd;
565
+ if (result.stdout.trim()) {
566
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
567
+ fs.writeFileSync(filePath, result.stdout);
568
+ }
569
+ }
570
+ catch {
571
+ // ignore — container may not have started or no previous logs
572
+ }
573
+ };
574
+ await Promise.allSettled(pods
575
+ .filter((pod) => pod.metadata?.name)
576
+ .flatMap((pod) => {
577
+ const podName = pod.metadata.name;
578
+ const podDir = path.join(outputDir, "pods", podName);
579
+ const containers = [
580
+ ...(pod.spec?.initContainers ?? []),
581
+ ...(pod.spec?.containers ?? []),
582
+ ];
583
+ return containers
584
+ .filter((c) => c.name)
585
+ .flatMap((c) => [
586
+ saveLogs(path.join(podDir, `${c.name}.log`), quiet `kubectl logs ${podName} -n ${namespace} -c ${c.name}`),
587
+ saveLogs(path.join(podDir, `${c.name}.previous.log`), quiet `kubectl logs ${podName} -n ${namespace} -c ${c.name} --previous`),
588
+ ]);
589
+ }));
590
+ }
591
+ catch {
592
+ // ignore
593
+ }
594
+ }
528
595
  /**
529
596
  * Check if a pod is in a failure state. Returns failure info or null if healthy.
530
597
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@red-hat-developer-hub/e2e-test-utils",
3
- "version": "1.1.33",
3
+ "version": "1.1.34",
4
4
  "description": "Test utilities for RHDH E2E tests",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {