Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

result.fix.match() assertion will use the tags present in the expected message when include_only_expected parameter set to True #1009

Merged
merged 13 commits into from
Nov 17, 2023
1 change: 1 addition & 0 deletions doc/newsfragments/2691_changed.fix_match_assertion.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Introduced a new parameter ``include_only_expected`` for py:meth:`result.fix.match() <testplan.testing.multitest.result.FixNamespace.match>` assertion. It will only compare the tags present in the expected message when the parameter is set to `True`.
120 changes: 67 additions & 53 deletions testplan/common/utils/comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import traceback
from collections.abc import Mapping, Iterable, Container
from itertools import zip_longest
from typing import List, Tuple, Dict, Hashable, Union
from typing import Any, List, Tuple, Dict, Hashable, Union
from typing import Callable as typing_Callable

from .reporting import Absent, fmt, NATIVE_TYPES, callable_name

Expand Down Expand Up @@ -63,7 +64,7 @@ def check_dict_keys(data, has_keys=None, absent_keys=None):
return existing_diff, absent_intersection


class Callable:
class Callable: # TODO: This should not be called Callable - needs refactoring
"""
Some of our assertions can make use of callables that accept a
single argument as comparator values. We also provide the helper
Expand Down Expand Up @@ -187,7 +188,6 @@ def __str__(self):


class MetaCallable(Callable):

delimiter = None

def __init__(self, *callables):
Expand All @@ -209,7 +209,6 @@ def __str__(self):


class Or(MetaCallable):

delimiter = "or"

def __call__(self, value):
Expand All @@ -220,7 +219,6 @@ def __call__(self, value):


class And(MetaCallable):

delimiter = "and"

def __call__(self, value):
Expand Down Expand Up @@ -481,19 +479,21 @@ def _cmp_dicts(
lhs: Dict,
rhs: Dict,
ignore: Container,
only: Container,
include: Container,
report_mode: int,
value_cmp_func: Union[Callable, None],
include_only_rhs: bool = False,
) -> Tuple[str, List]:
"""
Compares two dictionaries with optional restriction to keys,

