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
36 changes: 36 additions & 0 deletions src/vip_tests/workbench/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,42 @@ def pytest_collection_modifyitems(
# instead of timing out on an opaque "Locator expected to be visible" error.
TERMINAL_SESSION_FAILURE_STATES = ("Failed",)

# ---------------------------------------------------------------------------
# Resource profile helpers
# Shared between test_session_capacity.py and test_session_capacity_k8s.py,
# both of which need to detect and skip resource profiles that Workbench
# renders as visible-but-disabled for the authenticated user.
# ---------------------------------------------------------------------------


class ResourceProfileDisabled(Exception):
"""Raised when the target resource profile is present but disabled for the user.

Workbench renders resource profiles the authenticated user is not entitled
to (e.g. a group-restricted profile) as visible options with
``aria-disabled='true'`` / ``data-disabled``. Clicking one just blocks
until Playwright's timeout, so ``_launch_session`` raises this instead and
lets the caller record the profile as unavailable and move on.
"""

def __init__(self, profile: str) -> None:
super().__init__(profile)
self.profile = profile


def _option_is_disabled(option: Locator) -> bool:
"""Return True if a ``[role='option']`` is disabled for the current user.

Radix-based selects mark unavailable options with ``aria-disabled='true'``
and an (empty-valued) ``data-disabled`` attribute; ``get_attribute``
returns ``""`` for the latter, so test for presence rather than truthiness.
"""
return (
option.get_attribute("aria-disabled") == "true"
or option.get_attribute("data-disabled") is not None
)


# ---------------------------------------------------------------------------


Expand Down
83 changes: 75 additions & 8 deletions src/vip_tests/workbench/test_session_capacity.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@

from __future__ import annotations

from dataclasses import dataclass

import pytest
from playwright.sync_api import Page, expect
from pytest_bdd import scenarios, then, when

from vip_tests.workbench.conftest import (
TIMEOUT_DIALOG,
TIMEOUT_QUICK,
ResourceProfileDisabled,
_option_is_disabled,
_quit_vip_sessions_via_cookies,
format_capacity_failure,
wait_for_session_active,
Expand All @@ -38,13 +42,30 @@
_SESSION_PREFIX = f"_vip_cap_{int(__import__('time').time())}_"


@dataclass(frozen=True)
class DetectedProfile:
"""A resource profile discovered from the New Session dialog's dropdown.

Disabled profiles are still reported so the caller can distinguish "no
profiles at all" from "all profiles disabled for this user" and skip with
an accurate reason.
"""

name: str
disabled: bool


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _detect_profiles(page: Page) -> list[str]:
"""Open the New Session dialog and read available resource profiles."""
def _detect_profiles(page: Page) -> list[DetectedProfile]:
"""Open the New Session dialog and read available resource profiles.

Returns one :class:`DetectedProfile` per profile discovered, naming it and
recording whether it is disabled for the authenticated user.
"""
page.locator(Homepage.NEW_SESSION_BUTTON).first.click(timeout=TIMEOUT_DIALOG)

dialog = page.locator(NewSessionDialog.DIALOG)
Expand All @@ -64,11 +85,12 @@ def _detect_profiles(page: Page) -> list[str]:
options = page.locator("[role='option']")
options.first.wait_for(state="visible", timeout=TIMEOUT_QUICK)
count = options.count()
profiles = []
profiles: list[DetectedProfile] = []
for i in range(count):
text = (options.nth(i).text_content() or "").strip()
option = options.nth(i)
text = (option.text_content() or "").strip()
if text:
profiles.append(text)
profiles.append(DetectedProfile(name=text, disabled=_option_is_disabled(option)))

# Close the dropdown, then close the dialog via Escape.
page.keyboard.press("Escape")
Expand All @@ -83,7 +105,11 @@ def _launch_session(
session_name: str,
profile: str | None = None,
) -> None:
"""Open the New Session dialog, optionally select a resource profile, and launch."""
"""Open the New Session dialog, optionally select a resource profile, and launch.

Raises ``ResourceProfileDisabled`` if the selected profile is disabled for
the authenticated user.
"""
page.locator(Homepage.NEW_SESSION_BUTTON).first.click(timeout=TIMEOUT_DIALOG)

dialog = page.locator(NewSessionDialog.DIALOG)
Expand All @@ -109,6 +135,15 @@ def _launch_session(
profile_dropdown.click()
page.wait_for_timeout(500)
option = page.locator(f"[role='option']:has-text('{profile}')").first
option.wait_for(state="visible", timeout=TIMEOUT_QUICK)
if _option_is_disabled(option):
# Profile is offered but disabled for this user (e.g. a
# group-restricted profile). Clicking would just block
# until timeout, so close the dialog and signal the caller.
page.keyboard.press("Escape")
page.keyboard.press("Escape")
expect(dialog).to_be_hidden(timeout=TIMEOUT_DIALOG)
raise ResourceProfileDisabled(profile)
option.click(timeout=TIMEOUT_QUICK)
else:
pytest.skip(f"Resource profile dropdown not available; cannot select '{profile}'")
Expand Down Expand Up @@ -164,7 +199,15 @@ def launch_sessions(page: Page, vip_config):
# Auto-detect from the dropdown.
detected = _detect_profiles(page)
if detected:
profiles_to_test = detected
enabled = [p.name for p in detected if not p.disabled]
if not enabled:
# Every profile is offered but disabled for this user — nothing
# is launchable, so there is no capacity to exercise.
names = ", ".join(p.name for p in detected)
pytest.skip(
f"All resource profiles are disabled for the authenticated user: {names}"
)
profiles_to_test = enabled
else:
# No profiles dropdown — launch with default.
profiles_to_test = [None]
Expand All @@ -173,12 +216,36 @@ def launch_sessions(page: Page, vip_config):
session_count = 1

all_sessions: list[dict[str, str | None]] = []
disabled_profiles: list[str] = []
for profile in profiles_to_test:
profile_disabled = False
for i in range(session_count):
label = profile or "default"
name = f"{_SESSION_PREFIX}{label}_{i}"
_launch_session(page, name, profile)
try:
_launch_session(page, name, profile)
except ResourceProfileDisabled as exc:
# Configured profile the current user cannot launch. Treat as
# an environment condition (entitlement/group restriction):
# record it and move on to the remaining profiles rather than
# aborting the whole scenario.
disabled_profiles.append(exc.profile)
profile_disabled = True
break
all_sessions.append({"name": name, "profile": profile})
if profile_disabled:
continue

if not all_sessions and disabled_profiles:
# No configured profile was launchable — there is no capacity to
# exercise. Skip rather than fail so the scenario is reported as
# skipped (not passed) on a correctly-restricted test account,
# distinct from an actual capacity failure.
names = ", ".join(disabled_profiles)
pytest.skip(
f"Resource profile(s) '{names}' are disabled for the authenticated "
"user (likely a group/entitlement restriction)"
)

return all_sessions

Expand Down
33 changes: 30 additions & 3 deletions src/vip_tests/workbench/test_session_capacity_k8s.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
from vip_tests.workbench.conftest import (
TIMEOUT_DIALOG,
TIMEOUT_QUICK,
ResourceProfileDisabled,
_option_is_disabled,
_quit_vip_sessions_via_cookies,
wait_for_session_active,
)
Expand All @@ -46,7 +48,11 @@


def _launch_session(page: Page, session_name: str, profile: str | None = None) -> None:
"""Open the New Session dialog and launch with an optional resource profile."""
"""Open the New Session dialog and launch with an optional resource profile.

Raises ``ResourceProfileDisabled`` if the selected profile is disabled for
the authenticated user.
"""
page.locator(Homepage.NEW_SESSION_BUTTON).first.click(timeout=TIMEOUT_DIALOG)

dialog = page.locator(NewSessionDialog.DIALOG)
Expand All @@ -68,6 +74,15 @@ def _launch_session(page: Page, session_name: str, profile: str | None = None) -
profile_dropdown.click()
page.wait_for_timeout(500)
option = page.locator(f"[role='option']:has-text('{profile}')").first
option.wait_for(state="visible", timeout=TIMEOUT_QUICK)
if _option_is_disabled(option):
# Profile is offered but disabled for this user (e.g. a
# group-restricted profile). Clicking would just block
# until timeout, so close the dialog and signal the caller.
page.keyboard.press("Escape")
page.keyboard.press("Escape")
expect(dialog).to_be_hidden(timeout=TIMEOUT_DIALOG)
raise ResourceProfileDisabled(profile)
option.click(timeout=TIMEOUT_QUICK)
else:
pytest.skip(f"Resource profile dropdown not available; cannot select '{profile}'")
Expand Down Expand Up @@ -233,7 +248,13 @@ def launch_profiled_session(page: Page, vip_config) -> list[dict]:
profile_map = vip_config.workbench.kubernetes.node_pool_profiles
profile = next(iter(profile_map.values()))
name = f"{_SESSION_PREFIX}prof_0"
_launch_session(page, name, profile=profile)
try:
_launch_session(page, name, profile=profile)
except ResourceProfileDisabled as exc:
pytest.skip(
f"Resource profile '{exc.profile}' is disabled for the "
"authenticated user (likely a group/entitlement restriction)"
)
return [{"name": name, "profile": profile}]


Expand All @@ -248,7 +269,13 @@ def launch_limited_session(page: Page, vip_config) -> list[dict]:
)
profile = profiles[0] if profiles else None
name = f"{_SESSION_PREFIX}lim_0"
_launch_session(page, name, profile=profile)
try:
_launch_session(page, name, profile=profile)
except ResourceProfileDisabled as exc:
pytest.skip(
f"Resource profile '{exc.profile}' is disabled for the "
"authenticated user (likely a group/entitlement restriction)"
)
return [{"name": name, "profile": profile}]


Expand Down
Loading