Implemented matches()
This commit is contained in:
@@ -3,6 +3,8 @@ from dataclasses import dataclass
|
||||
|
||||
from fastcore.basics import NotStr
|
||||
|
||||
from myfasthtml.core.testclient import MyFT
|
||||
|
||||
|
||||
class Predicate:
|
||||
def __init__(self, value):
|
||||
@@ -13,6 +15,14 @@ class Predicate:
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.__class__.__name__}({self.value})"
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Predicate):
|
||||
return False
|
||||
return self.value == other.value
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.value)
|
||||
|
||||
|
||||
class StartsWith(Predicate):
|
||||
@@ -73,51 +83,152 @@ class ErrorOutput:
|
||||
# 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} ...")
|
||||
path_str = self._str_element(MyFT(elt_name, {attr_name: attr_value}), keep_open=True)
|
||||
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 += ")"
|
||||
# display the tag and its attributes
|
||||
tag_str = self._str_element(self.element, self.expected)
|
||||
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}"
|
||||
# Try to show where the differences are
|
||||
error_str = self._detect_error(self.element, self.expected)
|
||||
if error_str:
|
||||
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)
|
||||
for expected_child in self.expected.children:
|
||||
if hasattr(expected_child, "tag"):
|
||||
if element_index < len(self.element.children):
|
||||
# display the child
|
||||
element_child = self.element.children[element_index]
|
||||
child_str = self._str_element(element_child, expected_child, keep_open=False)
|
||||
self._add_to_output(child_str)
|
||||
|
||||
# manage errors in children
|
||||
child_error_str = self._detect_error(element_child, expected_child)
|
||||
if child_error_str:
|
||||
self._add_to_output(child_error_str)
|
||||
element_index += 1
|
||||
|
||||
else:
|
||||
# When there are fewer children than expected, we display a placeholder
|
||||
child_str = "! ** MISSING ** !"
|
||||
self._add_to_output(child_str)
|
||||
|
||||
else:
|
||||
self._add_to_output(child)
|
||||
self._add_to_output(expected_child)
|
||||
|
||||
self.indent = self.indent[:-2]
|
||||
self._add_to_output(")")
|
||||
else:
|
||||
self._add_to_output(str(self.element))
|
||||
# Try to show where the differences are
|
||||
error_str = self._detect_error(self.element, self.expected)
|
||||
if error_str:
|
||||
self._add_to_output(error_str)
|
||||
|
||||
def _add_to_output(self, msg):
|
||||
self.output.append(f"{self.indent}{msg}")
|
||||
|
||||
@staticmethod
|
||||
def _str_element(element, expected=None, keep_open=None):
|
||||
# compare to itself if no expected element is provided
|
||||
if expected is None:
|
||||
expected = element
|
||||
|
||||
# the attributes are compared to the expected element
|
||||
elt_attrs = {attr_name: element.attrs.get(attr_name, "** MISSING **") for attr_name in
|
||||
[attr_name for attr_name in expected.attrs if attr_name is not None]}
|
||||
elt_attrs_str = " ".join(f'"{attr_name}"="{attr_value}"' for attr_name, attr_value in elt_attrs.items())
|
||||
|
||||
#
|
||||
tag_str = f"({element.tag} {elt_attrs_str}"
|
||||
|
||||
# manage the closing tag
|
||||
if keep_open is False:
|
||||
tag_str += " ...)" if len(element.children) > 0 else ")"
|
||||
elif keep_open is True:
|
||||
tag_str += "..." if elt_attrs_str == "" else " ..."
|
||||
else:
|
||||
# close the tag if there are no children
|
||||
if len(element.children) == 0: tag_str += ")"
|
||||
|
||||
return tag_str
|
||||
|
||||
def _detect_error(self, element, expected):
|
||||
if hasattr(expected, "tag") and hasattr(element, "tag"):
|
||||
tag_str = len(element.tag) * (" " if element.tag == expected.tag else "^")
|
||||
elt_attrs = {attr_name: element.attrs.get(attr_name, "** MISSING **") for attr_name in expected.attrs}
|
||||
attrs_in_error = [attr_name for attr_name, attr_value in elt_attrs.items() if
|
||||
not self._matches(attr_value, 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" {tag_str} {elt_attrs_error}"
|
||||
return error_str
|
||||
else:
|
||||
if not self._matches(element, expected):
|
||||
return len(str(element)) * "^"
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _matches(element, expected):
|
||||
if element == expected:
|
||||
return True
|
||||
elif isinstance(expected, Predicate):
|
||||
return expected.validate(element)
|
||||
else:
|
||||
return element == expected
|
||||
|
||||
|
||||
class ErrorComparisonOutput:
|
||||
def __init__(self, actual_error_output, expected_error_output):
|
||||
self.actual_error_output = actual_error_output
|
||||
self.expected_error_output = expected_error_output
|
||||
|
||||
@staticmethod
|
||||
def adjust(to_adjust, reference):
|
||||
for index, ref_line in enumerate(reference):
|
||||
if "^^" in ref_line:
|
||||
# insert an empty line in to_adjust
|
||||
to_adjust.insert(index, "")
|
||||
|
||||
return to_adjust
|
||||
|
||||
def render(self):
|
||||
# init if needed
|
||||
if not self.actual_error_output.output:
|
||||
self.actual_error_output.compute()
|
||||
if not self.expected_error_output.output:
|
||||
self.expected_error_output.compute()
|
||||
|
||||
actual = self.actual_error_output.output
|
||||
expected = self.expected_error_output.output
|
||||
|
||||
# actual = self.adjust(actual, expected) # does not seem to be needed
|
||||
expected = self.adjust(expected, actual)
|
||||
|
||||
actual_max_length = len(max(actual, key=len))
|
||||
# expected_max_length = len(max(expected, key=len))
|
||||
|
||||
output = []
|
||||
for a, e in zip(actual, expected):
|
||||
line = f"{a:<{actual_max_length}} | {e}".rstrip()
|
||||
output.append(line)
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
def matches(actual, expected, path=""):
|
||||
def print_path(p):
|
||||
return f"Path: '{p}'\n\t" if p else ""
|
||||
return f"Path : '{p}'\n" if p else ""
|
||||
|
||||
def _type(x):
|
||||
return type(x)
|
||||
@@ -126,7 +237,11 @@ def matches(actual, expected, path=""):
|
||||
return str(elt) if elt else "None"
|
||||
|
||||
def _debug_compare(a, b):
|
||||
return f"actual={_debug(a)}, expected={_debug(b)}"
|
||||
actual_out = ErrorOutput(path, a, b)
|
||||
expected_out = ErrorOutput(path, b, b)
|
||||
|
||||
comparison_out = ErrorComparisonOutput(actual_out, expected_out)
|
||||
return comparison_out.render()
|
||||
|
||||
def _error_msg(msg, _actual=None, _expected=None):
|
||||
if _actual is None and _expected is None:
|
||||
@@ -138,7 +253,7 @@ def matches(actual, expected, path=""):
|
||||
else:
|
||||
debug_info = _debug_compare(_actual, _expected)
|
||||
|
||||
return f"{print_path(path)}{msg} : {debug_info}"
|
||||
return f"{print_path(path)}Error : {msg}\n{debug_info}"
|
||||
|
||||
def _assert_error(msg, _actual=None, _expected=None):
|
||||
assert False, _error_msg(msg, _actual=_actual, _expected=_expected)
|
||||
@@ -188,7 +303,7 @@ def matches(actual, expected, path=""):
|
||||
|
||||
elif hasattr(expected, "tag"):
|
||||
# validate the tags names
|
||||
assert actual.tag == expected.tag, _error_msg("The elements are different: ",
|
||||
assert actual.tag == expected.tag, _error_msg("The elements are different.",
|
||||
_actual=actual.tag,
|
||||
_expected=expected.tag)
|
||||
|
||||
@@ -200,14 +315,14 @@ def matches(actual, expected, path=""):
|
||||
|
||||
# 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:",
|
||||
assert expected_attr in actual.attrs, _error_msg(f"'{expected_attr}' is not found in Actual.",
|
||||
_actual=actual.attrs)
|
||||
|
||||
if isinstance(expected_value, Predicate):
|
||||
assert expected_value.validate(actual.attrs[expected_attr]), \
|
||||
_error_msg(f"The condition '{expected_value}' is not satisfied: ",
|
||||
_actual=actual.attrs[expected_attr],
|
||||
_expected=expected_value)
|
||||
_error_msg(f"The condition '{expected_value}' is not satisfied.",
|
||||
_actual=actual,
|
||||
_expected=expected)
|
||||
|
||||
else:
|
||||
assert actual.attrs[expected_attr] == expected.attrs[expected_attr], \
|
||||
|
||||
Reference in New Issue
Block a user