Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .ci/pipelines/lib/test-run-tracker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ readonly RHDH_TEST_RUN_TRACKER_LIB_SOURCED=1
# shellcheck source=.ci/pipelines/reporting.sh
source "$(dirname "${BASH_SOURCE[0]}")/../reporting.sh"

# Sentinel value for STATUS_NUMBER_OF_TEST_FAILED when the real count
# is unknown (deploy failed, Prow timeout, JUnit file missing, or
# Playwright crashed without producing failure counts).
UNKNOWN_FAILURE_COUNT="N/A"

# Internal state
_TEST_RUN_COUNTER=0

Expand Down Expand Up @@ -36,7 +41,7 @@ test_run_tracker::mark_deploy_failed() {
test_run_tracker::register "$label"
save_status_failed_to_deploy "${_TEST_RUN_COUNTER}" true
save_status_test_failed "${_TEST_RUN_COUNTER}" true
save_status_number_of_test_failed "${_TEST_RUN_COUNTER}" "N/A"
save_status_number_of_test_failed "${_TEST_RUN_COUNTER}" "${UNKNOWN_FAILURE_COUNT}"
save_overall_result 1
}

Expand Down
16 changes: 11 additions & 5 deletions .ci/pipelines/lib/testing.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ testing::run_tests() {
test_run_tracker::register "$artifacts_subdir"
test_run_tracker::mark_deploy_success

# Pessimistic default: assume tests failed until Playwright proves otherwise.
# If the job is killed (Prow timeout) or Playwright hangs, the STATUS files
# still have entries for all registered test runs — preventing misaligned
# arrays that break downstream reporting (Slack notifications).
test_run_tracker::mark_test_result "false" "${UNKNOWN_FAILURE_COUNT}"

BASE_URL="${url}"
export BASE_URL
log::info "BASE_URL: ${BASE_URL}"
Expand Down Expand Up @@ -183,15 +189,15 @@ testing::run_tests() {
failed_tests=$((_junit_failures + _junit_errors))
if [[ "${failed_tests}" -eq 0 ]]; then
# Playwright exited non-zero but JUnit reports 0 failures and 0 errors —
# the process likely crashed or timed out globally. Report "some" so the
# Slack alert doesn't misleadingly say "0 tests failed".
failed_tests="some"
# the process likely crashed or timed out globally. Use the sentinel so
# the Slack alert doesn't misleadingly say "0 tests failed".
failed_tests="${UNKNOWN_FAILURE_COUNT}"
fi
echo "Number of failed tests: ${failed_tests}"
else
echo "JUnit results file not found: ${e2e_tests_dir}/${JUNIT_RESULTS}"
failed_tests="some"
echo "Number of failed tests unknown, saving as $failed_tests."
failed_tests="${UNKNOWN_FAILURE_COUNT}"
echo "Number of failed tests unknown, saving as ${failed_tests}."
fi
test_run_tracker::mark_test_result "$test_passed" "${failed_tests}"
return "$test_result"
Expand Down
44 changes: 32 additions & 12 deletions .ci/pipelines/reporting.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ readonly REPORTING_LIB_SOURCED=1
source "$(dirname "${BASH_SOURCE[0]}")"/lib/log.sh

# Variables for reporting
export STATUS_DEPLOYMENT_NAMESPACE # Array that holds the namespaces of deployments.
export STATUS_FAILED_TO_DEPLOY # Array that indicates if deployment failed. false = success, true = failure
export STATUS_TEST_FAILED # Array that indicates if test run failed. false = success, true = failure
export OVERALL_RESULT # Overall result of the test run. 0 = success, 1 = failure
export STATUS_DEPLOYMENT_NAMESPACE # Array that holds the namespaces of deployments.
export STATUS_FAILED_TO_DEPLOY # Array that indicates if deployment failed. false = success, true = failure
export STATUS_TEST_FAILED # Array that indicates if test run failed. false = success, true = failure
export STATUS_NUMBER_OF_TEST_FAILED # Array that holds the number of test failures per deployment.
export OVERALL_RESULT # Overall result of the test run. 0 = success, 1 = failure

mkdir -p "$ARTIFACT_DIR/reporting"

Expand All @@ -22,35 +23,54 @@ save_status_deployment_namespace() {
local current_namespace=$2
log::debug "Saving STATUS_DEPLOYMENT_NAMESPACE[\"${current_deployment}\"]=${current_namespace}"
STATUS_DEPLOYMENT_NAMESPACE["${current_deployment}"]="${current_namespace}"
printf "%s\n" "${STATUS_DEPLOYMENT_NAMESPACE["${current_deployment}"]}" >> "$SHARED_DIR/STATUS_DEPLOYMENT_NAMESPACE.txt"
cp "$SHARED_DIR/STATUS_DEPLOYMENT_NAMESPACE.txt" "$ARTIFACT_DIR/reporting/STATUS_DEPLOYMENT_NAMESPACE.txt"
_regenerate_status_file "STATUS_DEPLOYMENT_NAMESPACE"
}

save_status_failed_to_deploy() {
local current_deployment=$1
local status=$2
log::debug "Saving STATUS_FAILED_TO_DEPLOY[\"${current_deployment}\"]=${status}"
STATUS_FAILED_TO_DEPLOY["${current_deployment}"]="${status}"
printf "%s\n" "${STATUS_FAILED_TO_DEPLOY["${current_deployment}"]}" >> "$SHARED_DIR/STATUS_FAILED_TO_DEPLOY.txt"
cp "$SHARED_DIR/STATUS_FAILED_TO_DEPLOY.txt" "$ARTIFACT_DIR/reporting/STATUS_FAILED_TO_DEPLOY.txt"
_regenerate_status_file "STATUS_FAILED_TO_DEPLOY"
}

save_status_test_failed() {
local current_deployment=$1
local status=$2
log::debug "Saving STATUS_TEST_FAILED[\"${current_deployment}\"]=${status}"
STATUS_TEST_FAILED["${current_deployment}"]="${status}"
printf "%s\n" "${STATUS_TEST_FAILED["${current_deployment}"]}" >> "$SHARED_DIR/STATUS_TEST_FAILED.txt"
cp "$SHARED_DIR/STATUS_TEST_FAILED.txt" "$ARTIFACT_DIR/reporting/STATUS_TEST_FAILED.txt"
_regenerate_status_file "STATUS_TEST_FAILED"
}

save_status_number_of_test_failed() {
local current_deployment=$1
local number=$2
log::debug "Saving STATUS_NUMBER_OF_TEST_FAILED[\"${current_deployment}\"]=${number}"
STATUS_NUMBER_OF_TEST_FAILED["${current_deployment}"]="${number}"
printf "%s\n" "${STATUS_NUMBER_OF_TEST_FAILED["${current_deployment}"]}" >> "$SHARED_DIR/STATUS_NUMBER_OF_TEST_FAILED.txt"
cp "$SHARED_DIR/STATUS_NUMBER_OF_TEST_FAILED.txt" "$ARTIFACT_DIR/reporting/STATUS_NUMBER_OF_TEST_FAILED.txt"
_regenerate_status_file "STATUS_NUMBER_OF_TEST_FAILED"
}

# Regenerate a STATUS file from its in-memory associative array.
# Writes the file from scratch each time so that the same deployment ID
# can be safely updated multiple times (e.g. pessimistic default written
# before Playwright runs, then overwritten with the real result).
#
# IMPORTANT: All callers must run in the same shell process.
# Associative arrays are not inherited by child processes (export -f
# only exports function definitions, not array contents). If this
# function runs in a subshell, it will see an empty array and truncate
# the file. This is fine today — all test_run_tracker calls are
# sequential in the main shell.
_regenerate_status_file() {
Comment thread
zdrapela marked this conversation as resolved.
local var_name=$1
local -n _arr="${var_name}"
local file="$SHARED_DIR/${var_name}.txt"
: > "$file"
local key
for key in $(printf '%s\n' "${!_arr[@]}" | sort -n); do
printf '%s\n' "${_arr[$key]}" >> "$file"
done
cp "$file" "$ARTIFACT_DIR/reporting/${var_name}.txt"
}

save_overall_result() {
Expand Down
60 changes: 30 additions & 30 deletions e2e-tests/playwright/e2e/configuration-test/config-map.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ test.describe("Change app-config at e2e test runtime", () => {
await ensureRuntimeDeployed();
});

// RHDH's frontend opens an SSE (EventSource) connection for live
// updates. With tracing enabled, Playwright's fixture teardown hangs
// waiting for network idle, which never resolves while SSE stays open
// (microsoft/playwright#41513, fixed in v1.62). Navigating away drops
// the connection so teardown completes immediately.
test.afterEach(async ({ page }) => {
await page.goto("about:blank").catch(() => {});
});

test("Verify title change after ConfigMap modification", async ({ page }) => {
test.setTimeout(300000);

Expand All @@ -34,38 +43,29 @@ test.describe("Change app-config at e2e test runtime", () => {

const kubeUtils = new KubeClient();
const dynamicTitle = generateDynamicTitle();
try {
console.log("Updating app-config ConfigMap with new title.");
await kubeUtils.patchAppConfig(namespace, (appConfig: Record<string, unknown>) => {
if (!isRecord(appConfig.app)) {
throw new Error("Invalid app-config structure: expected 'app' section not found.");
}
console.log(`Current title: ${String(appConfig.app.title)}`);
appConfig.app.title = dynamicTitle;
console.log(`New title: ${dynamicTitle}`);
});

console.log(`Restarting deployment '${deploymentName}' to apply ConfigMap changes.`);
await kubeUtils.restartDeployment(deploymentName, namespace);
console.log("Updating app-config ConfigMap with new title.");
await kubeUtils.patchAppConfig(namespace, (appConfig: Record<string, unknown>) => {
if (!isRecord(appConfig.app)) {
throw new Error("Invalid app-config structure: expected 'app' section not found.");
}
console.log(`Current title: ${String(appConfig.app.title)}`);
appConfig.app.title = dynamicTitle;
console.log(`New title: ${dynamicTitle}`);
});

console.log(`Restarting deployment '${deploymentName}' to apply ConfigMap changes.`);
await kubeUtils.restartDeployment(deploymentName, namespace);

const common = new Common(page);
await page.context().clearCookies();
await page.context().clearPermissions();
await page.reload({ waitUntil: "domcontentloaded" });
await common.loginAsGuest();
await new UIhelper(page).openSidebar("Home");
console.log("Verifying new title in the UI... ");
expect(await page.title()).toContain(dynamicTitle);
console.log("Title successfully verified in the UI.");
} catch (error) {
console.log(`Test failed during ConfigMap update or deployment restart:`, error);
throw error;
} finally {
// Navigate away from RHDH to close WebSocket connections before
// Playwright tears down the page — prevents a long hang during
// context/trace cleanup.
await page.goto("about:blank").catch(() => {});
}
const common = new Common(page);
await page.context().clearCookies();
await page.context().clearPermissions();
await page.reload({ waitUntil: "domcontentloaded" });
await common.loginAsGuest();
await new UIhelper(page).openSidebar("Home");
console.log("Verifying new title in the UI... ");
expect(await page.title()).toContain(dynamicTitle);
console.log("Title successfully verified in the UI.");
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ test.describe("Verify TLS configuration with Azure Database for PostgreSQL healt
});
});

// Drop RHDH SSE connection so Playwright trace teardown doesn't hang
// (microsoft/playwright#41513, fixed in v1.62).
test.afterEach(async ({ page }) => {
await page.goto("about:blank").catch(() => {});
Comment thread
zdrapela marked this conversation as resolved.
});

test("Configure and restart deployment", async ({}, testInfo) => {
if (!config.host) {
testInfo.skip(true, `AZURE_DB_*_HOST not set for ${config.name}`);
Expand All @@ -105,14 +111,10 @@ test.describe("Verify TLS configuration with Azure Database for PostgreSQL healt
});

test("Verify successful DB connection", async ({ page }) => {
try {
const uiHelper = new UIhelper(page);
const common = new Common(page);
await common.loginAsGuest();
await uiHelper.verifyHeading("Welcome back!");
} finally {
await page.goto("about:blank").catch(() => {});
}
const uiHelper = new UIhelper(page);
const common = new Common(page);
await common.loginAsGuest();
await uiHelper.verifyHeading("Welcome back!");
});
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ test.describe("Verify TLS configuration with RDS PostgreSQL health check", () =>
});
});

// Drop RHDH SSE connection so Playwright trace teardown doesn't hang
// (microsoft/playwright#41513, fixed in v1.62).
test.afterEach(async ({ page }) => {
await page.goto("about:blank").catch(() => {});
});

test("Configure and restart deployment", async ({}, testInfo) => {
if (!config.host) {
testInfo.skip(true, `RDS_*_HOST not set for ${config.name}`);
Expand All @@ -105,14 +111,10 @@ test.describe("Verify TLS configuration with RDS PostgreSQL health check", () =>
});

test("Verify successful DB connection", async ({ page }) => {
try {
const uiHelper = new UIhelper(page);
const common = new Common(page);
await common.loginAsGuest();
await uiHelper.verifyHeading("Welcome back!");
} finally {
await page.goto("about:blank").catch(() => {});
}
const uiHelper = new UIhelper(page);
const common = new Common(page);
await common.loginAsGuest();
await uiHelper.verifyHeading("Welcome back!");
});
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { ChildProcessWithoutNullStreams, spawn } from "child_process";
import { test, expect } from "@support/coverage/test";

import { Common } from "../../utils/common";
import { resolveInstallMethod } from "../../utils/helper";
import { getReleaseName, resolveInstallMethod } from "../../utils/helper";
import { KubeClient } from "../../utils/kube-client";
import { ensureRuntimeDeployed } from "../../utils/runtime-deploy";
import { setPortForwardRestarter } from "./schema-mode-db";
Expand Down Expand Up @@ -80,7 +80,7 @@ function killPortForward(proc: ChildProcessWithoutNullStreams | undefined): Prom

test.describe("Verify pluginDivisionMode: schema", () => {
const namespace = process.env.NAME_SPACE_RUNTIME ?? "showcase-runtime";
const releaseName = process.env.RELEASE_NAME ?? "rhdh";
const releaseName = getReleaseName();
const installMethod = resolveInstallMethod();

let portForwardProcess: ChildProcessWithoutNullStreams | undefined;
Expand Down Expand Up @@ -156,6 +156,17 @@ test.describe("Verify pluginDivisionMode: schema", () => {
await killPortForward(portForwardProcess);
});

// RHDH's frontend opens an SSE (EventSource) connection for live
// updates. With tracing enabled, Playwright's fixture teardown hangs
// waiting for network idle, which never resolves while SSE stays open
// (microsoft/playwright#41513, fixed in v1.62). Navigating away drops
// the connection so teardown completes immediately.
// Requesting `page` creates a context for every test, including non-UI
// ones — acceptable overhead vs per-test conditional logic.
test.afterEach(async ({ page }) => {
await page.goto("about:blank").catch(() => {});
Comment thread
zdrapela marked this conversation as resolved.
});

test("Verify database user has restricted permissions", async () => {
const hasRestrictedPerms = await testSetup.verifyRestrictedDatabasePermissions();
expect(hasRestrictedPerms).toBe(true);
Expand All @@ -181,17 +192,10 @@ test.describe("Verify pluginDivisionMode: schema", () => {
}

const common = new Common(page);
try {
await common.loginAsGuest();
await common.loginAsGuest();

await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();

console.log("RHDH is accessible - plugins successfully created schemas in schema mode");
} finally {
// Navigate away from RHDH to close WebSocket connections before
// Playwright tears down the page — prevents a long hang during
// context/trace cleanup.
await page.goto("about:blank").catch(() => {});
}
console.log("RHDH is accessible - plugins successfully created schemas in schema mode");
});
});
14 changes: 14 additions & 0 deletions e2e-tests/playwright/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,20 @@ export function resolveInstallMethod(): "helm" | "operator" {
return job.includes("operator") ? "operator" : "helm";
}

/**
* Canonical release name resolution. Returns the RELEASE_NAME env var if set
* and non-empty, otherwise defaults to "rhdh".
*
* Note: the explicit check is used instead of `||` because oxlint's
* strict-boolean-expressions and prefer-nullish-coalescing rules
* (pedantic category) reject `||` on string operands.
*/
export function getReleaseName(): string {
return process.env.RELEASE_NAME !== undefined && process.env.RELEASE_NAME !== ""
Comment thread
zdrapela marked this conversation as resolved.
? process.env.RELEASE_NAME
: "rhdh";
}

/** Base64-encode a string. */
export function base64Encode(value: string): string {
return Buffer.from(value).toString("base64");
Expand Down
7 changes: 2 additions & 5 deletions e2e-tests/playwright/utils/kube-client/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as k8s from "@kubernetes/client-node";

import { getErrorMessage, hasErrorResponse, hasStatusCode } from "../errors";
import { resolveInstallMethod } from "../helper";
import { getReleaseName, resolveInstallMethod } from "../helper";

export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
Expand Down Expand Up @@ -135,10 +135,7 @@ export function getKubeApiErrorMessage(error: unknown): string {
* then falls back to JOB_NAME pattern matching.
*/
export function getRhdhDeploymentName(): string {
const releaseName =
process.env.RELEASE_NAME !== undefined && process.env.RELEASE_NAME !== ""
? process.env.RELEASE_NAME
: "rhdh";
const releaseName = getReleaseName();
return resolveInstallMethod() === "operator"
? `backstage-${releaseName}`
: `${releaseName}-developer-hub`;
Expand Down
Loading
Loading