:param lhs: dictionary to compare
:param rhs: dictionary to compare
:param ignore: collection of keys to ignore during comparison
:param only: collection of keys to restrict comparison to
:param include: collection of keys to restrict comparison to
:param report_mode: report option code
:param value_cmp_func: value comparison function
:param include_only_rhs: use the keys present in rhs.
:return: pair of match result and comparison result
"""

Expand All @@ -506,8 +506,10 @@ def should_ignore_key(key: Hashable) -> bool:
"""
if key in ignore:
should_ignore = True
elif only is not None:
should_ignore = key not in only
elif include_only_rhs is True:
should_ignore = key not in rhs.keys()
elif include is not None:
should_ignore = key not in include
else:
should_ignore = False
return should_ignore
Expand All @@ -521,24 +523,26 @@ def should_ignore_key(key: Hashable) -> bool:
# enforce ignorance of match
results.append(
_rec_compare(
lhs_val,
rhs_val,
ignore,
only,
iter_key,
report_mode,
lhs=lhs_val,
rhs=rhs_val,
ignore=ignore,
include=include,
key=iter_key,
report_mode=report_mode,
value_cmp_func=None,
include_only_rhs=include_only_rhs,
)
)
else:
result = _rec_compare(
lhs_val,
rhs_val,
ignore,
only,
iter_key,
report_mode,
value_cmp_func,
lhs=lhs_val,
rhs=rhs_val,
ignore=ignore,
include=include,
key=iter_key,
report_mode=report_mode,
value_cmp_func=value_cmp_func,
include_only_rhs=include_only_rhs,
)
# Decide whether to keep or discard the result, depending on the
# reporting mode.
Expand All @@ -559,11 +563,12 @@ def _rec_compare(
lhs,
rhs,
ignore,
only,
include,
key,
report_mode,
value_cmp_func,
_regex_adapter=RegexAdapter,
include_only_rhs=False,
):
"""
Recursive deep comparison implementation
Expand Down Expand Up @@ -674,13 +679,14 @@ def _rec_compare(
for lhs_item, rhs_item in zip_longest(lhs, rhs):
# iterate all elems in both iterable non-mapping objects
result = _rec_compare(
lhs_item,
rhs_item,
ignore,
only,
lhs=lhs_item,
rhs=rhs_item,
ignore=ignore,
include=include,
key=None,
report_mode=report_mode,
value_cmp_func=value_cmp_func,
include_only_rhs=include_only_rhs,
)

match = Match.combine(match, result[1])
Expand All @@ -696,7 +702,13 @@ def _rec_compare(
## DICTS
if lhs_cat == rhs_cat == Category.DICT:
match, results = _cmp_dicts(
lhs, rhs, ignore, only, report_mode, value_cmp_func
lhs=lhs,
rhs=rhs,
ignore=ignore,
include=include,
report_mode=report_mode,
value_cmp_func=value_cmp_func,
include_only_rhs=include_only_rhs,
)
lhs_vals, rhs_vals = _partition(results)
return _build_res(
Expand Down Expand Up @@ -724,7 +736,6 @@ def untyped_fixtag(x, y):
isinstance(val, float) or isinstance(val, decimal.Decimal)
for val in (x, y)
):

x_, y_ = (
val.rstrip("0").rstrip(".") if "." in val else val
for val in (x_, y_)
Expand Down Expand Up @@ -771,39 +782,36 @@ class ReportOptions(enum.Enum):


def compare(
lhs,
rhs,
ignore=None,
only=None,
lhs: Dict,
rhs: Dict,
ignore: List[Hashable] = None,
include: List[Hashable] = None,
report_mode=ReportOptions.ALL,
value_cmp_func=COMPARE_FUNCTIONS["native_equality"],
):
value_cmp_func: typing_Callable[[Any, Any], bool] = COMPARE_FUNCTIONS[
"native_equality"
],
include_only_rhs: bool = False,
) -> Tuple[bool, List[Tuple]]:
"""
Compare two iterable key, value objects (e.g. dict or dict-like mapping)
and return a status and a detailed comparison table, useful for reporting.

Ignore has precedence over only.
Ignore has precedence over include.

:param lhs: object compared against rhs
:type lhs: ``dict`` interface (``__contains__`` and ``.items()``)
:param rhs: object compared against lhs
:type rhs: ``dict`` interface (``__contains__`` and ``.items()``)
:param ignore: list of keys to ignore in the comparison
:type ignore: ``list``
:param only: list of keys to exclusively consider in the comparison
:type only: ``list``
:param include: list of keys to exclusively consider in the comparison
:param report_mode: Specify which comparisons should be kept and reported.
Default option is to report all comparisons but this
can be restricted if desired. See ReportOptions enum
for more detail.
:type report_mode: ``ReportOptions``
:param value_cmp_func: function to compare values in a dict. Defaults
to COMPARE_FUNCTIONS['native_equality'].
:type value_cmp_func: Callable[[Any, Any], bool]
:param include_only_rhs: use the keys present in rhs.

:return: Tuple of comparison bool ``(passed: True, failed: False)`` and
a description object for the testdb report
:rtype: ``tuple`` of (``bool``, ``list`` of ``tuple``)
"""

if (lhs is None) and (rhs is None):
Expand Down Expand Up @@ -834,16 +842,22 @@ def compare(
ignore = ignore or []

match, comparisons = _cmp_dicts(
lhs, rhs, ignore, only, report_mode, value_cmp_func
lhs=lhs,
rhs=rhs,
ignore=ignore,
include=include,
report_mode=report_mode,
value_cmp_func=value_cmp_func,
include_only_rhs=include_only_rhs,
)

# For the keys in only not matching anything,
# For the keys in include not matching anything,
# we report them as absent in expected and value.
if isinstance(only, list) and only and comparisons is not None:
if isinstance(include, list) and include and comparisons is not None:
keys_found = set()
for elem in comparisons:
keys_found.add(elem[0])
for key in only:
for key in include:
if key not in keys_found:
comparisons.append(
(key, Match.IGNORED, Absent.descr, Absent.descr)
Expand Down Expand Up @@ -988,19 +1002,19 @@ class Expected:
Input to the "unordered_compare" function.
"""

