Skip to content

Commit

Permalink
Added Scope feature to timeout, retry_assertion and strice_mode (#2319)
Browse files Browse the repository at this point in the history
* added Wait For Condition keyword
* added scope to self.timeout
* fixed bugs in Switch Page

Signed-off-by: René Rohner <snooz@posteo.de>
  • Loading branch information
Snooz82 authored Sep 23, 2022
1 parent 6bc8332 commit 64eeaa4
Show file tree
Hide file tree
Showing 25 changed files with 617 additions and 103 deletions.
38 changes: 25 additions & 13 deletions Browser/base/librarycomponent.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

from robot.utils import timestr_to_secs # type: ignore

from ..utils import get_variable_value, logger
from ..utils import SettingsStack, get_variable_value, logger
from ..utils.data_types import DelayedKeyword, HighLightElement

if TYPE_CHECKING:
Expand All @@ -43,19 +43,27 @@ def playwright(self):

@property
def timeout(self) -> float:
return self.library.timeout
return self.library.timeout_stack.get()

@timeout.setter
def timeout(self, value: float):
self.library.timeout = value
@property
def timeout_stack(self) -> SettingsStack:
return self.library.timeout_stack

@timeout_stack.setter
def timeout_stack(self, stack: SettingsStack):
self.library.timeout_stack = stack

@property
def retry_assertions_for(self) -> float:
return self.library.retry_assertions_for
return self.library.retry_assertions_for_stack.get()

@property
def retry_assertions_for_stack(self) -> SettingsStack:
return self.library.retry_assertions_for_stack

@retry_assertions_for.setter
def retry_assertions_for(self, value: float):
self.library.retry_assertions_for = value
@retry_assertions_for_stack.setter
def retry_assertions_for_stack(self, stack: SettingsStack):
self.library.retry_assertions_for_stack = stack

@property
def unresolved_promises(self):
Expand Down Expand Up @@ -138,11 +146,15 @@ def _replace_placeholder_variable(self, placeholder):

@property
def strict_mode(self) -> bool:
return self.library.strict_mode
return self.library.strict_mode_stack.get()

@property
def strict_mode_stack(self) -> SettingsStack:
return self.library.strict_mode_stack

@strict_mode.setter
def strict_mode(self, mode: bool):
self.library.strict_mode = mode
@strict_mode_stack.setter
def strict_mode_stack(self, stack: SettingsStack):
self.library.strict_mode_stack = stack

def parse_run_on_failure_keyword(
self, keyword_name: Union[str, None]
Expand Down
149 changes: 99 additions & 50 deletions Browser/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from concurrent.futures._base import Future
from datetime import timedelta
from pathlib import Path
from typing import Dict, List, Optional, Set, Union
from typing import Any, Dict, List, Optional, Set, Union

from assertionengine import AssertionOperator, Formatter
from overrides import overrides
Expand Down Expand Up @@ -52,7 +52,15 @@
)
from .keywords.crawling import Crawling
from .playwright import Playwright
from .utils import AutoClosingLevel, get_normalized_keyword, is_falsy, keyword, logger
from .utils import (
AutoClosingLevel,
Scope,
SettingsStack,
get_normalized_keyword,
is_falsy,
keyword,
logger,
)

# Importing this directly from .utils break the stub type checks
from .utils.data_types import DelayedKeyword, HighLightElement, SupportedBrowsers
Expand Down Expand Up @@ -609,6 +617,22 @@ class Browser(DynamicCore):
``PORT`` is the port you want to use for the node process.
To execute tests then with pabot for example do ``ROBOT_FRAMEWORK_BROWSER_NODE_PORT=PORT pabot ..``.
= Scope Setting =
Some keywords which manipulates library settings have a scope argument.
With that scope argument one can set the "live time" of that setting.
Available Scopes are: `Global`, `Suite` and `Test`/`Task`
See `Scope`.
Is a scope finished, this scoped setting, like timeout, will no longer be used.
Live Times:
- A `Global` scope will live forever until it is overwritten by another `Global` scope. Or locally temporarily overridden by a more narrow scope.
- A `Suite` scope will locally override the `Global` scope and live until the end of the Suite within it is set, or if it is overwritten by a later setting with `Global` or same scope. Children suite does inherit the setting from the parent suite but also may have its own local `Suite` setting that then will be inherited to its children suites.
- A `Test` or `Task` scope will be inherited from its parent suite but when set, lives until the end of that particular test or task.
A new set higher order scope will always remove the lower order scope which may be in charge.
So the setting of a `Suite` scope from a test, will set that scope to the robot file suite where that test is and removes the `Test` scope that may have been in place.
= Extending Browser library with a JavaScript module =
Browser library can be extended with JavaScript. The module must be in CommonJS format that Node.js uses.
Expand Down Expand Up @@ -774,13 +798,17 @@ def __init__(
Waiter(self),
WebAppState(self),
]

self.timeout = self.convert_timeout(params["timeout"])
self.timeout_stack = SettingsStack(
self.convert_timeout(params["timeout"]), self
)
self.playwright = Playwright(
self, params["enable_playwright_debug"], playwright_process_port
)
self._auto_closing_level: AutoClosingLevel = params["auto_closing_level"]
self.retry_assertions_for = self.convert_timeout(params["retry_assertions_for"])
self.retry_assertions_for_stack = SettingsStack(
self.convert_timeout(params["retry_assertions_for"]),
self,
)
# Parsing needs keywords to be discovered.
self.external_browser_executable: Dict[SupportedBrowsers, str] = (
params["external_browser_executable"] or {}
Expand All @@ -790,7 +818,7 @@ def __init__(
self.presenter_mode: Union[HighLightElement, bool] = params[
"enable_presenter_mode"
]
self.strict_mode = params["strict"]
self.strict_mode_stack = SettingsStack(params["strict"], self)
self.show_keyword_call_banner = params["show_keyword_call_banner"]

self._execution_stack: List[dict] = []
Expand All @@ -801,12 +829,17 @@ def __init__(
self.keyword_call_banner_add_style: str = ""
self._keyword_formatters: dict = {}
self._current_loglevel: Optional[str] = None
self.is_test_case_running = False

DynamicCore.__init__(self, libraries)
self.run_on_failure_keyword = self._parse_run_on_failure_keyword(
params["run_on_failure"]
)

@property
def timeout(self):
return self.timeout_stack.get()

def _parse_run_on_failure_keyword(
self, keyword_name: Union[str, None]
) -> DelayedKeyword:
Expand Down Expand Up @@ -940,6 +973,7 @@ def state_file(self):
return self.browser_output / "state"

def _start_suite(self, name, attrs):
self._start_stack(attrs, Scope.Suite)
if not self._suite_cleanup_done:
self._suite_cleanup_done = True
for path in [
Expand All @@ -957,14 +991,60 @@ def _start_suite(self, name, attrs):
except ConnectionError as e:
logger.debug(f"Browser._start_suite connection problem: {e}")

def _start_test(self, name, attr):
def _start_test(self, name, attrs):
self._start_stack(attrs, Scope.Test)
self.is_test_case_running = True
if self._auto_closing_level == AutoClosingLevel.TEST:
try:
self._execution_stack.append(self.get_browser_catalog())
except ConnectionError as e:
logger.debug(f"Browser._start_test connection problem: {e}")

def _start_keyword(self, name, attrs):
if (
self.show_keyword_call_banner is False
or (self.show_keyword_call_banner is None and not self.presenter_mode)
or attrs["libname"] != "Browser"
or attrs["status"] == "NOT RUN"
):
return
self._show_keyword_call(attrs)
self.current_arguments = tuple(attrs["args"])
if "secret" in attrs["kwname"].lower() and attrs["libname"] == "Browser":
self._set_logging(False)

if attrs["type"] == "Teardown":
timeout_pattern = "Test timeout .* exceeded."
test = EXECUTION_CONTEXTS.current.test
if (
test is not None
and test.status == "FAIL"
and re.match(timeout_pattern, test.message)
):
self.screenshot_on_failure(test.name)

def run_keyword(self, name, args, kwargs=None):
try:
return DynamicCore.run_keyword(self, name, args, kwargs)
except AssertionError as e:
self.keyword_error()
e.args = self._alter_keyword_error(e.args)
if self.pause_on_failure:
sys.__stdout__.write(f"\n[ FAIL ] {e}")
sys.__stdout__.write(
"\n[Paused on failure] Press Enter to continue..\n"
)
sys.__stdout__.flush()
input()
raise e

def _end_keyword(self, name, attrs):
if "secret" in attrs["kwname"].lower() and attrs["libname"] == "Browser":
self._set_logging(True)

def _end_test(self, name, attrs):
self._end_scope(attrs)
self.is_test_case_running = False
if len(self._unresolved_promises) > 0:
logger.warn(f"Waiting unresolved promises at the end of test '{name}'")
self.wait_for_all_promises()
Expand All @@ -984,6 +1064,7 @@ def _end_test(self, name, attrs):
logger.debug(f"Browser._end_test connection problem: {e}")

def _end_suite(self, name, attrs):
self._end_scope(attrs)
if self._auto_closing_level != AutoClosingLevel.MANUAL:
if len(self._execution_stack) == 0:
logger.debug("Browser._end_suite empty execution stack")
Expand All @@ -996,6 +1077,16 @@ def _end_suite(self, name, attrs):
except ConnectionError as e:
logger.debug(f"Browser._end_suite connection problem: {e}")

def _start_stack(self, attrs: Dict[str, Any], scope: Scope):
self.timeout_stack.start(attrs["id"], scope)
self.strict_mode_stack.start(attrs["id"], scope)
self.retry_assertions_for_stack.start(attrs["id"], scope)

def _end_scope(self, attrs: Dict[str, Any]):
self.timeout_stack.end(attrs["id"])
self.strict_mode_stack.end(attrs["id"])
self.retry_assertions_for_stack.end(attrs["id"])

def _prune_execution_stack(self, catalog_before: dict) -> None:
catalog_after = self.get_browser_catalog()
ctx_before_ids = [c["id"] for b in catalog_before for c in b["contexts"]]
Expand Down Expand Up @@ -1032,48 +1123,6 @@ def _alter_keyword_error(cls, args: tuple) -> tuple:
message = augment(message)
return (message,) + args[1:]

def run_keyword(self, name, args, kwargs=None):
try:
return DynamicCore.run_keyword(self, name, args, kwargs)
except AssertionError as e:
self.keyword_error()
e.args = self._alter_keyword_error(e.args)
if self.pause_on_failure:
sys.__stdout__.write(f"\n[ FAIL ] {e}")
sys.__stdout__.write(
"\n[Paused on failure] Press Enter to continue..\n"
)
sys.__stdout__.flush()
input()
raise e

def _start_keyword(self, name, attrs):
if (
self.show_keyword_call_banner is False
or (self.show_keyword_call_banner is None and not self.presenter_mode)
or attrs["libname"] != "Browser"
or attrs["status"] == "NOT RUN"
):
return
self._show_keyword_call(attrs)
self.current_arguments = tuple(attrs["args"])
if "secret" in attrs["kwname"].lower() and attrs["libname"] == "Browser":
self._set_logging(False)

if attrs["type"] == "Teardown":
timeout_pattern = "Test timeout .* exceeded."
test = EXECUTION_CONTEXTS.current.test
if (
test is not None
and test.status == "FAIL"
and re.match(timeout_pattern, test.message)
):
self.screenshot_on_failure(test.name)

def _end_keyword(self, name, attrs):
if "secret" in attrs["kwname"].lower() and attrs["libname"] == "Browser":
self._set_logging(True)

def _set_logging(self, status: bool):
try:
context = BuiltIn()._context.output
Expand Down Expand Up @@ -1156,7 +1205,7 @@ def _failure_screenshot_path(self):

def get_timeout(self, timeout: Union[timedelta, None]) -> float:
if timeout is None:
return self.timeout
return self.timeout_stack.get()
return self.convert_timeout(timeout)

def convert_timeout(
Expand Down
19 changes: 13 additions & 6 deletions Browser/keywords/browser_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

from ..base import LibraryComponent
from ..generated.playwright_pb2 import Request
from ..utils import keyword, logger
from ..utils import Scope, keyword, logger
from ..utils.data_types import BoundingBox, Permission, ScreenshotFileTypes


Expand Down Expand Up @@ -205,11 +205,14 @@ def _is_embed(self, filename: str) -> bool:
return True if filename.upper() == "EMBED" else False

@keyword(tags=("Setter", "Config"))
def set_browser_timeout(self, timeout: timedelta) -> str:
def set_browser_timeout(
self, timeout: timedelta, scope: Scope = Scope.Suite
) -> str:
"""Sets the timeout used by most input and getter keywords.
| =Arguments= | =Description= |
| ``timeout`` | Timeout of it is for current playwright context and for new contexts. Supports Robot Framework [https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#time-format|time format] . Returns the previous value of the timeout. |
| ``scope`` | Scope defines the live time of that setting. Available values are ``Global``, ``Suite`` or ``Test``/``Task``. See `Scope Settings` for more details. |
Example:
| ${old_timeout} = `Set Browser Timeout` 1m 30 seconds
Expand All @@ -219,7 +222,7 @@ def set_browser_timeout(self, timeout: timedelta) -> str:
[https://forum.robotframework.org/t//4328|Comment >>]
"""
old_timeout = self.millisecs_to_timestr(self.timeout)
self.timeout = self.convert_timeout(timeout)
self.timeout_stack.set(self.convert_timeout(timeout), scope)
try:
with self.playwright.grpc_channel() as stub:
response = stub.SetTimeout(Request().Timeout(timeout=self.timeout))
Expand All @@ -232,10 +235,14 @@ def set_browser_timeout(self, timeout: timedelta) -> str:
return old_timeout

@keyword(tags=("Setter", "Config"))
def set_retry_assertions_for(self, timeout: timedelta) -> str:
def set_retry_assertions_for(
self, timeout: timedelta, scope: Scope = Scope.Suite
) -> str:
"""Sets the timeout used in retrying assertions when they fail.
Assertion retry timeout will determine how long Browser library will retry an assertion to be true.
| =Arguments= | =Description= |
| ``timeout`` | Assertion retry timeout will determine how long Browser library will retry an assertion to be true. |
| ``scope`` | Scope defines the live time of that setting. Available values are ``Global``, ``Suite`` or ``Test``/``Task``. See `Scope` for more details. |
The other keyword `Set Browser timeout` controls how long Playwright
will perform waiting in the node side for Elements to fulfill the
Expand All @@ -255,7 +262,7 @@ def set_retry_assertions_for(self, timeout: timedelta) -> str:
[https://forum.robotframework.org/t//4331|Comment >>]
"""
old_retry_assertions_for = self.millisecs_to_timestr(self.retry_assertions_for)
self.retry_assertions_for = self.convert_timeout(timeout)
self.retry_assertions_for_stack.set(self.convert_timeout(timeout), scope)
return old_retry_assertions_for

@keyword(tags=("Setter", "Config"))
Expand Down
Loading

0 comments on commit 64eeaa4

Please sign in to comment.