From 5214411dbd9d5276c0840aaca5d3121ee570b799 Mon Sep 17 00:00:00 2001 From: Robert Nemes Date: Fri, 27 Oct 2023 14:38:00 +0100 Subject: [PATCH 01/12] result.fix.match() now supports repeating groups --- .../2691_changed.fix_match_assertion.rst | 1 + .../testing/multitest/entries/assertions.py | 30 +++++++++++++++++-- testplan/testing/multitest/result.py | 4 +-- 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 doc/newsfragments/2691_changed.fix_match_assertion.rst diff --git a/doc/newsfragments/2691_changed.fix_match_assertion.rst b/doc/newsfragments/2691_changed.fix_match_assertion.rst new file mode 100644 index 000000000..fe9737ce1 --- /dev/null +++ b/doc/newsfragments/2691_changed.fix_match_assertion.rst @@ -0,0 +1 @@ +py:meth:`result.fix.match() ` assertion now supports repeating groups when a FixMessage object specified in the ``include_tags`` or ``exclude_tags`` parameters. \ No newline at end of file diff --git a/testplan/testing/multitest/entries/assertions.py b/testplan/testing/multitest/entries/assertions.py index 532d6b66d..7e52bf3e2 100644 --- a/testplan/testing/multitest/entries/assertions.py +++ b/testplan/testing/multitest/entries/assertions.py @@ -17,9 +17,10 @@ import subprocess import sys import tempfile - import lxml +from ms.fix import FixMessage + from testplan.common.utils.convert import make_tuple, flatten_dict_comparison from testplan.common.utils import comparison, difflib from testplan.common.utils.process import subprocess_popen @@ -1368,6 +1369,25 @@ def __init__( Otherwise, if either side is untyped we will compare the values as strings. """ + + def get_taglist_from_msg(msg: FixMessage): + taglist = [] + for tag in list(msg): + taglist.append(tag) + if isinstance(msg[tag], list): + # we hit a repeating group + for fixfragment in msg[tag]: + taglist.extend( + [ + fragment_tag + for fragment_tag in get_taglist_from_msg( + fixfragment + ) + if fragment_tag not in taglist + ] + ) + return taglist + typed_value = getattr(value, "typed_values", False) typed_expected = getattr(expected, "typed_values", False) @@ -1379,8 +1399,12 @@ def __init__( super(FixMatch, self).__init__( value=value, expected=expected, - include_keys=include_tags, - exclude_keys=exclude_tags, + include_keys=get_taglist_from_msg(include_tags) + if isinstance(include_tags, FixMessage) + else include_tags, + exclude_keys=get_taglist_from_msg(exclude_tags) + if isinstance(exclude_tags, FixMessage) + else exclude_tags, report_mode=report_mode, description=description, category=category, diff --git a/testplan/testing/multitest/result.py b/testplan/testing/multitest/result.py index b8e830c47..065c48401 100644 --- a/testplan/testing/multitest/result.py +++ b/testplan/testing/multitest/result.py @@ -1217,9 +1217,9 @@ def match( regex patterns or callables for advanced comparison. :type expected: ``dict`` - :param include_tags: Tags to exclusively consider in the comparison. + :param include_tags: Tags to exclusively consider in the comparison. (Accepts FixMessage object) :type include_tags: ``list`` of ``object`` (items must be hashable) - :param exclude_tags: Keys to ignore in the comparison. + :param exclude_tags: Keys to ignore in the comparison. (Accepts FixMessage object) :type exclude_tags: ``list`` of ``object`` (items must be hashable) :param report_mode: Specify which comparisons should be kept and reported. Default option is to report all From 9929d9baee454975951838481bec60050c0f511d Mon Sep 17 00:00:00 2001 From: Robert Nemes Date: Sun, 29 Oct 2023 23:00:49 +0000 Subject: [PATCH 02/12] removed using of external fix package --- .../2691_changed.fix_match_assertion.rst | 2 +- testplan/testing/multitest/entries/assertions.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/doc/newsfragments/2691_changed.fix_match_assertion.rst b/doc/newsfragments/2691_changed.fix_match_assertion.rst index fe9737ce1..10ce6491f 100644 --- a/doc/newsfragments/2691_changed.fix_match_assertion.rst +++ b/doc/newsfragments/2691_changed.fix_match_assertion.rst @@ -1 +1 @@ -py:meth:`result.fix.match() ` assertion now supports repeating groups when a FixMessage object specified in the ``include_tags`` or ``exclude_tags`` parameters. \ No newline at end of file +py:meth:`result.fix.match() ` assertion now supports repeating groups when a FIX message is specified in the ``include_tags`` or ``exclude_tags`` parameters. \ No newline at end of file diff --git a/testplan/testing/multitest/entries/assertions.py b/testplan/testing/multitest/entries/assertions.py index 7e52bf3e2..1155a3c74 100644 --- a/testplan/testing/multitest/entries/assertions.py +++ b/testplan/testing/multitest/entries/assertions.py @@ -19,8 +19,6 @@ import tempfile import lxml -from ms.fix import FixMessage - from testplan.common.utils.convert import make_tuple, flatten_dict_comparison from testplan.common.utils import comparison, difflib from testplan.common.utils.process import subprocess_popen @@ -1370,7 +1368,13 @@ def __init__( strings. """ - def get_taglist_from_msg(msg: FixMessage): + def get_taglist_from_msg(msg: dict) -> list: + """ + Extracts tags from a FIX message-like object. + + :param msg: FIX message-like object + :return: list of tags + """ taglist = [] for tag in list(msg): taglist.append(tag) @@ -1400,10 +1404,10 @@ def get_taglist_from_msg(msg: FixMessage): value=value, expected=expected, include_keys=get_taglist_from_msg(include_tags) - if isinstance(include_tags, FixMessage) + if isinstance(include_tags, dict) else include_tags, exclude_keys=get_taglist_from_msg(exclude_tags) - if isinstance(exclude_tags, FixMessage) + if isinstance(exclude_tags, dict) else exclude_tags, report_mode=report_mode, description=description, From d5bdb55bdf3d1944870a7903b206fe0c6d5b9740 Mon Sep 17 00:00:00 2001 From: Robert Nemes Date: Mon, 6 Nov 2023 16:18:18 +0000 Subject: [PATCH 03/12] Refactored solution --- .../2691_changed.fix_match_assertion.rst | 2 +- testplan/common/utils/comparison.py | 26 ++++++++++--------- .../testing/multitest/entries/assertions.py | 8 ++---- testplan/testing/multitest/result.py | 3 ++- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/doc/newsfragments/2691_changed.fix_match_assertion.rst b/doc/newsfragments/2691_changed.fix_match_assertion.rst index 10ce6491f..968a93961 100644 --- a/doc/newsfragments/2691_changed.fix_match_assertion.rst +++ b/doc/newsfragments/2691_changed.fix_match_assertion.rst @@ -1 +1 @@ -py:meth:`result.fix.match() ` assertion now supports repeating groups when a FIX message is specified in the ``include_tags`` or ``exclude_tags`` parameters. \ No newline at end of file +py:meth:`result.fix.match() ` assertion now only compares the tags present in the expected message when ``include_tags`` parameter set to `True`. \ No newline at end of file diff --git a/testplan/common/utils/comparison.py b/testplan/common/utils/comparison.py index a207a08b6..3787cbff2 100644 --- a/testplan/common/utils/comparison.py +++ b/testplan/common/utils/comparison.py @@ -532,13 +532,14 @@ def should_ignore_key(key: Hashable) -> bool: ) 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, + only=only[iter_key] if isinstance(only, dict) and iter_key in only + else only, + key=iter_key, + report_mode=report_mode, + value_cmp_func=value_cmp_func, ) # Decide whether to keep or discard the result, depending on the # reporting mode. @@ -671,13 +672,14 @@ def _rec_compare( if lhs_cat == rhs_cat == Category.ITERABLE: results = [] match = Match.IGNORED - for lhs_item, rhs_item in zip_longest(lhs, rhs): + for i, (lhs_item, rhs_item) in enumerate(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, + only=only[i] if isinstance(only, list) and i < len(only) + else only, key=None, report_mode=report_mode, value_cmp_func=value_cmp_func, diff --git a/testplan/testing/multitest/entries/assertions.py b/testplan/testing/multitest/entries/assertions.py index 1155a3c74..a800fe4eb 100644 --- a/testplan/testing/multitest/entries/assertions.py +++ b/testplan/testing/multitest/entries/assertions.py @@ -1403,12 +1403,8 @@ def get_taglist_from_msg(msg: dict) -> list: super(FixMatch, self).__init__( value=value, expected=expected, - include_keys=get_taglist_from_msg(include_tags) - if isinstance(include_tags, dict) - else include_tags, - exclude_keys=get_taglist_from_msg(exclude_tags) - if isinstance(exclude_tags, dict) - else exclude_tags, + include_keys=include_tags, + exclude_keys=exclude_tags, report_mode=report_mode, description=description, category=category, diff --git a/testplan/testing/multitest/result.py b/testplan/testing/multitest/result.py index 065c48401..d763ec710 100644 --- a/testplan/testing/multitest/result.py +++ b/testplan/testing/multitest/result.py @@ -1244,7 +1244,8 @@ def match( expected=expected, description=description, category=category, - include_tags=include_tags, + include_tags=expected if include_tags is True + else include_tags, exclude_tags=exclude_tags, report_mode=report_mode, expected_description=expected_description, From 032abd897ba476b984f1e76cf2b10e0fd9f65afe Mon Sep 17 00:00:00 2001 From: Robert Nemes Date: Mon, 6 Nov 2023 16:26:33 +0000 Subject: [PATCH 04/12] Updated docstring --- testplan/common/utils/comparison.py | 6 +++-- .../testing/multitest/entries/assertions.py | 26 +------------------ testplan/testing/multitest/result.py | 10 +++---- 3 files changed, 10 insertions(+), 32 deletions(-) diff --git a/testplan/common/utils/comparison.py b/testplan/common/utils/comparison.py index 3787cbff2..711b227be 100644 --- a/testplan/common/utils/comparison.py +++ b/testplan/common/utils/comparison.py @@ -535,7 +535,8 @@ def should_ignore_key(key: Hashable) -> bool: lhs=lhs_val, rhs=rhs_val, ignore=ignore, - only=only[iter_key] if isinstance(only, dict) and iter_key in only + only=only[iter_key] + if isinstance(only, dict) and iter_key in only else only, key=iter_key, report_mode=report_mode, @@ -678,7 +679,8 @@ def _rec_compare( lhs=lhs_item, rhs=rhs_item, ignore=ignore, - only=only[i] if isinstance(only, list) and i < len(only) + only=only[i] + if isinstance(only, list) and i < len(only) else only, key=None, report_mode=report_mode, diff --git a/testplan/testing/multitest/entries/assertions.py b/testplan/testing/multitest/entries/assertions.py index a800fe4eb..532d6b66d 100644 --- a/testplan/testing/multitest/entries/assertions.py +++ b/testplan/testing/multitest/entries/assertions.py @@ -17,6 +17,7 @@ import subprocess import sys import tempfile + import lxml from testplan.common.utils.convert import make_tuple, flatten_dict_comparison @@ -1367,31 +1368,6 @@ def __init__( Otherwise, if either side is untyped we will compare the values as strings. """ - - def get_taglist_from_msg(msg: dict) -> list: - """ - Extracts tags from a FIX message-like object. - - :param msg: FIX message-like object - :return: list of tags - """ - taglist = [] - for tag in list(msg): - taglist.append(tag) - if isinstance(msg[tag], list): - # we hit a repeating group - for fixfragment in msg[tag]: - taglist.extend( - [ - fragment_tag - for fragment_tag in get_taglist_from_msg( - fixfragment - ) - if fragment_tag not in taglist - ] - ) - return taglist - typed_value = getattr(value, "typed_values", False) typed_expected = getattr(expected, "typed_values", False) diff --git a/testplan/testing/multitest/result.py b/testplan/testing/multitest/result.py index d763ec710..612c0d77f 100644 --- a/testplan/testing/multitest/result.py +++ b/testplan/testing/multitest/result.py @@ -1217,9 +1217,10 @@ def match( regex patterns or callables for advanced comparison. :type expected: ``dict`` - :param include_tags: Tags to exclusively consider in the comparison. (Accepts FixMessage object) - :type include_tags: ``list`` of ``object`` (items must be hashable) - :param exclude_tags: Keys to ignore in the comparison. (Accepts FixMessage object) + :param include_tags: Tags to exclusively consider in the comparison. + It will automatically use the tags from the expected message when set to True. + :type include_tags: ``list`` of ``object`` (items must be hashable) or ``bool`` + :param exclude_tags: Keys to ignore in the comparison. :type exclude_tags: ``list`` of ``object`` (items must be hashable) :param report_mode: Specify which comparisons should be kept and reported. Default option is to report all @@ -1244,8 +1245,7 @@ def match( expected=expected, description=description, category=category, - include_tags=expected if include_tags is True - else include_tags, + include_tags=expected if include_tags is True else include_tags, exclude_tags=exclude_tags, report_mode=report_mode, expected_description=expected_description, From 0da673f3687d8c24f9f7843e8e5fe3c474ca947a Mon Sep 17 00:00:00 2001 From: Robert Nemes Date: Wed, 8 Nov 2023 16:04:28 +0000 Subject: [PATCH 05/12] Added test for new functionality --- testplan/common/utils/comparison.py | 23 ++-- .../testplan/testing/multitest/test_result.py | 124 ++++++++++++++++++ 2 files changed, 139 insertions(+), 8 deletions(-) diff --git a/testplan/common/utils/comparison.py b/testplan/common/utils/comparison.py index 711b227be..72b9d5481 100644 --- a/testplan/common/utils/comparison.py +++ b/testplan/common/utils/comparison.py @@ -507,7 +507,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 + if isinstance(only, Iterable): + should_ignore = key not in only + else: + should_ignore = not key == only else: should_ignore = False return should_ignore @@ -521,12 +524,14 @@ 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, + only=only[iter_key] + if isinstance(only, dict) and iter_key in only + else only, + key=iter_key, + report_mode=report_mode, value_cmp_func=None, ) ) @@ -680,7 +685,9 @@ def _rec_compare( rhs=rhs_item, ignore=ignore, only=only[i] - if isinstance(only, list) and i < len(only) + if isinstance(only, Iterable) + and i < len(only) + and isinstance(only[i], dict) else only, key=None, report_mode=report_mode, diff --git a/tests/unit/testplan/testing/multitest/test_result.py b/tests/unit/testplan/testing/multitest/test_result.py index 6118120a6..b1eb9e700 100644 --- a/tests/unit/testplan/testing/multitest/test_result.py +++ b/tests/unit/testplan/testing/multitest/test_result.py @@ -730,6 +730,130 @@ def test_flattened_comparison_result(self, fix_ns): and _700[4] == (None, "ABSENT") # key not found in expected data ) + def test_subset_of_tags_with_only_true(self, fix_ns): + """Test the comparison result in flattened entries.""" + + expected = { + 35: "D", + 55: 2, + 555: [ + { + 601: "A", + 683: [ + {689: "a"}, + {689: "b"}, + ], + }, + { + 601: "B", + 683: [ + {688: "c", 689: "c"}, + {688: "d"}, + ], + }, + ], + } + + actual = { + 35: "D", + 22: 5, + 55: 2, + 38: 5, + 555: [ + { + 600: "A", + 601: "A", + 683: [ + {688: "a", 689: "a"}, + {688: "b", 689: "b"}, + ], + }, + { + 600: "B", + 601: "B", + 55: 4, + 683: [ + {688: "c", 689: "c"}, + {688: "d", 689: "d"}, + ], + }, + ], + } + + assert fix_ns.match( + actual=actual, + expected=expected, + description="complex fix message comparison", + include_tags=True, + ) + assert len(fix_ns.result.entries) == 1 + + # Comparison result is a list of list items in below format: + # [indent, key, result, (act_type, act_value), (exp_type, exp_value)] + + comp_result = fix_ns.result.entries[0].comparison + + _35 = [item for item in comp_result if item[1] == 35][0] + assert _35[0] == 0 and _35[2][0].lower() == comparison.Match.PASS + _22 = [item for item in comp_result if item[1] == 22][0] + assert ( + _22[0] == 0 + and _22[2][0].lower() == comparison.Match.IGNORED + and _22[3][1] == "5" + and _22[4][1] == "ABSENT" + ) + _55 = [item for item in comp_result if item[1] == 55][0] + assert _55[0] == 0 and _55[2][0].lower() == comparison.Match.PASS + _38 = [item for item in comp_result if item[1] == 38][0] + assert ( + _38[0] == 0 + and _38[2][0].lower() == comparison.Match.IGNORED + and _38[3][1] == "5" + and _38[4][1] == "ABSENT" + ) + _555 = [item for item in comp_result if item[1] == 555][0] + assert _555[0] == 0 and _555[2][0].lower() == comparison.Match.PASS + _600 = [item for item in comp_result if item[1] == 600][0] + assert ( + _600[0] == 1 + and _600[2][0].lower() == comparison.Match.IGNORED + and _600[3][1] == "A" + and _600[4][1] == "ABSENT" + ) + _601 = [item for item in comp_result if item[1] == 601][0] + assert _601[0] == 1 and _601[2][0].lower() == comparison.Match.PASS + _683 = [item for item in comp_result if item[1] == 683][0] + assert _683[0] == 1 and _683[2][0].lower() == comparison.Match.PASS + _688_1, _688_2, _688_3, _688_4 = [ + item for item in comp_result if item[1] == 688 + ] + assert ( + _688_1[0] == 2 + and _688_1[2][0].lower() == comparison.Match.IGNORED + and _688_1[3][1] == "a" + and _688_1[4][1] == "ABSENT" + ) + assert ( + _688_2[0] == 2 + and _688_2[2][0].lower() == comparison.Match.IGNORED + and _688_2[3][1] == "b" + and _688_2[4][1] == "ABSENT" + ) + assert _688_3[0] == 2 and _688_3[2][0].lower() == comparison.Match.PASS + assert _688_4[0] == 2 and _688_4[2][0].lower() == comparison.Match.PASS + _689_1, _689_2, _689_3, _689_4 = [ + item for item in comp_result if item[1] == 689 + ] + assert _689_1[0] == 2 and _689_1[2][0].lower() == comparison.Match.PASS + assert _689_2[0] == 2 and _689_2[2][0].lower() == comparison.Match.PASS + assert _689_3[0] == 2 and _689_3[2][0].lower() == comparison.Match.PASS + assert ( + _689_4[0] == 2 + and _689_4[2][0].lower() == comparison.Match.IGNORED + and _689_4[3][1] == "d" + and _689_4[4][1] == "ABSENT" + ) + class TestResultBaseNamespace: """Test assertions and other methods in the base result.* namespace.""" From 2c50091416de458611028c5d296bb314f7eeebeb Mon Sep 17 00:00:00 2001 From: Robert Nemes Date: Wed, 8 Nov 2023 16:12:20 +0000 Subject: [PATCH 06/12] Added test for new functionality --- tests/unit/testplan/testing/multitest/test_result.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/unit/testplan/testing/multitest/test_result.py b/tests/unit/testplan/testing/multitest/test_result.py index b1eb9e700..6dbee2a37 100644 --- a/tests/unit/testplan/testing/multitest/test_result.py +++ b/tests/unit/testplan/testing/multitest/test_result.py @@ -802,8 +802,14 @@ def test_subset_of_tags_with_only_true(self, fix_ns): and _22[3][1] == "5" and _22[4][1] == "ABSENT" ) - _55 = [item for item in comp_result if item[1] == 55][0] - assert _55[0] == 0 and _55[2][0].lower() == comparison.Match.PASS + _55_1, _55_2 = [item for item in comp_result if item[1] == 55] + assert _55_1[0] == 0 and _55_1[2][0].lower() == comparison.Match.PASS + assert ( + _55_2[0] == 1 + and _55_2[2][0].lower() == comparison.Match.IGNORED + and _55_2[3][1] == "4" + and _55_2[4][1] == "ABSENT" + ) _38 = [item for item in comp_result if item[1] == 38][0] assert ( _38[0] == 0 From 930804f644527e63065c48af86f9612f68f4c6eb Mon Sep 17 00:00:00 2001 From: Robert Nemes Date: Wed, 8 Nov 2023 17:34:53 +0000 Subject: [PATCH 07/12] Updated test docstring --- tests/unit/testplan/testing/multitest/test_result.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/testplan/testing/multitest/test_result.py b/tests/unit/testplan/testing/multitest/test_result.py index 6dbee2a37..ffce50780 100644 --- a/tests/unit/testplan/testing/multitest/test_result.py +++ b/tests/unit/testplan/testing/multitest/test_result.py @@ -730,8 +730,11 @@ def test_flattened_comparison_result(self, fix_ns): and _700[4] == (None, "ABSENT") # key not found in expected data ) - def test_subset_of_tags_with_only_true(self, fix_ns): - """Test the comparison result in flattened entries.""" + def test_subset_of_tags_with_include_tags_true(self, fix_ns): + """ + Test the comparison result when the expected FIX message with repeating groups is the subset of the actual + while include_tags set to True. + """ expected = { 35: "D", From 8530693c1626ace354e78bf99cc6f1634a72a27b Mon Sep 17 00:00:00 2001 From: rnemes Date: Sat, 11 Nov 2023 17:10:15 +0100 Subject: [PATCH 08/12] Refactored solution by adding a new parameters --- testplan/common/utils/comparison.py | 23 ++---- .../testing/multitest/entries/assertions.py | 6 +- testplan/testing/multitest/result.py | 72 ++++++++----------- .../testplan/testing/multitest/test_result.py | 2 +- 4 files changed, 42 insertions(+), 61 deletions(-) diff --git a/testplan/common/utils/comparison.py b/testplan/common/utils/comparison.py index 72b9d5481..b2026b4c8 100644 --- a/testplan/common/utils/comparison.py +++ b/testplan/common/utils/comparison.py @@ -506,11 +506,10 @@ def should_ignore_key(key: Hashable) -> bool: """ if key in ignore: should_ignore = True + elif only is True: + should_ignore = key not in rhs.keys() elif only is not None: - if isinstance(only, Iterable): - should_ignore = key not in only - else: - should_ignore = not key == only + should_ignore = key not in only else: should_ignore = False return should_ignore @@ -527,9 +526,7 @@ def should_ignore_key(key: Hashable) -> bool: lhs=lhs_val, rhs=rhs_val, ignore=ignore, - only=only[iter_key] - if isinstance(only, dict) and iter_key in only - else only, + only=only, key=iter_key, report_mode=report_mode, value_cmp_func=None, @@ -540,9 +537,7 @@ def should_ignore_key(key: Hashable) -> bool: lhs=lhs_val, rhs=rhs_val, ignore=ignore, - only=only[iter_key] - if isinstance(only, dict) and iter_key in only - else only, + only=only, key=iter_key, report_mode=report_mode, value_cmp_func=value_cmp_func, @@ -678,17 +673,13 @@ def _rec_compare( if lhs_cat == rhs_cat == Category.ITERABLE: results = [] match = Match.IGNORED - for i, (lhs_item, rhs_item) in enumerate(zip_longest(lhs, rhs)): + for lhs_item, rhs_item in zip_longest(lhs, rhs): # iterate all elems in both iterable non-mapping objects result = _rec_compare( lhs=lhs_item, rhs=rhs_item, ignore=ignore, - only=only[i] - if isinstance(only, Iterable) - and i < len(only) - and isinstance(only[i], dict) - else only, + only=only, key=None, report_mode=report_mode, value_cmp_func=value_cmp_func, diff --git a/testplan/testing/multitest/entries/assertions.py b/testplan/testing/multitest/entries/assertions.py index 532d6b66d..ed9275a5a 100644 --- a/testplan/testing/multitest/entries/assertions.py +++ b/testplan/testing/multitest/entries/assertions.py @@ -1308,6 +1308,7 @@ def __init__( self, value, expected, + use_keys_of_expected=False, include_keys=None, exclude_keys=None, report_mode=comparison.ReportOptions.ALL, @@ -1319,6 +1320,7 @@ def __init__( ): self.value = value self.expected = expected + self.use_keys_of_expected = use_keys_of_expected self.include_keys = include_keys self.exclude_keys = exclude_keys self.actual_description = actual_description @@ -1337,7 +1339,7 @@ def evaluate(self): lhs=self.value, rhs=self.expected, ignore=self.exclude_keys, - only=self.include_keys, + only=True if self.use_keys_of_expected else self.include_keys, report_mode=self._report_mode, value_cmp_func=self._value_cmp_func, ) @@ -1355,6 +1357,7 @@ def __init__( self, value, expected, + use_tags_of_expected=False, include_tags=None, exclude_tags=None, report_mode=comparison.ReportOptions.ALL, @@ -1379,6 +1382,7 @@ def __init__( super(FixMatch, self).__init__( value=value, expected=expected, + use_keys_of_expected=use_tags_of_expected, include_keys=include_tags, exclude_keys=exclude_tags, report_mode=report_mode, diff --git a/testplan/testing/multitest/result.py b/testplan/testing/multitest/result.py index 612c0d77f..e70cc1478 100644 --- a/testplan/testing/multitest/result.py +++ b/testplan/testing/multitest/result.py @@ -12,7 +12,7 @@ import re import threading from functools import wraps -from typing import Callable, Optional +from typing import Callable, Optional, Dict, Hashable, List, Any import functools from testplan import defaults @@ -938,17 +938,20 @@ def check( @assertion def match( self, - actual, - expected, - description=None, - category=None, - include_keys=None, - exclude_keys=None, + actual: Dict, + expected: Dict, + use_keys_of_expected: bool = False, + description: str = None, + category: str = None, + include_keys: List[Hashable] = None, + exclude_keys: List[Hashable] = None, report_mode=comparison.ReportOptions.ALL, - actual_description=None, - expected_description=None, - value_cmp_func=comparison.COMPARE_FUNCTIONS["native_equality"], - ): + actual_description: str = None, + expected_description: str = None, + value_cmp_func: Callable[ + [Any, Any], bool + ] = comparison.COMPARE_FUNCTIONS["native_equality"], + ) -> bool: r""" Matches two dictionaries, supports nested data. Custom comparators can be used as values on the ``expected`` dict. @@ -985,40 +988,31 @@ def match( ) :param actual: Original dictionary. - :type actual: ``dict``. :param expected: Comparison dictionary, can contain custom comparators (e.g. regex, lambda functions) - :type expected: ``dict`` + :param use_keys_of_expected: Use the keys present in the expected message. :param include_keys: Keys to exclusively consider in the comparison. - :type include_keys: ``list`` of ``object`` (items must be hashable) :param exclude_keys: Keys to ignore in the comparison. - :type exclude_keys: ``list`` of ``object`` (items must be hashable) :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: ``testplan.common.utils.comparison.ReportOptions`` :param actual_description: Column header description for original dict. - :type actual_description: ``str`` :param expected_description: Column header description for expected dict. - :type expected_description: ``str`` :param description: Text description for the assertion. - :type description: ``str`` :param category: Custom category that will be used for summarization. - :type category: ``str`` :param value_cmp_func: Function to use to compare values in expected and actual dicts. Defaults to using `operator.eq()`. - :type value_cmp_func: ``Callable[[Any, Any], bool]`` :return: Assertion pass status - :rtype: ``bool`` """ entry = assertions.DictMatch( value=actual, expected=expected, description=description, + use_keys_of_expected=use_keys_of_expected, include_keys=include_keys, exclude_keys=exclude_keys, report_mode=report_mode, @@ -1178,16 +1172,17 @@ def check( @assertion def match( self, - actual, - expected, - description=None, - category=None, - include_tags=None, - exclude_tags=None, + actual: Dict, + expected: Dict, + use_tags_of_expected: bool = False, + description: str = None, + category: str = None, + include_tags: List[Hashable] = None, + exclude_tags: List[Hashable] = None, report_mode=comparison.ReportOptions.ALL, - actual_description=None, - expected_description=None, - ): + actual_description: str = None, + expected_description: str = None, + ) -> bool: """ Matches two FIX messages, supports repeating groups (nested data). Custom comparators can be used as values on the ``expected`` msg. @@ -1212,40 +1207,31 @@ def match( ) :param actual: Original FIX message. - :type actual: ``dict`` :param expected: Expected FIX message, can include compiled regex patterns or callables for advanced comparison. - :type expected: ``dict`` + :param use_tags_of_expected: Use the tags present in the expected message. :param include_tags: Tags to exclusively consider in the comparison. - It will automatically use the tags from the expected message when set to True. - :type include_tags: ``list`` of ``object`` (items must be hashable) or ``bool`` :param exclude_tags: Keys to ignore in the comparison. - :type exclude_tags: ``list`` of ``object`` (items must be hashable) :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: ``testplan.common.utils.comparison.ReportOptions`` :param actual_description: Column header description for original msg. - :type actual_description: ``str`` :param expected_description: Column header description for expected msg. - :type expected_description: ``str`` :param description: Text description for the assertion. - :type description: ``str`` :param category: Custom category that will be used for summarization. - :type category: ``str`` :return: Assertion pass status - :rtype: ``bool`` """ entry = assertions.FixMatch( value=actual, expected=expected, description=description, category=category, - include_tags=expected if include_tags is True else include_tags, + use_tags_of_expected=use_tags_of_expected, + include_tags=include_tags, exclude_tags=exclude_tags, report_mode=report_mode, expected_description=expected_description, diff --git a/tests/unit/testplan/testing/multitest/test_result.py b/tests/unit/testplan/testing/multitest/test_result.py index ffce50780..f02f7fd26 100644 --- a/tests/unit/testplan/testing/multitest/test_result.py +++ b/tests/unit/testplan/testing/multitest/test_result.py @@ -787,7 +787,7 @@ def test_subset_of_tags_with_include_tags_true(self, fix_ns): actual=actual, expected=expected, description="complex fix message comparison", - include_tags=True, + use_tags_of_expected=True, ) assert len(fix_ns.result.entries) == 1 From 6d0275917abd0cf6850e30faf812120e99326484 Mon Sep 17 00:00:00 2001 From: rnemes Date: Tue, 14 Nov 2023 14:32:44 +0100 Subject: [PATCH 09/12] Introduced new parameter include_only_tags_of_expected --- .../2691_changed.fix_match_assertion.rst | 2 +- testplan/common/utils/comparison.py | 185 ++++++++++-------- .../testing/multitest/entries/assertions.py | 47 ++--- testplan/testing/multitest/result.py | 16 +- .../testplan/testing/multitest/test_result.py | 2 +- 5 files changed, 132 insertions(+), 120 deletions(-) diff --git a/doc/newsfragments/2691_changed.fix_match_assertion.rst b/doc/newsfragments/2691_changed.fix_match_assertion.rst index 968a93961..83c178dc8 100644 --- a/doc/newsfragments/2691_changed.fix_match_assertion.rst +++ b/doc/newsfragments/2691_changed.fix_match_assertion.rst @@ -1 +1 @@ -py:meth:`result.fix.match() ` assertion now only compares the tags present in the expected message when ``include_tags`` parameter set to `True`. \ No newline at end of file +Introduced a new parameter ``include_only_tags_of_expected`` for py:meth:`result.fix.match() ` assertion, It will only compares the tags present in the expected message when the parameter is set to `True`. \ No newline at end of file diff --git a/testplan/common/utils/comparison.py b/testplan/common/utils/comparison.py index b2026b4c8..a38d6f4fa 100644 --- a/testplan/common/utils/comparison.py +++ b/testplan/common/utils/comparison.py @@ -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 @@ -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 @@ -187,7 +188,6 @@ def __str__(self): class MetaCallable(Callable): - delimiter = None def __init__(self, *callables): @@ -209,7 +209,6 @@ def __str__(self): class Or(MetaCallable): - delimiter = "or" def __call__(self, value): @@ -220,7 +219,6 @@ def __call__(self, value): class And(MetaCallable): - delimiter = "and" def __call__(self, value): @@ -248,8 +246,8 @@ def __call__(self, value): def __eq__(self, other): return ( - self.__class__ == other.__class__ - and self.callable_obj == other.callable_obj + self.__class__ == other.__class__ + and self.callable_obj == other.callable_obj ) @@ -478,12 +476,13 @@ def _partition(results): def _cmp_dicts( - lhs: Dict, - rhs: Dict, - ignore: Container, - only: Container, - report_mode: int, - value_cmp_func: Union[Callable, None], + lhs: Dict, + rhs: Dict, + ignore: Container, + include: Container, + report_mode: int, + value_cmp_func: Union[Callable, None], + include_only_keys_of_rhs: bool = False, ) -> Tuple[str, List]: """ Compares two dictionaries with optional restriction to keys, @@ -491,7 +490,7 @@ def _cmp_dicts( :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 :return: pair of match result and comparison result @@ -506,10 +505,10 @@ def should_ignore_key(key: Hashable) -> bool: """ if key in ignore: should_ignore = True - elif only is True: + elif include_only_keys_of_rhs is True: should_ignore = key not in rhs.keys() - elif only is not None: - should_ignore = key not in only + elif include is not None: + should_ignore = key not in include else: should_ignore = False return should_ignore @@ -526,10 +525,11 @@ def should_ignore_key(key: Hashable) -> bool: lhs=lhs_val, rhs=rhs_val, ignore=ignore, - only=only, + include=include, key=iter_key, report_mode=report_mode, value_cmp_func=None, + include_only_keys_of_rhs=include_only_keys_of_rhs, ) ) else: @@ -537,10 +537,11 @@ def should_ignore_key(key: Hashable) -> bool: lhs=lhs_val, rhs=rhs_val, ignore=ignore, - only=only, + include=include, key=iter_key, report_mode=report_mode, value_cmp_func=value_cmp_func, + include_only_keys_of_rhs=include_only_keys_of_rhs, ) # Decide whether to keep or discard the result, depending on the # reporting mode. @@ -558,14 +559,15 @@ def should_ignore_key(key: Hashable) -> bool: def _rec_compare( - lhs, - rhs, - ignore, - only, - key, - report_mode, - value_cmp_func, - _regex_adapter=RegexAdapter, + lhs, + rhs, + ignore, + include, + key, + report_mode, + value_cmp_func, + _regex_adapter=RegexAdapter, + include_only_keys_of_rhs=False, ): """ Recursive deep comparison implementation @@ -579,9 +581,9 @@ def _rec_compare( ## NO VALS if ( - ((lhs_cat == Category.ABSENT) or (rhs_cat == Category.ABSENT)) - and (lhs_cat != Category.CALLABLE) - and (rhs_cat != Category.CALLABLE) + ((lhs_cat == Category.ABSENT) or (rhs_cat == Category.ABSENT)) + and (lhs_cat != Category.CALLABLE) + and (rhs_cat != Category.CALLABLE) ): return _build_res( key=key, @@ -679,10 +681,11 @@ def _rec_compare( lhs=lhs_item, rhs=rhs_item, ignore=ignore, - only=only, + include=include, key=None, report_mode=report_mode, value_cmp_func=value_cmp_func, + include_only_keys_of_rhs=include_only_keys_of_rhs, ) match = Match.combine(match, result[1]) @@ -698,7 +701,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_keys_of_rhs=include_only_keys_of_rhs, ) lhs_vals, rhs_vals = _partition(results) return _build_res( @@ -723,10 +732,9 @@ def untyped_fixtag(x, y): if not ret: if any( - isinstance(val, float) or isinstance(val, decimal.Decimal) - for val in (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_) @@ -773,39 +781,36 @@ class ReportOptions(enum.Enum): def compare( - lhs, - rhs, - ignore=None, - only=None, - report_mode=ReportOptions.ALL, - value_cmp_func=COMPARE_FUNCTIONS["native_equality"], -): + lhs: Dict, + rhs: Dict, + ignore: List[Hashable] = None, + include: List[Hashable] = None, + report_mode=ReportOptions.ALL, + value_cmp_func: typing_Callable[[Any, Any], bool] = COMPARE_FUNCTIONS[ + "native_equality" + ], + include_only_keys_of_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_keys_of_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): @@ -836,16 +841,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_keys_of_rhs=include_only_keys_of_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) @@ -940,21 +951,21 @@ def is_missed_message(comparisons): """ absent_side = (0, None, Absent.descr) return ( - sum( - [ - (0 if entry[2] == absent_side else 1) - for entry in comparisons - ] - ) - == 0 + sum( + [ + (0 if entry[2] == absent_side else 1) + for entry in comparisons + ] + ) + == 0 ) or ( - sum( - [ - (0 if entry[3] == absent_side else 1) - for entry in comparisons - ] - ) - == 0 + sum( + [ + (0 if entry[3] == absent_side else 1) + for entry in comparisons + ] + ) + == 0 ) pass_flag, comparisons = cmpr_tuple @@ -971,7 +982,7 @@ def is_missed_message(comparisons): comparison_match = comparison[1] # tag exists and matches, or ignored if (comparison_match == Match.PASS) or ( - comparison_match == Match.IGNORED + comparison_match == Match.IGNORED ): match_err = 0 else: # tag exists, but wrong data or tag is missing @@ -990,28 +1001,28 @@ 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( - match_name, - values, - comparisons, - description=None, - tag_weightings=None, - value_cmp_func=COMPARE_FUNCTIONS["native_equality"], + match_name, + values, + comparisons, + description=None, + tag_weightings=None, + value_cmp_func=COMPARE_FUNCTIONS["native_equality"], ): """ Matches a list of expected values against a list of expected comparisons. @@ -1107,7 +1118,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 @@ -1339,12 +1350,12 @@ def __bool__(self): # python 3 bool() def dictmatch_all_compat( - match_name, - comparisons, - values, - description, - key_weightings, - value_cmp_func=COMPARE_FUNCTIONS["native_equality"], + match_name, + comparisons, + values, + description, + key_weightings, + value_cmp_func=COMPARE_FUNCTIONS["native_equality"], ): """This is being used for internal compatibility.""" matches = unordered_compare( diff --git a/testplan/testing/multitest/entries/assertions.py b/testplan/testing/multitest/entries/assertions.py index ed9275a5a..0685977d6 100644 --- a/testplan/testing/multitest/entries/assertions.py +++ b/testplan/testing/multitest/entries/assertions.py @@ -9,6 +9,7 @@ import cmath import collections import decimal +import lxml import numbers import operator import os @@ -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 @@ -1306,21 +1306,21 @@ class DictMatch(Assertion): def __init__( self, - value, - expected, - use_keys_of_expected=False, - include_keys=None, - exclude_keys=None, + value: Dict, + expected: Dict, + include_only_keys_of_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.use_keys_of_expected = use_keys_of_expected + self.include_only_keys_of_expected = include_only_keys_of_expected self.include_keys = include_keys self.exclude_keys = exclude_keys self.actual_description = actual_description @@ -1339,9 +1339,10 @@ def evaluate(self): lhs=self.value, rhs=self.expected, ignore=self.exclude_keys, - only=True if self.use_keys_of_expected else self.include_keys, + include=self.include_keys, report_mode=self._report_mode, value_cmp_func=self._value_cmp_func, + include_only_keys_of_rhs=self.include_only_keys_of_expected, ) self.comparison = flatten_dict_comparison(cmp_result) return passed @@ -1355,16 +1356,16 @@ class FixMatch(DictMatch): def __init__( self, - value, - expected, - use_tags_of_expected=False, - include_tags=None, - exclude_tags=None, + value: Dict, + expected: Dict, + include_only_tags_of_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. @@ -1382,7 +1383,7 @@ def __init__( super(FixMatch, self).__init__( value=value, expected=expected, - use_keys_of_expected=use_tags_of_expected, + include_only_keys_of_expected=include_only_tags_of_expected, include_keys=include_tags, exclude_keys=exclude_tags, report_mode=report_mode, diff --git a/testplan/testing/multitest/result.py b/testplan/testing/multitest/result.py index e70cc1478..8601653c1 100644 --- a/testplan/testing/multitest/result.py +++ b/testplan/testing/multitest/result.py @@ -940,7 +940,7 @@ def match( self, actual: Dict, expected: Dict, - use_keys_of_expected: bool = False, + include_only_keys_of_expected: bool = False, description: str = None, category: str = None, include_keys: List[Hashable] = None, @@ -951,7 +951,7 @@ def match( value_cmp_func: Callable[ [Any, Any], bool ] = comparison.COMPARE_FUNCTIONS["native_equality"], - ) -> bool: + ) -> assertions.DictMatch: r""" Matches two dictionaries, supports nested data. Custom comparators can be used as values on the ``expected`` dict. @@ -990,7 +990,7 @@ def match( :param actual: Original dictionary. :param expected: Comparison dictionary, can contain custom comparators (e.g. regex, lambda functions) - :param use_keys_of_expected: Use the keys present in the expected message. + :param include_only_keys_of_expected: Use the keys present in the expected message. :param include_keys: Keys to exclusively consider in the comparison. :param exclude_keys: Keys to ignore in the comparison. :param report_mode: Specify which comparisons should be kept and @@ -1012,7 +1012,7 @@ def match( value=actual, expected=expected, description=description, - use_keys_of_expected=use_keys_of_expected, + include_only_keys_of_expected=include_only_keys_of_expected, include_keys=include_keys, exclude_keys=exclude_keys, report_mode=report_mode, @@ -1174,7 +1174,7 @@ def match( self, actual: Dict, expected: Dict, - use_tags_of_expected: bool = False, + include_only_tags_of_expected: bool = False, description: str = None, category: str = None, include_tags: List[Hashable] = None, @@ -1182,7 +1182,7 @@ def match( report_mode=comparison.ReportOptions.ALL, actual_description: str = None, expected_description: str = None, - ) -> bool: + ) -> assertions.FixMatch: """ Matches two FIX messages, supports repeating groups (nested data). Custom comparators can be used as values on the ``expected`` msg. @@ -1210,7 +1210,7 @@ def match( :param expected: Expected FIX message, can include compiled regex patterns or callables for advanced comparison. - :param use_tags_of_expected: Use the tags present in the expected message. + :param include_only_tags_of_expected: Use the tags present in the expected message. :param include_tags: Tags to exclusively consider in the comparison. :param exclude_tags: Keys to ignore in the comparison. :param report_mode: Specify which comparisons should be kept and @@ -1230,7 +1230,7 @@ def match( expected=expected, description=description, category=category, - use_tags_of_expected=use_tags_of_expected, + include_only_tags_of_expected=include_only_tags_of_expected, include_tags=include_tags, exclude_tags=exclude_tags, report_mode=report_mode, diff --git a/tests/unit/testplan/testing/multitest/test_result.py b/tests/unit/testplan/testing/multitest/test_result.py index f02f7fd26..a1d57bcb8 100644 --- a/tests/unit/testplan/testing/multitest/test_result.py +++ b/tests/unit/testplan/testing/multitest/test_result.py @@ -787,7 +787,7 @@ def test_subset_of_tags_with_include_tags_true(self, fix_ns): actual=actual, expected=expected, description="complex fix message comparison", - use_tags_of_expected=True, + include_only_tags_of_expected=True, ) assert len(fix_ns.result.entries) == 1 From 7df834b18351e0f859f570c757696579b8476294 Mon Sep 17 00:00:00 2001 From: Robert Nemes Date: Tue, 14 Nov 2023 13:51:37 +0000 Subject: [PATCH 10/12] Ran black formatter --- testplan/common/utils/comparison.py | 118 ++++++++++++++-------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/testplan/common/utils/comparison.py b/testplan/common/utils/comparison.py index a38d6f4fa..43d2e4a35 100644 --- a/testplan/common/utils/comparison.py +++ b/testplan/common/utils/comparison.py @@ -246,8 +246,8 @@ def __call__(self, value): def __eq__(self, other): return ( - self.__class__ == other.__class__ - and self.callable_obj == other.callable_obj + self.__class__ == other.__class__ + and self.callable_obj == other.callable_obj ) @@ -476,13 +476,13 @@ def _partition(results): def _cmp_dicts( - lhs: Dict, - rhs: Dict, - ignore: Container, - include: Container, - report_mode: int, - value_cmp_func: Union[Callable, None], - include_only_keys_of_rhs: bool = False, + lhs: Dict, + rhs: Dict, + ignore: Container, + include: Container, + report_mode: int, + value_cmp_func: Union[Callable, None], + include_only_keys_of_rhs: bool = False, ) -> Tuple[str, List]: """ Compares two dictionaries with optional restriction to keys, @@ -559,15 +559,15 @@ def should_ignore_key(key: Hashable) -> bool: def _rec_compare( - lhs, - rhs, - ignore, - include, - key, - report_mode, - value_cmp_func, - _regex_adapter=RegexAdapter, - include_only_keys_of_rhs=False, + lhs, + rhs, + ignore, + include, + key, + report_mode, + value_cmp_func, + _regex_adapter=RegexAdapter, + include_only_keys_of_rhs=False, ): """ Recursive deep comparison implementation @@ -581,9 +581,9 @@ def _rec_compare( ## NO VALS if ( - ((lhs_cat == Category.ABSENT) or (rhs_cat == Category.ABSENT)) - and (lhs_cat != Category.CALLABLE) - and (rhs_cat != Category.CALLABLE) + ((lhs_cat == Category.ABSENT) or (rhs_cat == Category.ABSENT)) + and (lhs_cat != Category.CALLABLE) + and (rhs_cat != Category.CALLABLE) ): return _build_res( key=key, @@ -732,8 +732,8 @@ def untyped_fixtag(x, y): if not ret: if any( - isinstance(val, float) or isinstance(val, decimal.Decimal) - for val in (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 @@ -781,15 +781,15 @@ class ReportOptions(enum.Enum): def compare( - lhs: Dict, - rhs: Dict, - ignore: List[Hashable] = None, - include: List[Hashable] = None, - report_mode=ReportOptions.ALL, - value_cmp_func: typing_Callable[[Any, Any], bool] = COMPARE_FUNCTIONS[ - "native_equality" - ], - include_only_keys_of_rhs: bool = False, + lhs: Dict, + rhs: Dict, + ignore: List[Hashable] = None, + include: List[Hashable] = None, + report_mode=ReportOptions.ALL, + value_cmp_func: typing_Callable[[Any, Any], bool] = COMPARE_FUNCTIONS[ + "native_equality" + ], + include_only_keys_of_rhs: bool = False, ) -> Tuple[bool, List[Tuple]]: """ Compare two iterable key, value objects (e.g. dict or dict-like mapping) @@ -951,21 +951,21 @@ def is_missed_message(comparisons): """ absent_side = (0, None, Absent.descr) return ( - sum( - [ - (0 if entry[2] == absent_side else 1) - for entry in comparisons - ] - ) - == 0 + sum( + [ + (0 if entry[2] == absent_side else 1) + for entry in comparisons + ] + ) + == 0 ) or ( - sum( - [ - (0 if entry[3] == absent_side else 1) - for entry in comparisons - ] - ) - == 0 + sum( + [ + (0 if entry[3] == absent_side else 1) + for entry in comparisons + ] + ) + == 0 ) pass_flag, comparisons = cmpr_tuple @@ -982,7 +982,7 @@ def is_missed_message(comparisons): comparison_match = comparison[1] # tag exists and matches, or ignored if (comparison_match == Match.PASS) or ( - comparison_match == Match.IGNORED + comparison_match == Match.IGNORED ): match_err = 0 else: # tag exists, but wrong data or tag is missing @@ -1017,12 +1017,12 @@ def __init__(self, value, ignore=None, include=None): def unordered_compare( - match_name, - values, - comparisons, - description=None, - tag_weightings=None, - value_cmp_func=COMPARE_FUNCTIONS["native_equality"], + match_name, + values, + comparisons, + description=None, + tag_weightings=None, + value_cmp_func=COMPARE_FUNCTIONS["native_equality"], ): """ Matches a list of expected values against a list of expected comparisons. @@ -1350,12 +1350,12 @@ def __bool__(self): # python 3 bool() def dictmatch_all_compat( - match_name, - comparisons, - values, - description, - key_weightings, - value_cmp_func=COMPARE_FUNCTIONS["native_equality"], + match_name, + comparisons, + values, + description, + key_weightings, + value_cmp_func=COMPARE_FUNCTIONS["native_equality"], ): """This is being used for internal compatibility.""" matches = unordered_compare( From 8133af9d9a4842a21c33f626145d043b0f731c65 Mon Sep 17 00:00:00 2001 From: Robert Nemes Date: Wed, 15 Nov 2023 14:16:42 +0000 Subject: [PATCH 11/12] Changed parameter names as requested --- .../2691_changed.fix_match_assertion.rst | 2 +- testplan/common/utils/comparison.py | 20 +++++++++---------- .../testing/multitest/entries/assertions.py | 10 +++++----- testplan/testing/multitest/result.py | 12 +++++------ .../testplan/testing/multitest/test_result.py | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/doc/newsfragments/2691_changed.fix_match_assertion.rst b/doc/newsfragments/2691_changed.fix_match_assertion.rst index 83c178dc8..f291b2738 100644 --- a/doc/newsfragments/2691_changed.fix_match_assertion.rst +++ b/doc/newsfragments/2691_changed.fix_match_assertion.rst @@ -1 +1 @@ -Introduced a new parameter ``include_only_tags_of_expected`` for py:meth:`result.fix.match() ` assertion, It will only compares the tags present in the expected message when the parameter is set to `True`. \ No newline at end of file +Introduced a new parameter ``include_only_expected`` for py:meth:`result.fix.match() ` assertion, It will only compares the tags present in the expected message when the parameter is set to `True`. \ No newline at end of file diff --git a/testplan/common/utils/comparison.py b/testplan/common/utils/comparison.py index 43d2e4a35..19121e7a4 100644 --- a/testplan/common/utils/comparison.py +++ b/testplan/common/utils/comparison.py @@ -482,7 +482,7 @@ def _cmp_dicts( include: Container, report_mode: int, value_cmp_func: Union[Callable, None], - include_only_keys_of_rhs: bool = False, + include_only_rhs: bool = False, ) -> Tuple[str, List]: """ Compares two dictionaries with optional restriction to keys, @@ -505,7 +505,7 @@ def should_ignore_key(key: Hashable) -> bool: """ if key in ignore: should_ignore = True - elif include_only_keys_of_rhs is True: + elif include_only_rhs is True: should_ignore = key not in rhs.keys() elif include is not None: should_ignore = key not in include @@ -529,7 +529,7 @@ def should_ignore_key(key: Hashable) -> bool: key=iter_key, report_mode=report_mode, value_cmp_func=None, - include_only_keys_of_rhs=include_only_keys_of_rhs, + include_only_rhs=include_only_rhs, ) ) else: @@ -541,7 +541,7 @@ def should_ignore_key(key: Hashable) -> bool: key=iter_key, report_mode=report_mode, value_cmp_func=value_cmp_func, - include_only_keys_of_rhs=include_only_keys_of_rhs, + include_only_rhs=include_only_rhs, ) # Decide whether to keep or discard the result, depending on the # reporting mode. @@ -567,7 +567,7 @@ def _rec_compare( report_mode, value_cmp_func, _regex_adapter=RegexAdapter, - include_only_keys_of_rhs=False, + include_only_rhs=False, ): """ Recursive deep comparison implementation @@ -685,7 +685,7 @@ def _rec_compare( key=None, report_mode=report_mode, value_cmp_func=value_cmp_func, - include_only_keys_of_rhs=include_only_keys_of_rhs, + include_only_rhs=include_only_rhs, ) match = Match.combine(match, result[1]) @@ -707,7 +707,7 @@ def _rec_compare( include=include, report_mode=report_mode, value_cmp_func=value_cmp_func, - include_only_keys_of_rhs=include_only_keys_of_rhs, + include_only_rhs=include_only_rhs, ) lhs_vals, rhs_vals = _partition(results) return _build_res( @@ -789,7 +789,7 @@ def compare( value_cmp_func: typing_Callable[[Any, Any], bool] = COMPARE_FUNCTIONS[ "native_equality" ], - include_only_keys_of_rhs: bool = False, + include_only_rhs: bool = False, ) -> Tuple[bool, List[Tuple]]: """ Compare two iterable key, value objects (e.g. dict or dict-like mapping) @@ -807,7 +807,7 @@ def compare( for more detail. :param value_cmp_func: function to compare values in a dict. Defaults to COMPARE_FUNCTIONS['native_equality']. - :param include_only_keys_of_rhs: use the keys present in rhs. + :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 @@ -847,7 +847,7 @@ def compare( include=include, report_mode=report_mode, value_cmp_func=value_cmp_func, - include_only_keys_of_rhs=include_only_keys_of_rhs, + include_only_rhs=include_only_rhs, ) # For the keys in include not matching anything, diff --git a/testplan/testing/multitest/entries/assertions.py b/testplan/testing/multitest/entries/assertions.py index 0685977d6..11d9c9f9d 100644 --- a/testplan/testing/multitest/entries/assertions.py +++ b/testplan/testing/multitest/entries/assertions.py @@ -1308,7 +1308,7 @@ def __init__( self, value: Dict, expected: Dict, - include_only_keys_of_expected: bool = False, + include_only_expected: bool = False, include_keys: List[Hashable] = None, exclude_keys: List[Hashable] = None, report_mode=comparison.ReportOptions.ALL, @@ -1320,7 +1320,7 @@ def __init__( ): self.value = value self.expected = expected - self.include_only_keys_of_expected = include_only_keys_of_expected + self.include_only_expected = include_only_expected self.include_keys = include_keys self.exclude_keys = exclude_keys self.actual_description = actual_description @@ -1342,7 +1342,7 @@ def evaluate(self): include=self.include_keys, report_mode=self._report_mode, value_cmp_func=self._value_cmp_func, - include_only_keys_of_rhs=self.include_only_keys_of_expected, + include_only_rhs=self.include_only_expected, ) self.comparison = flatten_dict_comparison(cmp_result) return passed @@ -1358,7 +1358,7 @@ def __init__( self, value: Dict, expected: Dict, - include_only_tags_of_expected: bool = False, + include_only_expected: bool = False, include_tags: List[Hashable] = None, exclude_tags: List[Hashable] = None, report_mode=comparison.ReportOptions.ALL, @@ -1383,7 +1383,7 @@ def __init__( super(FixMatch, self).__init__( value=value, expected=expected, - include_only_keys_of_expected=include_only_tags_of_expected, + include_only_expected=include_only_expected, include_keys=include_tags, exclude_keys=exclude_tags, report_mode=report_mode, diff --git a/testplan/testing/multitest/result.py b/testplan/testing/multitest/result.py index 8601653c1..0eb524c21 100644 --- a/testplan/testing/multitest/result.py +++ b/testplan/testing/multitest/result.py @@ -940,7 +940,7 @@ def match( self, actual: Dict, expected: Dict, - include_only_keys_of_expected: bool = False, + include_only_expected: bool = False, description: str = None, category: str = None, include_keys: List[Hashable] = None, @@ -990,7 +990,7 @@ def match( :param actual: Original dictionary. :param expected: Comparison dictionary, can contain custom comparators (e.g. regex, lambda functions) - :param include_only_keys_of_expected: Use the keys present in the expected message. + :param include_only_expected: Use the keys present in the expected message. :param include_keys: Keys to exclusively consider in the comparison. :param exclude_keys: Keys to ignore in the comparison. :param report_mode: Specify which comparisons should be kept and @@ -1012,7 +1012,7 @@ def match( value=actual, expected=expected, description=description, - include_only_keys_of_expected=include_only_keys_of_expected, + include_only_expected=include_only_expected, include_keys=include_keys, exclude_keys=exclude_keys, report_mode=report_mode, @@ -1174,7 +1174,7 @@ def match( self, actual: Dict, expected: Dict, - include_only_tags_of_expected: bool = False, + include_only_expected: bool = False, description: str = None, category: str = None, include_tags: List[Hashable] = None, @@ -1210,7 +1210,7 @@ def match( :param expected: Expected FIX message, can include compiled regex patterns or callables for advanced comparison. - :param include_only_tags_of_expected: Use the tags present in the expected message. + :param include_only_expected: Use the tags present in the expected message. :param include_tags: Tags to exclusively consider in the comparison. :param exclude_tags: Keys to ignore in the comparison. :param report_mode: Specify which comparisons should be kept and @@ -1230,7 +1230,7 @@ def match( expected=expected, description=description, category=category, - include_only_tags_of_expected=include_only_tags_of_expected, + include_only_expected=include_only_expected, include_tags=include_tags, exclude_tags=exclude_tags, report_mode=report_mode, diff --git a/tests/unit/testplan/testing/multitest/test_result.py b/tests/unit/testplan/testing/multitest/test_result.py index a1d57bcb8..03313c1c5 100644 --- a/tests/unit/testplan/testing/multitest/test_result.py +++ b/tests/unit/testplan/testing/multitest/test_result.py @@ -787,7 +787,7 @@ def test_subset_of_tags_with_include_tags_true(self, fix_ns): actual=actual, expected=expected, description="complex fix message comparison", - include_only_tags_of_expected=True, + include_only_expected=True, ) assert len(fix_ns.result.entries) == 1 From 3a6d6d6ae0cae7acfbce2cb0e4faab04c20ed5ac Mon Sep 17 00:00:00 2001 From: Robert Nemes Date: Wed, 15 Nov 2023 15:01:13 +0000 Subject: [PATCH 12/12] Updated docstring and newsfragment --- doc/newsfragments/2691_changed.fix_match_assertion.rst | 2 +- testplan/common/utils/comparison.py | 1 + testplan/testing/multitest/result.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/newsfragments/2691_changed.fix_match_assertion.rst b/doc/newsfragments/2691_changed.fix_match_assertion.rst index f291b2738..bb403764b 100644 --- a/doc/newsfragments/2691_changed.fix_match_assertion.rst +++ b/doc/newsfragments/2691_changed.fix_match_assertion.rst @@ -1 +1 @@ -Introduced a new parameter ``include_only_expected`` for py:meth:`result.fix.match() ` assertion, It will only compares the tags present in the expected message when the parameter is set to `True`. \ No newline at end of file +Introduced a new parameter ``include_only_expected`` for py:meth:`result.fix.match() ` assertion. It will only compare the tags present in the expected message when the parameter is set to `True`. \ No newline at end of file diff --git a/testplan/common/utils/comparison.py b/testplan/common/utils/comparison.py index 19121e7a4..6989866b3 100644 --- a/testplan/common/utils/comparison.py +++ b/testplan/common/utils/comparison.py @@ -493,6 +493,7 @@ def _cmp_dicts( :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 """ diff --git a/testplan/testing/multitest/result.py b/testplan/testing/multitest/result.py index 0eb524c21..d7f7b5b70 100644 --- a/testplan/testing/multitest/result.py +++ b/testplan/testing/multitest/result.py @@ -990,7 +990,7 @@ def match( :param actual: Original dictionary. :param expected: Comparison dictionary, can contain custom comparators (e.g. regex, lambda functions) - :param include_only_expected: Use the keys present in the expected message. + :param include_only_expected: Use the keys present in the expected dictionary. :param include_keys: Keys to exclusively consider in the comparison. :param exclude_keys: Keys to ignore in the comparison. :param report_mode: Specify which comparisons should be kept and