def __init__(self, value, ignore=None, only=None):
def __init__(self, value, ignore=None, include=None):
"""
:param value: object compared against
each actual value in unordered_compare
:type value: ``dict``-like interface (__contains__ and .items())
:param ignore: list of keys to ignore in the comparison
:type ignore: ``list``
:param only: list of keys to exclusively consider in the comparison
:type only: ``list``
:param include: list of keys to exclusively consider in the comparison
:type include: ``list``
"""
self.value = value
self.ignore = ignore
self.only = only
self.include = include


def unordered_compare(
Expand Down Expand Up @@ -1105,7 +1119,7 @@ def unordered_compare(
cmpr.value,
msg,
ignore=cmpr.ignore,
only=cmpr.only,
include=cmpr.include,
value_cmp_func=value_cmp_func,
)
for cmpr in proc_cmps
Expand Down
43 changes: 24 additions & 19 deletions testplan/testing/multitest/entries/assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import cmath
import collections
import decimal
import lxml
import numbers
import operator
import os
Expand All @@ -17,8 +18,7 @@
import subprocess
import sys
import tempfile

import lxml
from typing import Dict, Hashable, List

from testplan.common.utils.convert import make_tuple, flatten_dict_comparison
from testplan.common.utils import comparison, difflib
Expand Down Expand Up @@ -1306,19 +1306,21 @@ class DictMatch(Assertion):

def __init__(
self,
value,
expected,
include_keys=None,
exclude_keys=None,
value: Dict,
expected: Dict,
include_only_expected: bool = False,
include_keys: List[Hashable] = None,
exclude_keys: List[Hashable] = None,
report_mode=comparison.ReportOptions.ALL,
description=None,
category=None,
actual_description=None,
expected_description=None,
description: str = None,
category: str = None,
actual_description: str = None,
expected_description: str = None,
value_cmp_func=comparison.COMPARE_FUNCTIONS["native_equality"],
):
self.value = value
self.expected = expected
self.include_only_expected = include_only_expected
self.include_keys = include_keys
self.exclude_keys = exclude_keys
self.actual_description = actual_description
Expand All @@ -1337,9 +1339,10 @@ def evaluate(self):
lhs=self.value,
rhs=self.expected,
ignore=self.exclude_keys,
only=self.include_keys,
include=self.include_keys,
report_mode=self._report_mode,
value_cmp_func=self._value_cmp_func,
include_only_rhs=self.include_only_expected,
)
self.comparison = flatten_dict_comparison(cmp_result)
return passed
Expand All @@ -1353,15 +1356,16 @@ class FixMatch(DictMatch):

def __init__(
self,
value,
expected,
include_tags=None,
exclude_tags=None,
value: Dict,
expected: Dict,
include_only_expected: bool = False,
include_tags: List[Hashable] = None,
exclude_tags: List[Hashable] = None,
report_mode=comparison.ReportOptions.ALL,
description=None,
category=None,
actual_description=None,
expected_description=None,
description: str = None,
category: str = None,
actual_description: str = None,
expected_description: str = None,
):
"""
If both FIX messages are typed, we enable strict type checking.
Expand All @@ -1379,6 +1383,7 @@ def __init__(
super(FixMatch, self).__init__(
value=value,
expected=expected,
include_only_expected=include_only_expected,
include_keys=include_tags,
exclude_keys=exclude_tags,
report_mode=report_mode,
Expand Down
Loading
Loading