Working on the class that will output the elements in case of error

This commit is contained in:
2025-10-25 17:38:28 +02:00
parent 847684a15b
commit 80215913b6
3 changed files with 308 additions and 54 deletions

View File

@@ -1,44 +1,123 @@
import re
from dataclasses import dataclass
from fastcore.basics import NotStr
class Predicate:
def __init__(self, value):
self.value = value
def validate(self, actual):
raise NotImplementedError
def __str__(self):
return f"{self.__class__.__name__}({self.value})"
class StartsWith(Predicate):
def __init__(self, value):
self.value = value
super().__init__(value)
def validate(self, actual):
return actual.startswith(self.value)
def __str__(self):
return f"StartsWith({self.value})"
class Contains(Predicate):
def __init__(self, value):
self.value = value
super().__init__(value)
def validate(self, actual):
return self.value in actual
def __str__(self):
return f"Contains({self.value})"
class DoesNotContain(Predicate):
def __init__(self, value):
self.value = value
super().__init__(value)
def validate(self, actual):
return self.value not in actual
@dataclass
class DoNotCheck:
desc: str = None
@dataclass
class Empty:
desc: str = None
class ErrorOutput:
def __init__(self, path, element, expected):
self.path = path
self.element = element
self.expected = expected
self.output = []
self.indent = ""
@staticmethod
def _unconstruct_path_item(item):
if "#" in item:
elt_name, elt_id = item.split("#")
return elt_name, "id", elt_id
elif match := re.match(r'(\w+)\[(class|name)=([^]]+)]', item):
return match.groups()
return item, None, None
def __str__(self):
return f"DoesNotContain({self.value})"
self.compute()
def compute(self):
# first render the path hierarchy
for p in self.path.split(".")[:-1]:
elt_name, attr_name, attr_value = self._unconstruct_path_item(p)
path_str = f'({elt_name} "{attr_name}"="{attr_value}"' if attr_name else f"({elt_name}"
self._add_to_output(f"{path_str} ...")
self.indent += " "
# then render the element
if hasattr(self.expected, "tag") and hasattr(self.element, "tag"):
# render the attributes
elt_attrs = {attr_name: self.element.attrs.get(attr_name, "** MISSING **") for attr_name in self.expected.attrs}
elt_attrs_str = " ".join(f'"{attr_name}"="{attr_value}"' for attr_name, attr_value in elt_attrs.items())
tag_str = f"({self.element.tag} {elt_attrs_str}"
if len(self.expected.children) == 0: tag_str += ")"
self._add_to_output(tag_str)
# display the error in the attributes
attrs_in_error = [attr_name for attr_name, attr_value in elt_attrs.items() if
attr_value != self.expected.attrs[attr_name]]
if attrs_in_error:
elt_attrs_error = " ".join(len(f'"{name}"="{value}"') * ("^" if name in attrs_in_error else " ")
for name, value in elt_attrs.items())
error_str = f" {len(self.element.tag) * " "} {elt_attrs_error}"
self._add_to_output(error_str)
# render the children
if len(self.expected.children) > 0:
self.indent += " "
element_index = 0
for child in self.expected.children:
if hasattr(child, "tag"):
child_attr_str = " ".join(f'"{attr_name}"="{attr_value}"' for attr_name, attr_value in child.attrs.items())
sub_children_indicator = " ..." if len(child.children) > 0 else ""
child_str = f"({child.tag} {child_attr_str}{sub_children_indicator})"
self._add_to_output(child_str)
else:
self._add_to_output(child)
self.indent = self.indent[:-2]
self._add_to_output(")")
def _add_to_output(self, msg):
self.output.append(f"{self.indent}{msg}")
def matches(actual, expected, path=""):
def print_path(p):
return f"Path '{p}':\n\t" if p else ""
return f"Path: '{p}'\n\t" if p else ""
def _type(x):
return type(x)
@@ -59,19 +138,38 @@ def matches(actual, expected, path=""):
else:
debug_info = _debug_compare(_actual, _expected)
return f"{print_path(path)}\n{msg} : {debug_info}"
return f"{print_path(path)}{msg} : {debug_info}"
def _assert_error(msg, _actual=None, _expected=None):
assert False, _error_msg(msg, _actual=_actual, _expected=_expected)
def _get_current_path(elt):
if hasattr(elt, "tag"):
res = f"{elt.tag}"
if "id" in elt.attrs:
res += f"#{elt.attrs['id']}"
elif "name" in elt.attrs:
res += f"[name={elt.attrs['name']}]"
elif "class" in elt.attrs:
res += f"[class={elt.attrs['class']}]"
return res
else:
return elt.__class__.__name__
if actual is not None and expected is None:
_assert_error("Actual is not None while expected is None", _actual=actual)
if isinstance(expected, DoNotCheck):
return True
if actual is None and expected is not None:
_assert_error("Actual is None while expected is ", _expected=expected)
# set the path
path += "." + _get_current_path(actual) if path else _get_current_path(actual)
assert _type(actual) == _type(expected) or (hasattr(actual, "tag") and hasattr(expected, "tag")), \
_assert_error("The types are different: ", _actual=actual, _expected=expected)
_error_msg("The types are different: ", _actual=actual, _expected=expected)
if isinstance(expected, (list, tuple)):
if len(actual) < len(expected):
@@ -82,11 +180,25 @@ def matches(actual, expected, path=""):
for actual_child, expected_child in zip(actual, expected):
assert matches(actual_child, expected_child, path=path)
elif isinstance(expected, NotStr):
to_compare = actual.s.lstrip('\n').lstrip()
assert to_compare.startswith(expected.s), _error_msg("Notstr values are different: ",
_actual=to_compare,
_expected=expected.s)
elif hasattr(expected, "tag"):
# validate the tags names
assert actual.tag == expected.tag, _error_msg("The elements are different: ",
_actual=actual.tag,
_expected=expected.tag)
# special case when the expected element is empty
if len(expected.children) > 0 and isinstance(expected.children[0], Empty):
assert len(actual.children) == 0, _error_msg("Actual is not empty:", _actual=actual)
assert len(actual.attrs) == 0, _error_msg("Actual is not empty:", _actual=actual)
return True
# compare the attributes
for expected_attr, expected_value in expected.attrs.items():
assert expected_attr in actual.attrs, _error_msg(f"'{expected_attr}' is not found in Actual:",
_actual=actual.attrs)
@@ -102,5 +214,17 @@ def matches(actual, expected, path=""):
_error_msg(f"The values are different for '{expected_attr}': ",
_actual=actual.attrs[expected_attr],
_expected=expected.attrs[expected_attr])
# compare the children
if len(actual.children) < len(expected.children):
_assert_error("Actual is lesser than expected: ", _actual=actual, _expected=expected)
for actual_child, expected_child in zip(actual.children, expected.children):
assert matches(actual_child, expected_child, path=path)
else:
assert actual == expected, _error_msg("The values are different: ",
_actual=actual,
_expected=expected)
return True