Skip to content

Commit

Permalink
Improve exception message building (#42)
Browse files Browse the repository at this point in the history
* Improve exception message building
  • Loading branch information
mreiche authored Dec 7, 2024
1 parent dd0a603 commit f2e94d2
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 78 deletions.
48 changes: 25 additions & 23 deletions paf/assertion.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ def param(value: any):
return f"[{value}]"


class AssertionErrorWrapper(RetryException, AssertionError):
pass


class AbstractAssertion(Generic[ACTUAL_TYPE], HasParent, ABC):
def __init__(
self,
Expand Down Expand Up @@ -69,22 +73,22 @@ def _test_sequence(

try:
def perform_test():
assert test(self.actual)
assert test(self.actual), "Expected"

retry(perform_test, lambda e: listener.assertion_failed(self, self._find_closest_ui_element(), e))
listener.assertion_passed(self, self._find_closest_ui_element())
return True

except RetryException as e:
listener.assertion_failed_finally(self, self._find_closest_ui_element(), e)
except RetryException as exception:
exception.add_subject(self.name_path)
if additional_subject:
exception.add_subject(additional_subject())
#exception.update_sequence(sequence)
listener.assertion_failed_finally(self, self._find_closest_ui_element(), exception)

if self._raise:
subject = self.name_path

if additional_subject:
subject += additional_subject()

raise AssertionError(f"Expected {subject} {e}")
raise AssertionErrorWrapper(exception)
#AssertionErrorWrapper(AssertionError(f"{exception.enclosed_exception} {subject}"), sequence)
return False

@property
Expand Down Expand Up @@ -121,44 +125,42 @@ def map(self, mapper: Mapper):
return self.__class__(
parent=self,
actual_supplier=lambda: mapper(self._actual_supplier()),
name_supplier=lambda: f"mapped ",
name_supplier=lambda: f" mapped ",
)



def between(self, lower: Number, higher: Number):
return BinaryAssertion(
parent=self,
actual_supplier=lambda: lower <= self._actual_supplier() <= higher,
name_supplier=lambda: f"between {Format.param(lower)} and {Format.param(higher)} ",
name_supplier=lambda: f"between {Format.param(lower)} and {Format.param(higher)}",
)

def greater_than(self, expected: Number):
return BinaryAssertion(
parent=self,
actual_supplier=lambda: self._actual_supplier() > expected,
name_supplier=lambda: f"greater than {Format.param(expected)} ",
name_supplier=lambda: f"greater than {Format.param(expected)}",
)

def lower_than(self, expected: Number):
return BinaryAssertion(
parent=self,
actual_supplier=lambda: self._actual_supplier() < expected,
name_supplier=lambda: f"lower than {Format.param(expected)} ",
name_supplier=lambda: f"lower than {Format.param(expected)}",
)

def greater_equal_than(self, expected: Number):
return BinaryAssertion(
parent=self,
actual_supplier=lambda: self._actual_supplier() >= expected,
name_supplier=lambda: f"greater equal than {Format.param(expected)} ",
name_supplier=lambda: f"greater equal than {Format.param(expected)}",
)

def lower_equal_than(self, expected: Number):
return BinaryAssertion(
parent=self,
actual_supplier=lambda: self._actual_supplier() <= expected,
name_supplier=lambda: f"lower equal than {Format.param(expected)} ",
name_supplier=lambda: f"lower equal than {Format.param(expected)}",
)


Expand All @@ -167,21 +169,21 @@ def starts_with(self, expected: str):
return BinaryAssertion(
parent=self,
actual_supplier=lambda: str(self._actual_supplier()).startswith(expected),
name_supplier=lambda: f"starts with {Format.param(expected)} ",
name_supplier=lambda: f"starts with {Format.param(expected)}",
)

def ends_with(self, expected: str):
return BinaryAssertion(
parent=self,
actual_supplier=lambda: str(self._actual_supplier()).endswith(expected),
name_supplier=lambda: f"ends with {Format.param(expected)} ",
name_supplier=lambda: f"ends with {Format.param(expected)}",
)

def contains(self, expected: str):
return BinaryAssertion(
parent=self,
actual_supplier=lambda: str(self._actual_supplier()).find(expected) >= 0,
name_supplier=lambda: f"contains {Format.param(expected)} ",
name_supplier=lambda: f"contains {Format.param(expected)}",
)

def matches(self, regex: str | re.Pattern):
Expand All @@ -191,7 +193,7 @@ def matches(self, regex: str | re.Pattern):
return BinaryAssertion(
parent=self,
actual_supplier=lambda: regex.search(self._actual_supplier()) is not None,
name_supplier=lambda: f"matches {Format.param(regex.pattern)} ",
name_supplier=lambda: f"matches {Format.param(regex.pattern)}",
)

def has_words(self, *words: any):
Expand All @@ -201,15 +203,15 @@ def has_words(self, *words: any):
return BinaryAssertion(
parent=self,
actual_supplier=lambda: regex.search(self._actual_supplier()) is not None,
name_supplier=lambda: f"has words {Format.param(pattern)} ",
name_supplier=lambda: f"has words {Format.param(pattern)}",
)

@property
def length(self):
return QuantityAssertion(
parent=self,
actual_supplier=lambda: len(self._actual_supplier()),
name_supplier=lambda: f"length {Format.param(len(self._actual_supplier()))} ",
name_supplier=lambda: f"length {Format.param(len(self._actual_supplier()))}",
)


Expand Down
75 changes: 72 additions & 3 deletions paf/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from dataclasses import dataclass
from datetime import datetime
from enum import Enum

from time import sleep, time
from typing import Callable, Optional
import inject
from selenium.webdriver.remote.webelement import WebElement

Expand Down Expand Up @@ -135,12 +136,12 @@ def datetime(self, date: datetime):

class NotFoundException(Exception):
def __init__(self):
super().__init__(f"Element not found")
super().__init__(f"Not found")


class NotUniqueException(Exception):
def __init__(self):
super().__init__(f"Element not unique")
super().__init__(f"Not unique")


class WebdriverRetainer(ABC):
Expand All @@ -150,5 +151,73 @@ def webdriver(self): # pragma: no cover
pass


class SubjectException(Exception):

def __init__(self, exception: Exception):
# if isinstance(exception, EncloseException):
# self._enclosed_exception = exception._enclosed_exception
# else:
self._subjects = []
if isinstance(exception, SubjectException):
self._subjects.extend(exception._subjects)
else:
self.add_subject(f"{exception}")
#self._enclosed_exception = exception

def add_subject(self, subject: str):
self._subjects.append(subject)

#@property
#def enclosed_exception(self):
#return self._enclosed_exception


class Sequence:
def __init__(self, retry_count: int = 3, wait_after_fail: float = 0.2):
self._max = retry_count
self._wait = wait_after_fail
self._count = 0
self._start_time = 0

def run(self, sequence: Callable[[], bool]):
self._start_time = time()
while True:
if sequence() or self._count >= self._max:
break

self._count += 1
sleep(self._wait)

@property
def duration(self):
return time() - self._start_time

@property
def count(self):
return self._count


class RetryException(SubjectException):
def __init__(self, exception: Exception, sequence: Sequence = None):
super().__init__(exception)

if sequence:
self._count = sequence.count
self._duration = sequence.duration
elif isinstance(exception, RetryException):
self._count = exception._count
self._duration = exception._duration
#self.update_sequence(sequence)

#def update_sequence(self, sequence: Sequence):

def __str__(self):
#prefix = f"{self._enclosed_exception}"
prefix = " ".join(self._subjects)
if len(prefix) > 0:
prefix += " "
return f"{prefix}after {self._count} retries ({round(self._duration, 2)} seconds)"


def inject_config(binder: inject.Binder):
binder.bind(Formatter, Formatter())
37 changes: 2 additions & 35 deletions paf/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from time import sleep, time
from typing import Callable, Optional

from paf.common import Property
from paf.common import Property, RetryException, Sequence
from paf.types import Consumer


Expand All @@ -27,39 +27,6 @@ def __set_config(config: Config):
__config = config


class Sequence:
def __init__(self, retry_count: int = 3, wait_after_fail: float = 0.2):
self._max = retry_count
self._wait = wait_after_fail
self._count = 0
self._start_time = 0

def run(self, sequence: Callable[[], bool]):
self._start_time = time()
while True:
if sequence() or self._count >= self._max:
break

self._count += 1
sleep(self._wait)

@property
def duration(self):
return time() - self._start_time

@property
def count(self):
return self._count


class RetryException(Exception):
def __init__(self, sequence: Sequence, exception: Exception):
prefix = f"{exception}"
if len(prefix) > 0:
prefix += f" "
super().__init__(f"{prefix}after {sequence.count} retries ({round(sequence.duration, 2)} seconds)")


@contextmanager
def change(
retry_count: int = None,
Expand Down Expand Up @@ -100,4 +67,4 @@ def _run():
sequence.run(_run)

if exception is not None:
raise RetryException(sequence, exception)
raise RetryException(exception, sequence)
11 changes: 6 additions & 5 deletions paf/uielement.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import paf.javascript as script
from paf.assertion import StringAssertion, Format, BinaryAssertion, QuantityAssertion, RectAssertion, ASSERTION
from paf.common import HasParent, Locator, Point, Rect, Property, Formatter, NotFoundException, NotUniqueException, \
WebdriverRetainer
WebdriverRetainer, RetryException, SubjectException
from paf.control import retry
from paf.dom import Attribute
from paf.listener import Listener
Expand Down Expand Up @@ -337,9 +337,10 @@ def _sequence():
try:
retry(_sequence, lambda e: listener.action_failed(action_name, self, e))
listener.action_passed(action_name, self)
except Exception as exception:
except SubjectException as exception:
exception.add_subject(self.name_path)
listener.action_failed_finally(action_name, self, exception)
raise Exception(f"{self.name_path}: {exception}")
raise exception

def click(self):
self._web_element_action_sequence(lambda x: x.click(), "click")
Expand Down Expand Up @@ -479,7 +480,7 @@ def _map():
return assertion_class(
parent=self._ui_element,
actual_supplier=_map,
name_supplier=lambda: f".{property_name} {Format.param(_map_failsafe())} ",
name_supplier=lambda: f".{property_name} {Format.param(_map_failsafe())}",
raise_exception=self._raise,
)

Expand Down Expand Up @@ -544,7 +545,7 @@ def count(self):
return QuantityAssertion[int](
parent=self._ui_element,
actual_supplier=self._ui_element._count_elements,
name_supplier=lambda: f" count {Format.param(self._ui_element._count_elements())} ",
name_supplier=lambda: f" count {Format.param(self._ui_element._count_elements())}",
raise_exception=self._raise,
)

Expand Down
2 changes: 0 additions & 2 deletions test/test_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@

import inject
import pytest
from selenium.webdriver.remote.webelement import WebElement

from paf import javascript
from paf.component import Component
from paf.locator import By
from paf.manager import WebDriverManager
Expand Down
3 changes: 0 additions & 3 deletions test/test_control.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import asyncio
import dataclasses
from time import sleep

import pytest

from paf.common import Property
from paf.control import change, get_config, retry

Expand Down
4 changes: 1 addition & 3 deletions test/test_demo_mode.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import logging

import inject
import pytest
from selenium.webdriver.support.color import Color
Expand Down Expand Up @@ -63,7 +61,7 @@ def test_highlight_not_found_log(finder: FinderPage, listener: Listener, caplog)

assert len(caplog.records) == 1
for record in caplog.records:
assert "Cannot highlight UiElement(By.css selector(#inexistent))[0]: Element not found" in record.message
assert "Cannot highlight UiElement(By.css selector(#inexistent))[0]: Not found" in record.message


def test_highlight_action_success_skips_highlighting(finder: FinderPage, listener: Listener):
Expand Down
Loading

0 comments on commit f2e94d2

Please sign in to comment.