Skip to content

Commit 4e2ad04

Browse files
committed
additional orchestrator tests (redhat-developer#3996)
1 parent 358ec0a commit 4e2ad04

3 files changed

Lines changed: 307 additions & 10 deletions

File tree

e2e-tests/playwright/e2e/audit-log/log-utils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,42 @@ export class LogUtils {
3131
});
3232
}
3333

34+
/**
35+
* Executes a command with retry logic.
36+
*
37+
* @param command The command to execute
38+
* @param args An array of arguments for the command
39+
* @param maxRetries Maximum number of retry attempts (default: 3)
40+
* @returns A promise that resolves with the command output
41+
*/
42+
static async executeCommandWithRetries(
43+
command: string,
44+
args: string[] = [],
45+
maxRetries: number = 3,
46+
): Promise<string> {
47+
let attempt = 0;
48+
while (attempt <= maxRetries) {
49+
try {
50+
console.log(
51+
`Attempt ${attempt + 1}/${maxRetries}: Executing command: ${command} ${args.join(" ")}`,
52+
);
53+
const output = await LogUtils.executeCommand(command, args);
54+
console.log(`Command executed successfully on attempt ${attempt + 1}`);
55+
return output;
56+
} catch (error) {
57+
console.error(
58+
`Error executing command on attempt ${attempt + 1}:`,
59+
error,
60+
);
61+
attempt++;
62+
}
63+
}
64+
65+
throw new Error(
66+
`Failed to execute command "${command} ${args.join(" ")}" after ${maxRetries} attempts.`,
67+
);
68+
}
69+
3470
/**
3571
* Executes a shell command and returns the output as a promise.
3672
*

e2e-tests/playwright/e2e/plugins/orchestrator/failswitch-workflow.spec.ts

Lines changed: 169 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { test } from "@playwright/test";
1+
import { test, expect } from "@playwright/test";
22
import { UIhelper } from "../../../utils/ui-helper";
33
import { Common } from "../../../utils/common";
44
import { Orchestrator } from "../../../support/pages/orchestrator";
55
import { skipIfJobName } from "../../../utils/helper";
66
import { JOB_NAME_PATTERNS } from "../../../utils/constants";
7+
import { LogUtils } from "../../audit-log/log-utils";
78

89
test.describe("Orchestrator failswitch workflow tests", () => {
910
test.skip(() => skipIfJobName(JOB_NAME_PATTERNS.OSD_GCP)); // skipping orchestrator tests on OSD-GCP due to infra not being installed
@@ -24,18 +25,183 @@ test.describe("Orchestrator failswitch workflow tests", () => {
2425
await uiHelper.openSidebar("Orchestrator");
2526
await orchestrator.selectFailSwitchWorkflowItem();
2627
await orchestrator.runFailSwitchWorkflow("OK");
28+
await orchestrator.validateCurrentWorkflowStatus("Completed");
2729
await orchestrator.reRunFailSwitchWorkflow("Wait");
2830
await orchestrator.abortWorkflow();
2931
await orchestrator.reRunFailSwitchWorkflow("KO");
30-
await orchestrator.validateWorkflowStatus("Failed");
32+
await orchestrator.validateCurrentWorkflowStatus("Failed");
33+
await uiHelper.openSidebar("Orchestrator");
34+
await orchestrator.selectFailSwitchWorkflowItem();
35+
await orchestrator.runFailSwitchWorkflow("Wait");
36+
await orchestrator.validateCurrentWorkflowStatus("Running");
3137
await uiHelper.openSidebar("Orchestrator");
3238
await orchestrator.validateWorkflowAllRuns();
39+
await orchestrator.validateWorkflowAllRunsStatusIcons();
3340
});
3441

35-
test("Failswitch workflow execution and test abort workflow", async () => {
42+
test("Test abort workflow", async () => {
3643
await uiHelper.openSidebar("Orchestrator");
3744
await orchestrator.selectFailSwitchWorkflowItem();
3845
await orchestrator.runFailSwitchWorkflow("Wait");
3946
await orchestrator.abortWorkflow();
4047
});
48+
49+
test("Test Running status validations", async () => {
50+
await uiHelper.openSidebar("Orchestrator");
51+
await orchestrator.selectFailSwitchWorkflowItem();
52+
await orchestrator.runFailSwitchWorkflow("Wait");
53+
await orchestrator.validateWorkflowStatusDetails("Running");
54+
});
55+
56+
test("Test Failed status validations", async () => {
57+
await uiHelper.openSidebar("Orchestrator");
58+
await orchestrator.selectFailSwitchWorkflowItem();
59+
await orchestrator.runFailSwitchWorkflow("KO");
60+
await orchestrator.validateWorkflowStatusDetails("Failed");
61+
});
62+
63+
test("Test Completed status validations", async () => {
64+
await uiHelper.openSidebar("Orchestrator");
65+
await orchestrator.selectFailSwitchWorkflowItem();
66+
await orchestrator.runFailSwitchWorkflow("OK");
67+
await orchestrator.validateWorkflowStatusDetails("Completed");
68+
});
69+
70+
test("Test rerunning from failure point using failswitch workflow", async ({}, testInfo) => {
71+
test.setTimeout(240000); // 4 minutes: pod restarts + 60s sleep + failure/recovery time
72+
const ns = testInfo.project.name;
73+
74+
test.skip(!ns, "NAME_SPACE not set");
75+
76+
const originalHttpbin = "https://httpbin.org/";
77+
try {
78+
await patchHttpbin(ns!, "https://foobar.org/");
79+
await restartAndWait(ns!);
80+
81+
await uiHelper.openSidebar("Orchestrator");
82+
await orchestrator.selectFailSwitchWorkflowItem();
83+
await orchestrator.runFailSwitchWorkflow("Wait");
84+
await orchestrator.validateCurrentWorkflowStatus("Failed"); // 2 minutes: 60s sleep + time to fail
85+
86+
await patchHttpbin(ns!, originalHttpbin);
87+
await restartAndWait(ns!);
88+
89+
await orchestrator.reRunOnFailure("From failure point");
90+
await orchestrator.validateCurrentWorkflowStatus("Completed");
91+
} catch (e) {
92+
test.info().annotations.push({
93+
type: "test-error",
94+
description: String(e),
95+
});
96+
throw e;
97+
} finally {
98+
try {
99+
await cleanupAfterTest(ns!, originalHttpbin);
100+
} catch (cleanupErr) {
101+
test.info().annotations.push({
102+
type: "cleanup-error",
103+
description: String(cleanupErr),
104+
});
105+
}
106+
}
107+
});
108+
109+
test("Failswitch links to another workflow and link works", async ({
110+
page,
111+
}) => {
112+
await uiHelper.openSidebar("Orchestrator");
113+
await orchestrator.selectFailSwitchWorkflowItem();
114+
await orchestrator.runFailSwitchWorkflow("OK");
115+
116+
// Verify suggested next workflow section and navigate via the greeting link
117+
await expect(
118+
page.getByRole("heading", { name: /suggested next workflow/i }),
119+
).toBeVisible();
120+
const greetingLink = page.getByRole("link", { name: /greeting/i });
121+
await expect(greetingLink).toBeVisible();
122+
await greetingLink.click();
123+
124+
// Popup should appear for Greeting workflow
125+
await expect(
126+
page.getByRole("dialog", { name: /greeting workflow/i }),
127+
).toBeVisible();
128+
await expect(
129+
page.getByRole("button", { name: /run workflow/i }),
130+
).toBeVisible();
131+
await page.getByRole("button", { name: /run workflow/i }).click();
132+
133+
// Verify Greeting workflow execute view shows correct header and "Next" button
134+
await expect(
135+
page.getByRole("heading", { name: "Greeting workflow" }),
136+
).toBeVisible();
137+
await expect(page.getByRole("button", { name: "Next" })).toBeVisible();
138+
});
41139
});
140+
141+
async function getHttpbinValue(ns: string): Promise<string | undefined> {
142+
const args = [
143+
"-n",
144+
ns,
145+
"get",
146+
"sonataflow",
147+
"failswitch",
148+
"-o",
149+
`jsonpath={.spec.podTemplate.container.env[?(@.name=='HTTPBIN')].value}`,
150+
];
151+
const out = await LogUtils.executeCommand("oc", args);
152+
return out || undefined;
153+
}
154+
155+
async function patchHttpbin(ns: string, value: string): Promise<void> {
156+
const patch = `{"spec":{"podTemplate":{"container":{"env":[{"name":"HTTPBIN","value":"${value}"}]}}}}`;
157+
console.log("patching HTTPBIN in sontaflow resource to ", value);
158+
const args = [
159+
"-n",
160+
ns,
161+
"patch",
162+
"sonataflow",
163+
"failswitch",
164+
"--type",
165+
"merge",
166+
"-p",
167+
patch,
168+
];
169+
await LogUtils.executeCommand("oc", args);
170+
}
171+
172+
async function restartAndWait(ns: string): Promise<void> {
173+
console.log("restarting deployment failswitch");
174+
const restartArgs = [
175+
"-n",
176+
ns,
177+
"rollout",
178+
"restart",
179+
"deployment",
180+
"failswitch",
181+
];
182+
await LogUtils.executeCommand("oc", restartArgs);
183+
184+
console.log("waiting for pods to be ready");
185+
const waitArgs = [
186+
"-n",
187+
ns,
188+
"wait",
189+
"--for=condition=ready",
190+
"pod",
191+
"-l",
192+
"app.kubernetes.io/name=failswitch",
193+
"--timeout=5s",
194+
];
195+
await LogUtils.executeCommandWithRetries("oc", waitArgs, 5);
196+
}
197+
198+
async function cleanupAfterTest(
199+
ns: string,
200+
originalHttpbin: string,
201+
): Promise<void> {
202+
const currentHttpbin = await getHttpbinValue(ns!);
203+
if (currentHttpbin !== originalHttpbin) {
204+
await patchHttpbin(ns!, originalHttpbin);
205+
await restartAndWait(ns!);
206+
}
207+
}

e2e-tests/playwright/support/pages/orchestrator.ts

Lines changed: 102 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,21 @@ export class Orchestrator {
135135
"aria-label",
136136
"Status",
137137
);
138-
await this.page.getByTestId("select").first().click();
138+
await this.page
139+
.getByLabel("Status")
140+
.getByRole("button", { name: "All" })
141+
.click();
139142

140143
const statuses = ["All", "Running", "Failed", "Completed", "Aborted"];
141144
for (const status of statuses) {
142145
await expect(this.page.getByRole("option", { name: status })).toHaveText(
143146
status,
144147
);
148+
await this.page.getByRole("option", { name: status }).click();
149+
await this.page
150+
.getByLabel("Status")
151+
.getByRole("button", { name: status })
152+
.click();
145153
}
146154
await this.page.getByRole("option", { name: "All" }).click();
147155

@@ -162,6 +170,32 @@ export class Orchestrator {
162170
}
163171
}
164172

173+
async validateWorkflowAllRunsStatusIcons() {
174+
await this.page.getByRole("tab", { name: "all runs" }).click();
175+
const statuses = ["Running", "Failed", "Completed", "-- Aborted"];
176+
for (const status of statuses) {
177+
await expect(this.page.getByText(status).first()).toHaveText(status);
178+
}
179+
await expect(
180+
this.page
181+
.getByRole("cell", { name: /Running/ })
182+
.locator("svg")
183+
.first(),
184+
).toBeVisible();
185+
await expect(
186+
this.page
187+
.getByRole("cell", { name: /Completed/ })
188+
.locator("svg")
189+
.first(),
190+
).toBeVisible();
191+
await expect(
192+
this.page
193+
.getByRole("cell", { name: /Failed/ })
194+
.locator("svg")
195+
.first(),
196+
).toBeVisible();
197+
}
198+
165199
async getPageUrl() {
166200
return this.page.url();
167201
}
@@ -192,6 +226,7 @@ export class Orchestrator {
192226
.getByRole("button", { name: "Abort" })
193227
.click();
194228
await expect(this.page.getByText("Run has aborted")).toBeVisible();
229+
await expect(this.page.getByText("-- Aborted")).toBeVisible();
195230
}
196231

197232
async validateErrorPopup() {
@@ -236,21 +271,81 @@ export class Orchestrator {
236271

237272
switch (input) {
238273
case "OK":
239-
await this.validateWorkflowStatus("Completed");
274+
await this.validateCurrentWorkflowStatus("Completed");
240275
break;
241276
case "KO":
242-
await this.validateWorkflowStatus("Failed");
277+
await this.validateCurrentWorkflowStatus("Failed");
243278
break;
244279
case "Wait":
245-
await this.validateWorkflowStatus("Running");
280+
await this.validateCurrentWorkflowStatus("Running");
246281
break;
247282
}
248283
}
249284

250-
async validateWorkflowStatus(status = "Completed") {
285+
async validateWorkflowStatusDetails(status = "Completed") {
286+
const details = this.page
287+
.getByRole("article")
288+
.filter({ has: this.page.getByRole("heading", { name: "Workflow" }) });
289+
290+
if (status === "Running") {
291+
// Verify Run status heading and spinner in details area
292+
await expect(
293+
details.getByRole("heading", { name: /Run\s*status/i }),
294+
).toBeVisible();
295+
await expect(
296+
this.page
297+
.locator("b")
298+
.filter({ hasText: "Running" })
299+
.getByRole("progressbar"),
300+
).toBeVisible();
301+
// Verify a button shows 'Running' text and has a spinner
302+
const workflowButtons = this.page
303+
.locator("div")
304+
.filter({ hasText: "Abort Running..." })
305+
.nth(4);
306+
await expect(workflowButtons).toHaveText(/Running/i);
307+
await expect(workflowButtons.getByRole("progressbar")).toBeVisible();
308+
// Results section verifications
309+
await expect(
310+
this.page.getByTestId("info-card-subheader").getByRole("img"),
311+
).toBeVisible();
312+
// Verify workflow is running message is visible with timestamp
313+
// Note: Following line is blocked in main branch due to bug RHDHBUGS-2220. TODO: Uncomment this once the bug is fixed.
314+
// await expect(this.page.getByText(/workflow is running\.?\s*Started at\s+\d{1,2}\/\d{1,2}\/\d{4},\s+\d{1,2}:\d{2}:\d{2}\s+(AM|PM)/i)).toBeVisible();
315+
}
316+
if (status === "Failed") {
317+
await expect(
318+
details.getByTestId("ErrorOutlineOutlinedIcon"),
319+
).toBeVisible();
320+
await expect(
321+
this.page.getByText(
322+
/Run has failed at\s+\d{1,2}\/\d{1,2}\/\d{4},\s+\d{1,2}:\d{2}:\d{2}\s+(AM|PM)/,
323+
),
324+
).toBeVisible();
325+
await expect(
326+
this.page.getByTestId("ErrorOutlineOutlinedIcon"),
327+
).toBeVisible();
328+
}
329+
if (status === "Completed") {
330+
await expect(
331+
this.page
332+
.locator("b")
333+
.filter({ hasText: "Completed" })
334+
.getByTestId("CheckCircleOutlinedIcon"),
335+
).toBeVisible();
336+
await expect(
337+
this.page.getByText(
338+
/Run completed at\s+\d{1,2}\/\d{1,2}\/\d{4},\s+\d{1,2}:\d{2}:\d{2}\s+(AM|PM)/,
339+
),
340+
).toBeVisible();
341+
await expect(this.page.getByTestId("SuccessOutlinedIcon")).toBeVisible();
342+
}
343+
}
344+
345+
async validateCurrentWorkflowStatus(status = "Completed", timeout = 120000) {
251346
await expect(this.page.getByText(`${status}`, { exact: true })).toBeVisible(
252347
{
253-
timeout: 600000,
348+
timeout,
254349
},
255350
);
256351
}
@@ -267,6 +362,6 @@ export class Orchestrator {
267362
async reRunOnFailure(input = "Entire workflow") {
268363
await expect(this.page.getByText("Run again")).toBeVisible();
269364
await this.page.getByText("Run again").click();
270-
await this.page.getByRole("option", { name: input }).click();
365+
await this.page.getByRole("menuitem", { name: input }).click();
271366
}
272367
}

0 commit comments

Comments
 (0)