@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.
- package/dist/playwright/teardown-reporter.d.ts +3 -1
- package/dist/playwright/teardown-reporter.d.ts.map +1 -1
- package/dist/playwright/teardown-reporter.js +25 -12
- package/dist/utils/kubernetes-client.d.ts +9 -0
- package/dist/utils/kubernetes-client.d.ts.map +1 -1
- package/dist/utils/kubernetes-client.js +70 -3
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
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;
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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;
|
|
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 -
|
|
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
|
*/
|