diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/playwright.py b/packages/pytest-simcore/src/pytest_simcore/helpers/playwright.py index 38539f0d7fb..f063b6efd61 100644 --- a/packages/pytest-simcore/src/pytest_simcore/helpers/playwright.py +++ b/packages/pytest-simcore/src/pytest_simcore/helpers/playwright.py @@ -5,12 +5,15 @@ from collections import defaultdict from collections.abc import Generator, Iterator from dataclasses import dataclass, field +from datetime import UTC, datetime, timedelta from enum import Enum, unique from typing import Any, Final +import httpx from playwright.sync_api import FrameLocator, Page, Request from playwright.sync_api import TimeoutError as PlaywrightTimeoutError from playwright.sync_api import WebSocket +from pydantic import AnyUrl from pytest_simcore.helpers.logging_tools import log_context SECOND: Final[int] = 1000 @@ -196,9 +199,11 @@ def __call__(self, message: str) -> None: class SocketIONodeProgressCompleteWaiter: node_id: str logger: logging.Logger + product_url: AnyUrl _current_progress: dict[NodeProgressType, float] = field( default_factory=defaultdict ) + _last_poll_timestamp: datetime = field(default_factory=lambda: datetime.now(tz=UTC)) def __call__(self, message: str) -> bool: # socket.io encodes messages like so @@ -234,6 +239,27 @@ def __call__(self, message: str) -> bool: round(progress, 1) == 1.0 for progress in self._current_progress.values() ) + + _current_timestamp = datetime.now(UTC) + if _current_timestamp - self._last_poll_timestamp > timedelta(seconds=5): + url = f"https://{self.node_id}.services.{self.get_partial_product_url()}" + response = httpx.get(url, timeout=10) + self.logger.info( + "Querying the service endpoint from the E2E test. Url: %s Response: %s", + url, + response, + ) + if response.status_code <= 401: + # NOTE: If the response status is less than 400, it means that the backend is ready (There are some services that respond with a 3XX) + # MD: for now I have included 401 - as this also means that backend is ready + if self.got_expected_node_progress_types(): + self.logger.warning( + "⚠️ Progress bar didn't receive 100 percent but service is already running: %s ⚠️", # https://github.com/ITISFoundation/osparc-simcore/issues/6449 + self.get_current_progress(), + ) + return True + self._last_poll_timestamp = datetime.now(UTC) + return False def got_expected_node_progress_types(self): @@ -245,6 +271,9 @@ def got_expected_node_progress_types(self): def get_current_progress(self): return self._current_progress.values() + def get_partial_product_url(self): + return f"{self.product_url}".split("//")[1] + def wait_for_pipeline_state( current_state: RunningState, @@ -332,9 +361,12 @@ def expected_service_running( websocket: WebSocket, timeout: int, press_start_button: bool, + product_url: AnyUrl, ) -> Generator[ServiceRunning, None, None]: with log_context(logging.INFO, msg="Waiting for node to run") as ctx: - waiter = SocketIONodeProgressCompleteWaiter(node_id=node_id, logger=ctx.logger) + waiter = SocketIONodeProgressCompleteWaiter( + node_id=node_id, logger=ctx.logger, product_url=product_url + ) service_running = ServiceRunning(iframe_locator=None) try: @@ -366,12 +398,15 @@ def wait_for_service_running( websocket: WebSocket, timeout: int, press_start_button: bool, + product_url: AnyUrl, ) -> FrameLocator: """NOTE: if the service was already started this will not work as some of the required websocket events will not be emitted again In which case this will need further adjutment""" with log_context(logging.INFO, msg="Waiting for node to run") as ctx: - waiter = SocketIONodeProgressCompleteWaiter(node_id=node_id, logger=ctx.logger) + waiter = SocketIONodeProgressCompleteWaiter( + node_id=node_id, logger=ctx.logger, product_url=product_url + ) with websocket.expect_event("framereceived", waiter, timeout=timeout): if press_start_button: _trigger_service_start(page, node_id) diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/playwright_sim4life.py b/packages/pytest-simcore/src/pytest_simcore/helpers/playwright_sim4life.py index d2349d6fa7c..c59718f4aff 100644 --- a/packages/pytest-simcore/src/pytest_simcore/helpers/playwright_sim4life.py +++ b/packages/pytest-simcore/src/pytest_simcore/helpers/playwright_sim4life.py @@ -6,8 +6,7 @@ import arrow from playwright.sync_api import FrameLocator, Page, WebSocket, expect -from pydantic import TypeAdapter # pylint: disable=no-name-in-module -from pydantic import ByteSize +from pydantic import AnyUrl, ByteSize, TypeAdapter # pylint: disable=no-name-in-module from .logging_tools import log_context from .playwright import ( @@ -105,6 +104,7 @@ def wait_for_launched_s4l( *, autoscaled: bool, copy_workspace: bool, + product_url: AnyUrl, ) -> WaitForS4LDict: with log_context(logging.INFO, "launch S4L") as ctx: predicate = S4LWaitForWebsocket(logger=ctx.logger) @@ -130,6 +130,7 @@ def wait_for_launched_s4l( ) + (_S4L_COPY_WORKSPACE_TIME if copy_workspace else 0), press_start_button=False, + product_url=product_url, ) s4l_websocket = ws_info.value ctx.logger.info("acquired S4L websocket!") diff --git a/tests/e2e-playwright/requirements/_test.in b/tests/e2e-playwright/requirements/_test.in index a50905c9086..b3fd9442f78 100644 --- a/tests/e2e-playwright/requirements/_test.in +++ b/tests/e2e-playwright/requirements/_test.in @@ -13,3 +13,4 @@ pytest-runner pytest-sugar pyyaml tenacity +httpx diff --git a/tests/e2e-playwright/requirements/_test.txt b/tests/e2e-playwright/requirements/_test.txt index 2934b76a3a8..011cb6fbd7c 100644 --- a/tests/e2e-playwright/requirements/_test.txt +++ b/tests/e2e-playwright/requirements/_test.txt @@ -1,24 +1,43 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile _test.in +# annotated-types==0.7.0 # via pydantic +anyio==4.6.2.post1 + # via httpx arrow==1.3.0 - # via -r requirements/_test.in + # via -r _test.in certifi==2024.8.30 - # via requests + # via + # httpcore + # httpx + # requests charset-normalizer==3.3.2 # via requests dnspython==2.6.1 # via email-validator docker==7.1.0 - # via -r requirements/_test.in + # via -r _test.in email-validator==2.2.0 # via pydantic faker==29.0.0 - # via -r requirements/_test.in + # via -r _test.in greenlet==3.0.3 # via playwright +h11==0.14.0 + # via httpcore +httpcore==1.0.7 + # via httpx +httpx==0.27.2 + # via -r _test.in idna==3.10 # via + # anyio # email-validator + # httpx # requests iniconfig==2.0.0 # via pytest @@ -34,8 +53,8 @@ playwright==1.47.0 # via pytest-playwright pluggy==1.5.0 # via pytest -pydantic==2.9.2 - # via -r requirements/_test.in +pydantic[email]==2.9.2 + # via -r _test.in pydantic-core==2.23.4 # via pydantic pyee==12.0.0 @@ -51,17 +70,17 @@ pytest==8.3.3 pytest-base-url==2.1.0 # via pytest-playwright pytest-html==4.1.1 - # via -r requirements/_test.in + # via -r _test.in pytest-instafail==0.5.0 - # via -r requirements/_test.in + # via -r _test.in pytest-metadata==3.1.1 # via pytest-html pytest-playwright==0.5.2 - # via -r requirements/_test.in + # via -r _test.in pytest-runner==6.0.1 - # via -r requirements/_test.in + # via -r _test.in pytest-sugar==1.0.0 - # via -r requirements/_test.in + # via -r _test.in python-dateutil==2.9.0.post0 # via # arrow @@ -69,15 +88,19 @@ python-dateutil==2.9.0.post0 python-slugify==8.0.4 # via pytest-playwright pyyaml==6.0.2 - # via -r requirements/_test.in + # via -r _test.in requests==2.32.3 # via # docker # pytest-base-url six==1.16.0 # via python-dateutil +sniffio==1.3.1 + # via + # anyio + # httpx tenacity==9.0.0 - # via -r requirements/_test.in + # via -r _test.in termcolor==2.4.0 # via pytest-sugar text-unidecode==1.3 diff --git a/tests/e2e-playwright/tests/sim4life/test_sim4life.py b/tests/e2e-playwright/tests/sim4life/test_sim4life.py index 96c361bb546..924e6efa535 100644 --- a/tests/e2e-playwright/tests/sim4life/test_sim4life.py +++ b/tests/e2e-playwright/tests/sim4life/test_sim4life.py @@ -11,6 +11,7 @@ from typing import Any from playwright.sync_api import Page, WebSocket +from pydantic import AnyUrl from pytest_simcore.helpers.playwright import ( ServiceType, web_socket_default_log_handler, @@ -33,6 +34,7 @@ def test_sim4life( use_plus_button: bool, is_autoscaled: bool, check_videostreaming: bool, + product_url: AnyUrl, ): if use_plus_button: project_data = create_project_from_new_button(service_key) @@ -54,6 +56,7 @@ def test_sim4life( log_in_and_out, autoscaled=is_autoscaled, copy_workspace=False, + product_url=product_url, ) s4l_websocket = resp["websocket"] with web_socket_default_log_handler(s4l_websocket): diff --git a/tests/e2e-playwright/tests/tip/test_ti_plan.py b/tests/e2e-playwright/tests/tip/test_ti_plan.py index aa878eb9274..56f028d197d 100644 --- a/tests/e2e-playwright/tests/tip/test_ti_plan.py +++ b/tests/e2e-playwright/tests/tip/test_ti_plan.py @@ -14,6 +14,7 @@ from typing import Any, Final from playwright.sync_api import Page, WebSocket +from pydantic import AnyUrl from pytest_simcore.helpers.logging_tools import log_context from pytest_simcore.helpers.playwright import ( MINUTE, @@ -92,6 +93,7 @@ def test_classic_ti_plan( # noqa: PLR0915 is_autoscaled: bool, is_product_lite: bool, create_tip_plan_from_dashboard: Callable[[str], dict[str, Any]], + product_url: AnyUrl, ): with log_context(logging.INFO, "Checking 'Access TIP' teaser"): # click to open and expand @@ -141,6 +143,7 @@ def test_classic_ti_plan( # noqa: PLR0915 else _ELECTRODE_SELECTOR_MAX_STARTUP_TIME ), press_start_button=False, + product_url=product_url, ) # NOTE: Sometimes this iframe flicks and shows a white page. This wait will avoid it page.wait_for_timeout(_ELECTRODE_SELECTOR_FLICKERING_WAIT_TIME) @@ -200,6 +203,7 @@ def test_classic_ti_plan( # noqa: PLR0915 else _JLAB_MAX_STARTUP_MAX_TIME ), press_start_button=False, + product_url=product_url, ) as service_running: app_mode_trigger_next_app(page) ti_iframe = service_running.iframe_locator @@ -284,6 +288,7 @@ def test_classic_ti_plan( # noqa: PLR0915 else _POST_PRO_MAX_STARTUP_TIME ), press_start_button=False, + product_url=product_url, ) as service_running: app_mode_trigger_next_app(page) s4l_postpro_iframe = service_running.iframe_locator