From a880b30d2f20a15a1f33883fc366ae9733499980 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 16 Dec 2021 10:01:59 +0100 Subject: [PATCH 01/80] draft regex base function --- netcompare/check_type.py | 19 +- netcompare/evaluator.py | 12 + tests/mock/regex_match/pre.json | 468 ++++++++++++++++++++++++++++++++ tests/test_type_check.py | 26 ++ 4 files changed, 523 insertions(+), 2 deletions(-) create mode 100644 tests/mock/regex_match/pre.json diff --git a/netcompare/check_type.py b/netcompare/check_type.py index 6e101d1..9ef4751 100644 --- a/netcompare/check_type.py +++ b/netcompare/check_type.py @@ -1,6 +1,6 @@ """CheckType Implementation.""" from typing import Mapping, Tuple, Union, List -from .evaluator import diff_generator, parameter_evaluator +from .evaluator import diff_generator, parameter_evaluator, regex_evaluator from .runner import extract_values_from_output @@ -20,7 +20,8 @@ def init(*args): return ToleranceType(*args) if check_type == "parameter_match": return ParameterMatchType(*args) - + if check_type == "regex": + return RegexType(*args) raise NotImplementedError @staticmethod @@ -88,10 +89,24 @@ def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple parameter = value_to_compare[1] except IndexError as error: raise f"Evaluating parameter must be defined as dict at index 1. You have: {value_to_compare}" from error + assert isinstance(parameter, dict), "check_option must be of type dict()" diff = parameter_evaluator(reference_value, parameter) return diff, not diff +class RegexType(CheckType): + """Regex Match class implementation.""" + + def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple[Mapping, bool]: + """Parameter Match evaluator implementation.""" + try: + parameter = value_to_compare[1] + except IndexError as error: + raise f"Evaluating parameter must be defined as dict at index 1. You have: {value_to_compare}" from error + assert (isinstance(parameter, dict), isinstance(parameter['regex'], str)), "check_option must be of type dict() as in example: {'regex': '\d.'}" + diff = regex_evaluator(reference_value, parameter) + return diff, not diff + # TODO: compare is no longer the entry point, we should use the libary as: # netcompare_check = CheckType.init(check_type_info, options) # pre_result = netcompare_check.get_value(pre_obj, path) diff --git a/netcompare/evaluator.py b/netcompare/evaluator.py index af7ce6b..d175cf5 100644 --- a/netcompare/evaluator.py +++ b/netcompare/evaluator.py @@ -108,3 +108,15 @@ def parameter_evaluator(values: Mapping, parameter: Mapping) -> Mapping: result[inner_key] = temp_dict return result + + +def regex_evaluator(values: Mapping, parameter: Mapping) -> Mapping: + """Regex Match evaluator engine.""" + # value: [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE'}] + # parameter: {'regex': '.*UNDERLAY.*'} + result = {} + if not isinstance(values, list): + raise TypeError("Something went wrong during JMSPath parsing. values must be of type list.") + + + return result \ No newline at end of file diff --git a/tests/mock/regex_match/pre.json b/tests/mock/regex_match/pre.json new file mode 100644 index 0000000..e40b778 --- /dev/null +++ b/tests/mock/regex_match/pre.json @@ -0,0 +1,468 @@ +{ + "jsonrpc": "2.0", + "id": "EapiExplorer-1", + "result": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "linkType": "the road to seniority", + "localAsn": "65130.1010", + "prefixesSent": 50, + "receivedUpdates": 0, + "peerAddress": "7.7.7.7", + "v6PrefixesSent": 0, + "establishedTransitions": 0, + "bgpPeerCaps": 75759616, + "negotiatedVersion": 0, + "sentUpdates": 0, + "v4SrTePrefixesSent": 0, + "lastEvent": "NoEvent", + "configuredKeepaliveTime": 5, + "ttl": 2, + "inMessageStats": { + "rtRefreshes": 0, + "notifications": 0, + "queueDepth": 0, + "updates": 0, + "keepalives": 0, + "opens": 0 + }, + "bgpSoftReconfigInbound": "Default", + "bgpPeerFlags": [ + 0, + 263168, + 0 + ], + "prefixesReceived": 100, + "v6SrTePrefixesSent": 0, + "prefixListInfo": { + "filterType": "PrefixList" + }, + "v6PrefixesReceived": 0, + "as4": 1, + "state": "Idle", + "updownTime": 1394, + "asn": "1.2354", + "routerId": "0.0.0.0", + "sentMessages": 0, + "version": 4, + "maintenance": false, + "autoLocalAddress": "disabled", + "lastState": "NoState", + "establishFailHint": "Peer is not activated in any address-family mode", + "bgpPeerOptions2": 3145728, + "bgpPeerOptions3": 98608, + "updateGroupIndex": 1, + "confHoldTime": 15, + "receivedMessages": 0, + "maxTtlHops": null, + "vrf": "default", + "peerGroup": "EVPN-OVERLAY-SPINE", + "noComms": false, + "holdTime": 15, + "peerInUpdateErrors": { + "inUpdErrDisableAfiSafi": 0, + "inUpdErrWithdraw": 0, + "inUpdErrIgnore": 0 + }, + "dropStats": { + "inDropNhAfV6": 0, + "inDropNhLocal": 0, + "inDropEnforceFirstAs": 0, + "inDropOrigId": 0, + "outDropV6LocalAddr": 0, + "prefixLuDroppedV6": 0, + "outDropV4LocalAddr": 0, + "prefixLuDroppedV4": 0, + "inDropNhInvalid": 0, + "inDropAsloop": 0 + }, + "bfdState": 2, + "bgpPeerHiscaps": 0, + "bgpPeerOptions": 36700192, + "v4SrTePrefixesReceived": 0, + "keepaliveTime": 5, + "miscFlags": 0, + "v6SrTePrefixesReceived": 0, + "idleReason": "Peer is not activated in any address-family mode", + "outMessageStats": { + "rtRefreshes": 0, + "notifications": 0, + "queueDepth": 0, + "updates": 0, + "keepalives": 0, + "opens": 0 + }, + "routeMapInfo": { + "filterType": "RouteMap" + }, + "updateSource": "8.8.8.8", + "localRouterId": "1.1.0.1", + "peerTcpInfo": { + "inqMaxLen": null, + "sndRttVariance": null, + "delayedAckTimeout": null, + "outqLen": null, + "retransTimeout": null, + "sendWindowScale": null, + "congestionWindow": null, + "slowStartThr": null, + "totalRetrans": null, + "sndRtt": null, + "rcvWindow": null, + "rcvWindowScale": null, + "inqLen": null, + "outqMaxLen": null, + "state": null, + "maxSegmentSize": null, + "rcvRtt": null, + "options": null + } + }, + { + "linkType": "external", + "localAsn": "65130.8888", + "receivedUpdates": 0, + "peerAddress": "10.1.0.0", + "v6PrefixesSent": 0, + "establishedTransitions": 0, + "bgpPeerCaps": 75759620, + "negotiatedVersion": 0, + "sentUpdates": 0, + "v4SrTePrefixesSent": 0, + "lastEvent": "Stop", + "configuredKeepaliveTime": 5, + "ttl": 1, + "inMessageStats": { + "rtRefreshes": 0, + "notifications": 0, + "queueDepth": 0, + "updates": 0, + "keepalives": 0, + "opens": 0 + }, + "bgpSoftReconfigInbound": "Default", + "bgpPeerFlags": [ + 16, + 262144, + 0 + ], + "prefixesReceived": 100, + "v6SrTePrefixesSent": 0, + "prefixListInfo": { + "filterType": "PrefixList" + }, + "v6PrefixesReceived": 0, + "as4": 1, + "state": "Idle", + "updownTime": 1394, + "asn": "1.2354", + "routerId": "0.0.0.0", + "sentMessages": 0, + "version": 4, + "maintenance": false, + "autoLocalAddress": "disabled", + "lastState": "Active", + "establishFailHint": "Could not find interface for peer", + "bgpPeerOptions2": 0, + "bgpPeerOptions3": 98352, + "updateGroupIndex": 1, + "confHoldTime": 15, + "receivedMessages": 0, + "maxTtlHops": null, + "vrf": "default", + "peerGroup": "IPv4-UNDERLAY-SPINE", + "noComms": false, + "holdTime": 15, + "peerInUpdateErrors": { + "inUpdErrDisableAfiSafi": 0, + "inUpdErrWithdraw": 0, + "inUpdErrIgnore": 0 + }, + "dropStats": { + "inDropNhAfV6": 0, + "inDropNhLocal": 0, + "inDropEnforceFirstAs": 0, + "inDropOrigId": 0, + "outDropV6LocalAddr": 0, + "prefixLuDroppedV6": 0, + "outDropV4LocalAddr": 0, + "prefixLuDroppedV4": 0, + "inDropNhInvalid": 0, + "inDropAsloop": 0 + }, + "bgpPeerHiscaps": 0, + "bgpPeerOptions": 35651584, + "v4SrTePrefixesReceived": 0, + "keepaliveTime": 5, + "miscFlags": 0, + "v6SrTePrefixesReceived": 0, + "prefixesSent": 50, + "outMessageStats": { + "rtRefreshes": 0, + "notifications": 0, + "queueDepth": 0, + "updates": 0, + "keepalives": 0, + "opens": 0 + }, + "routeMapInfo": { + "filterType": "RouteMap" + }, + "idleReason": "Could not find interface for peer", + "localRouterId": "1.1.0.1", + "peerTcpInfo": { + "inqMaxLen": null, + "sndRttVariance": null, + "delayedAckTimeout": null, + "outqLen": null, + "retransTimeout": null, + "sendWindowScale": null, + "congestionWindow": null, + "slowStartThr": null, + "totalRetrans": null, + "sndRtt": null, + "rcvWindow": null, + "rcvWindowScale": null, + "inqLen": null, + "outqMaxLen": null, + "state": null, + "maxSegmentSize": null, + "rcvRtt": null, + "options": null + } + }, + { + "linkType": "external", + "localAsn": "65130.1100", + "receivedUpdates": 0, + "peerAddress": "10.2.0.0", + "v6PrefixesSent": 0, + "establishedTransitions": 0, + "bgpPeerCaps": 75759620, + "negotiatedVersion": 0, + "sentUpdates": 0, + "v4SrTePrefixesSent": 0, + "lastEvent": "Stop", + "configuredKeepaliveTime": 5, + "ttl": 1, + "inMessageStats": { + "rtRefreshes": 0, + "notifications": 0, + "queueDepth": 0, + "updates": 0, + "keepalives": 0, + "opens": 0 + }, + "bgpSoftReconfigInbound": "Default", + "bgpPeerFlags": [ + 16, + 262144, + 0 + ], + "prefixesReceived": 100, + "v6SrTePrefixesSent": 0, + "prefixListInfo": { + "filterType": "PrefixList" + }, + "v6PrefixesReceived": 0, + "as4": 1, + "state": "Idle", + "updownTime": 1394, + "asn": "1.2354", + "routerId": "0.0.0.0", + "sentMessages": 0, + "version": 4, + "maintenance": false, + "autoLocalAddress": "disabled", + "lastState": "Active", + "establishFailHint": "Could not find interface for peer", + "bgpPeerOptions2": 0, + "bgpPeerOptions3": 98352, + "updateGroupIndex": 1, + "confHoldTime": 15, + "receivedMessages": 0, + "maxTtlHops": null, + "vrf": "default", + "peerGroup": "IPv4-UNDERLAY-SPINE", + "noComms": false, + "holdTime": 15, + "peerInUpdateErrors": { + "inUpdErrDisableAfiSafi": 0, + "inUpdErrWithdraw": 0, + "inUpdErrIgnore": 0 + }, + "dropStats": { + "inDropNhAfV6": 0, + "inDropNhLocal": 0, + "inDropEnforceFirstAs": 0, + "inDropOrigId": 0, + "outDropV6LocalAddr": 0, + "prefixLuDroppedV6": 0, + "outDropV4LocalAddr": 0, + "prefixLuDroppedV4": 0, + "inDropNhInvalid": 0, + "inDropAsloop": 0 + }, + "bgpPeerHiscaps": 0, + "bgpPeerOptions": 35651584, + "v4SrTePrefixesReceived": 0, + "keepaliveTime": 5, + "miscFlags": 0, + "v6SrTePrefixesReceived": 0, + "prefixesSent": 50, + "outMessageStats": { + "rtRefreshes": 0, + "notifications": 0, + "queueDepth": 0, + "updates": 0, + "keepalives": 0, + "opens": 0 + }, + "routeMapInfo": { + "filterType": "RouteMap" + }, + "idleReason": "Could not find interface for peer", + "localRouterId": "1.1.0.1", + "peerTcpInfo": { + "inqMaxLen": null, + "sndRttVariance": null, + "delayedAckTimeout": null, + "outqLen": null, + "retransTimeout": null, + "sendWindowScale": null, + "congestionWindow": null, + "slowStartThr": null, + "totalRetrans": null, + "sndRtt": null, + "rcvWindow": null, + "rcvWindowScale": null, + "inqLen": null, + "outqMaxLen": null, + "state": null, + "maxSegmentSize": null, + "rcvRtt": null, + "options": null + } + }, + { + "linkType": "external", + "localAsn": "65130.1100", + "receivedUpdates": 0, + "peerAddress": "10.64.207.255", + "v6PrefixesSent": 0, + "establishedTransitions": 0, + "bgpPeerCaps": 75759620, + "negotiatedVersion": 0, + "sentUpdates": 0, + "v4SrTePrefixesSent": 0, + "lastEvent": "Stop", + "configuredKeepaliveTime": 5, + "ttl": 1, + "inMessageStats": { + "rtRefreshes": 0, + "notifications": 0, + "queueDepth": 0, + "updates": 0, + "keepalives": 0, + "opens": 0 + }, + "bgpSoftReconfigInbound": "Default", + "bgpPeerFlags": [ + 16, + 262144, + 0 + ], + "prefixesReceived": 100, + "v6SrTePrefixesSent": 0, + "prefixListInfo": { + "filterType": "PrefixList" + }, + "v6PrefixesReceived": 0, + "as4": 1, + "state": "Idle", + "updownTime": 1394, + "asn": "12345", + "routerId": "0.0.0.0", + "sentMessages": 0, + "version": 4, + "maintenance": false, + "autoLocalAddress": "disabled", + "lastState": "Active", + "establishFailHint": "Could not find interface for peer", + "bgpPeerOptions2": 0, + "bgpPeerOptions3": 98352, + "updateGroupIndex": 2, + "confHoldTime": 15, + "receivedMessages": 0, + "maxTtlHops": null, + "vrf": "default", + "peerGroup": "IPv4-UNDERLAY-MLAG-PEER", + "noComms": false, + "holdTime": 15, + "peerInUpdateErrors": { + "inUpdErrDisableAfiSafi": 0, + "inUpdErrWithdraw": 0, + "inUpdErrIgnore": 0 + }, + "dropStats": { + "inDropNhAfV6": 0, + "inDropNhLocal": 0, + "inDropEnforceFirstAs": 0, + "inDropOrigId": 0, + "outDropV6LocalAddr": 0, + "prefixLuDroppedV6": 0, + "outDropV4LocalAddr": 0, + "prefixLuDroppedV4": 0, + "inDropNhInvalid": 0, + "inDropAsloop": 0 + }, + "bgpPeerHiscaps": 0, + "bgpPeerOptions": 52428800, + "v4SrTePrefixesReceived": 0, + "keepaliveTime": 5, + "miscFlags": 0, + "v6SrTePrefixesReceived": 0, + "prefixesSent": 50, + "outMessageStats": { + "rtRefreshes": 0, + "notifications": 0, + "queueDepth": 0, + "updates": 0, + "keepalives": 0, + "opens": 0 + }, + "routeMapInfo": { + "filterType": "RouteMap" + }, + "idleReason": "Could not find interface for peer", + "localRouterId": "1.1.0.1", + "peerTcpInfo": { + "inqMaxLen": null, + "sndRttVariance": null, + "delayedAckTimeout": null, + "outqLen": null, + "retransTimeout": null, + "sendWindowScale": null, + "congestionWindow": null, + "slowStartThr": null, + "totalRetrans": null, + "sndRtt": null, + "rcvWindow": null, + "rcvWindowScale": null, + "inqLen": null, + "outqMaxLen": null, + "state": null, + "maxSegmentSize": null, + "rcvRtt": null, + "options": null + } + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/tests/test_type_check.py b/tests/test_type_check.py index b01d26d..8f87e80 100644 --- a/tests/test_type_check.py +++ b/tests/test_type_check.py @@ -187,3 +187,29 @@ def test_param_match(filename, check_args, path, expected_result): assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( output=actual_results, expected_output=expected_result ) + + +regex_match = ( + "pre.json", + ("regex", {"regex": ".*UNDERLAY.*"}), + "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", + ( + { + "7.7.7.7": {"peerGroup": "EVPN-OVERLAY-SPINE"}, + }, + False, + ), +) + + +@pytest.mark.parametrize("filename, check_args, path, expected_result", [parameter_match_api]) +def test_regex_match(filename, check_args, path, expected_result): + """Validate regex check type.""" + check = CheckType.init(*check_args) + # There is not concept of "pre" and "post" in parameter_match. + data = load_json_file("regex", filename) + value = check.get_value(data, path) + actual_results = check.evaluate(value, check_args) + assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( + output=actual_results, expected_output=expected_result + ) \ No newline at end of file From e0ecb7fbf4c4743ee8002c35e70f49d7c9fec1cc Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 23 Dec 2021 11:00:51 +0100 Subject: [PATCH 02/80] fix file import --- netcompare/check_type.py | 2 +- tests/test_type_check.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/netcompare/check_type.py b/netcompare/check_type.py index b0df432..6f34f28 100644 --- a/netcompare/check_type.py +++ b/netcompare/check_type.py @@ -1,6 +1,6 @@ """CheckType Implementation.""" from typing import Mapping, Tuple, List, Dict, Any -from .evaluator import diff_generator, parameter_evaluator +from .evaluator import diff_generator, parameter_evaluator, regex_evaluator from .runner import extract_values_from_output diff --git a/tests/test_type_check.py b/tests/test_type_check.py index 8f87e80..ac1e4ce 100644 --- a/tests/test_type_check.py +++ b/tests/test_type_check.py @@ -202,12 +202,12 @@ def test_param_match(filename, check_args, path, expected_result): ) -@pytest.mark.parametrize("filename, check_args, path, expected_result", [parameter_match_api]) +@pytest.mark.parametrize("filename, check_args, path, expected_result", [regex_match]) def test_regex_match(filename, check_args, path, expected_result): """Validate regex check type.""" check = CheckType.init(*check_args) # There is not concept of "pre" and "post" in parameter_match. - data = load_json_file("regex", filename) + data = load_json_file("api", filename) value = check.get_value(data, path) actual_results = check.evaluate(value, check_args) assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( From 5dc7a123737305223b1b6304339cb6ae01509b03 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Tue, 11 Jan 2022 10:53:38 +0100 Subject: [PATCH 03/80] draft regex implementation --- netcompare/check_type.py | 10 +- netcompare/evaluator.py | 15 +- tests/mock/regex_match/pre.json | 468 -------------------------------- tests/test_type_check.py | 2 +- 4 files changed, 21 insertions(+), 474 deletions(-) delete mode 100644 tests/mock/regex_match/pre.json diff --git a/netcompare/check_type.py b/netcompare/check_type.py index 6f34f28..bd21391 100644 --- a/netcompare/check_type.py +++ b/netcompare/check_type.py @@ -112,11 +112,17 @@ def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple try: parameter = value_to_compare[1] except IndexError as error: - raise f"Evaluating parameter must be defined as dict at index 1. You have: {value_to_compare}" from error - assert (isinstance(parameter, dict), isinstance(parameter['regex'], str)), "check_option must be of type dict() as in example: {'regex': '\d.'}" + raise IndexError( + f"Evaluating parameter must be defined as dict at index 1. You have: {value_to_compare}" + ) from error + + if not all([isinstance(parameter, dict), isinstance(parameter["regex"], str)]): + raise TypeError("check_option must be of type dict() as in example: {'regex': '\d.'}") + diff = regex_evaluator(reference_value, parameter) return diff, not diff + # TODO: compare is no longer the entry point, we should use the libary as: # netcompare_check = CheckType.init(check_type_info, options) # pre_result = netcompare_check.get_value(pre_obj, path) diff --git a/netcompare/evaluator.py b/netcompare/evaluator.py index dddcfbf..5e380df 100644 --- a/netcompare/evaluator.py +++ b/netcompare/evaluator.py @@ -133,13 +133,22 @@ def parameter_evaluator(values: Mapping, parameter: Mapping) -> Dict: return result -def regex_evaluator(values: Mapping, parameter: Mapping) -> Mapping: +def regex_evaluator(values: Mapping, parameter: Mapping) -> Dict: """Regex Match evaluator engine.""" - # value: [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE'}] + # values: [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE'}}] # parameter: {'regex': '.*UNDERLAY.*'} result = {} if not isinstance(values, list): raise TypeError("Something went wrong during JMSPath parsing. values must be of type list.") + regex_expression = parameter['regex'] - return result \ No newline at end of file + for item in values: + for founded_value in item.values(): + for value in founded_value.values(): + match_result = re.search(regex_expression, value) + if match_result: + result.update(item) + + + return result diff --git a/tests/mock/regex_match/pre.json b/tests/mock/regex_match/pre.json deleted file mode 100644 index e40b778..0000000 --- a/tests/mock/regex_match/pre.json +++ /dev/null @@ -1,468 +0,0 @@ -{ - "jsonrpc": "2.0", - "id": "EapiExplorer-1", - "result": [ - { - "vrfs": { - "default": { - "peerList": [ - { - "linkType": "the road to seniority", - "localAsn": "65130.1010", - "prefixesSent": 50, - "receivedUpdates": 0, - "peerAddress": "7.7.7.7", - "v6PrefixesSent": 0, - "establishedTransitions": 0, - "bgpPeerCaps": 75759616, - "negotiatedVersion": 0, - "sentUpdates": 0, - "v4SrTePrefixesSent": 0, - "lastEvent": "NoEvent", - "configuredKeepaliveTime": 5, - "ttl": 2, - "inMessageStats": { - "rtRefreshes": 0, - "notifications": 0, - "queueDepth": 0, - "updates": 0, - "keepalives": 0, - "opens": 0 - }, - "bgpSoftReconfigInbound": "Default", - "bgpPeerFlags": [ - 0, - 263168, - 0 - ], - "prefixesReceived": 100, - "v6SrTePrefixesSent": 0, - "prefixListInfo": { - "filterType": "PrefixList" - }, - "v6PrefixesReceived": 0, - "as4": 1, - "state": "Idle", - "updownTime": 1394, - "asn": "1.2354", - "routerId": "0.0.0.0", - "sentMessages": 0, - "version": 4, - "maintenance": false, - "autoLocalAddress": "disabled", - "lastState": "NoState", - "establishFailHint": "Peer is not activated in any address-family mode", - "bgpPeerOptions2": 3145728, - "bgpPeerOptions3": 98608, - "updateGroupIndex": 1, - "confHoldTime": 15, - "receivedMessages": 0, - "maxTtlHops": null, - "vrf": "default", - "peerGroup": "EVPN-OVERLAY-SPINE", - "noComms": false, - "holdTime": 15, - "peerInUpdateErrors": { - "inUpdErrDisableAfiSafi": 0, - "inUpdErrWithdraw": 0, - "inUpdErrIgnore": 0 - }, - "dropStats": { - "inDropNhAfV6": 0, - "inDropNhLocal": 0, - "inDropEnforceFirstAs": 0, - "inDropOrigId": 0, - "outDropV6LocalAddr": 0, - "prefixLuDroppedV6": 0, - "outDropV4LocalAddr": 0, - "prefixLuDroppedV4": 0, - "inDropNhInvalid": 0, - "inDropAsloop": 0 - }, - "bfdState": 2, - "bgpPeerHiscaps": 0, - "bgpPeerOptions": 36700192, - "v4SrTePrefixesReceived": 0, - "keepaliveTime": 5, - "miscFlags": 0, - "v6SrTePrefixesReceived": 0, - "idleReason": "Peer is not activated in any address-family mode", - "outMessageStats": { - "rtRefreshes": 0, - "notifications": 0, - "queueDepth": 0, - "updates": 0, - "keepalives": 0, - "opens": 0 - }, - "routeMapInfo": { - "filterType": "RouteMap" - }, - "updateSource": "8.8.8.8", - "localRouterId": "1.1.0.1", - "peerTcpInfo": { - "inqMaxLen": null, - "sndRttVariance": null, - "delayedAckTimeout": null, - "outqLen": null, - "retransTimeout": null, - "sendWindowScale": null, - "congestionWindow": null, - "slowStartThr": null, - "totalRetrans": null, - "sndRtt": null, - "rcvWindow": null, - "rcvWindowScale": null, - "inqLen": null, - "outqMaxLen": null, - "state": null, - "maxSegmentSize": null, - "rcvRtt": null, - "options": null - } - }, - { - "linkType": "external", - "localAsn": "65130.8888", - "receivedUpdates": 0, - "peerAddress": "10.1.0.0", - "v6PrefixesSent": 0, - "establishedTransitions": 0, - "bgpPeerCaps": 75759620, - "negotiatedVersion": 0, - "sentUpdates": 0, - "v4SrTePrefixesSent": 0, - "lastEvent": "Stop", - "configuredKeepaliveTime": 5, - "ttl": 1, - "inMessageStats": { - "rtRefreshes": 0, - "notifications": 0, - "queueDepth": 0, - "updates": 0, - "keepalives": 0, - "opens": 0 - }, - "bgpSoftReconfigInbound": "Default", - "bgpPeerFlags": [ - 16, - 262144, - 0 - ], - "prefixesReceived": 100, - "v6SrTePrefixesSent": 0, - "prefixListInfo": { - "filterType": "PrefixList" - }, - "v6PrefixesReceived": 0, - "as4": 1, - "state": "Idle", - "updownTime": 1394, - "asn": "1.2354", - "routerId": "0.0.0.0", - "sentMessages": 0, - "version": 4, - "maintenance": false, - "autoLocalAddress": "disabled", - "lastState": "Active", - "establishFailHint": "Could not find interface for peer", - "bgpPeerOptions2": 0, - "bgpPeerOptions3": 98352, - "updateGroupIndex": 1, - "confHoldTime": 15, - "receivedMessages": 0, - "maxTtlHops": null, - "vrf": "default", - "peerGroup": "IPv4-UNDERLAY-SPINE", - "noComms": false, - "holdTime": 15, - "peerInUpdateErrors": { - "inUpdErrDisableAfiSafi": 0, - "inUpdErrWithdraw": 0, - "inUpdErrIgnore": 0 - }, - "dropStats": { - "inDropNhAfV6": 0, - "inDropNhLocal": 0, - "inDropEnforceFirstAs": 0, - "inDropOrigId": 0, - "outDropV6LocalAddr": 0, - "prefixLuDroppedV6": 0, - "outDropV4LocalAddr": 0, - "prefixLuDroppedV4": 0, - "inDropNhInvalid": 0, - "inDropAsloop": 0 - }, - "bgpPeerHiscaps": 0, - "bgpPeerOptions": 35651584, - "v4SrTePrefixesReceived": 0, - "keepaliveTime": 5, - "miscFlags": 0, - "v6SrTePrefixesReceived": 0, - "prefixesSent": 50, - "outMessageStats": { - "rtRefreshes": 0, - "notifications": 0, - "queueDepth": 0, - "updates": 0, - "keepalives": 0, - "opens": 0 - }, - "routeMapInfo": { - "filterType": "RouteMap" - }, - "idleReason": "Could not find interface for peer", - "localRouterId": "1.1.0.1", - "peerTcpInfo": { - "inqMaxLen": null, - "sndRttVariance": null, - "delayedAckTimeout": null, - "outqLen": null, - "retransTimeout": null, - "sendWindowScale": null, - "congestionWindow": null, - "slowStartThr": null, - "totalRetrans": null, - "sndRtt": null, - "rcvWindow": null, - "rcvWindowScale": null, - "inqLen": null, - "outqMaxLen": null, - "state": null, - "maxSegmentSize": null, - "rcvRtt": null, - "options": null - } - }, - { - "linkType": "external", - "localAsn": "65130.1100", - "receivedUpdates": 0, - "peerAddress": "10.2.0.0", - "v6PrefixesSent": 0, - "establishedTransitions": 0, - "bgpPeerCaps": 75759620, - "negotiatedVersion": 0, - "sentUpdates": 0, - "v4SrTePrefixesSent": 0, - "lastEvent": "Stop", - "configuredKeepaliveTime": 5, - "ttl": 1, - "inMessageStats": { - "rtRefreshes": 0, - "notifications": 0, - "queueDepth": 0, - "updates": 0, - "keepalives": 0, - "opens": 0 - }, - "bgpSoftReconfigInbound": "Default", - "bgpPeerFlags": [ - 16, - 262144, - 0 - ], - "prefixesReceived": 100, - "v6SrTePrefixesSent": 0, - "prefixListInfo": { - "filterType": "PrefixList" - }, - "v6PrefixesReceived": 0, - "as4": 1, - "state": "Idle", - "updownTime": 1394, - "asn": "1.2354", - "routerId": "0.0.0.0", - "sentMessages": 0, - "version": 4, - "maintenance": false, - "autoLocalAddress": "disabled", - "lastState": "Active", - "establishFailHint": "Could not find interface for peer", - "bgpPeerOptions2": 0, - "bgpPeerOptions3": 98352, - "updateGroupIndex": 1, - "confHoldTime": 15, - "receivedMessages": 0, - "maxTtlHops": null, - "vrf": "default", - "peerGroup": "IPv4-UNDERLAY-SPINE", - "noComms": false, - "holdTime": 15, - "peerInUpdateErrors": { - "inUpdErrDisableAfiSafi": 0, - "inUpdErrWithdraw": 0, - "inUpdErrIgnore": 0 - }, - "dropStats": { - "inDropNhAfV6": 0, - "inDropNhLocal": 0, - "inDropEnforceFirstAs": 0, - "inDropOrigId": 0, - "outDropV6LocalAddr": 0, - "prefixLuDroppedV6": 0, - "outDropV4LocalAddr": 0, - "prefixLuDroppedV4": 0, - "inDropNhInvalid": 0, - "inDropAsloop": 0 - }, - "bgpPeerHiscaps": 0, - "bgpPeerOptions": 35651584, - "v4SrTePrefixesReceived": 0, - "keepaliveTime": 5, - "miscFlags": 0, - "v6SrTePrefixesReceived": 0, - "prefixesSent": 50, - "outMessageStats": { - "rtRefreshes": 0, - "notifications": 0, - "queueDepth": 0, - "updates": 0, - "keepalives": 0, - "opens": 0 - }, - "routeMapInfo": { - "filterType": "RouteMap" - }, - "idleReason": "Could not find interface for peer", - "localRouterId": "1.1.0.1", - "peerTcpInfo": { - "inqMaxLen": null, - "sndRttVariance": null, - "delayedAckTimeout": null, - "outqLen": null, - "retransTimeout": null, - "sendWindowScale": null, - "congestionWindow": null, - "slowStartThr": null, - "totalRetrans": null, - "sndRtt": null, - "rcvWindow": null, - "rcvWindowScale": null, - "inqLen": null, - "outqMaxLen": null, - "state": null, - "maxSegmentSize": null, - "rcvRtt": null, - "options": null - } - }, - { - "linkType": "external", - "localAsn": "65130.1100", - "receivedUpdates": 0, - "peerAddress": "10.64.207.255", - "v6PrefixesSent": 0, - "establishedTransitions": 0, - "bgpPeerCaps": 75759620, - "negotiatedVersion": 0, - "sentUpdates": 0, - "v4SrTePrefixesSent": 0, - "lastEvent": "Stop", - "configuredKeepaliveTime": 5, - "ttl": 1, - "inMessageStats": { - "rtRefreshes": 0, - "notifications": 0, - "queueDepth": 0, - "updates": 0, - "keepalives": 0, - "opens": 0 - }, - "bgpSoftReconfigInbound": "Default", - "bgpPeerFlags": [ - 16, - 262144, - 0 - ], - "prefixesReceived": 100, - "v6SrTePrefixesSent": 0, - "prefixListInfo": { - "filterType": "PrefixList" - }, - "v6PrefixesReceived": 0, - "as4": 1, - "state": "Idle", - "updownTime": 1394, - "asn": "12345", - "routerId": "0.0.0.0", - "sentMessages": 0, - "version": 4, - "maintenance": false, - "autoLocalAddress": "disabled", - "lastState": "Active", - "establishFailHint": "Could not find interface for peer", - "bgpPeerOptions2": 0, - "bgpPeerOptions3": 98352, - "updateGroupIndex": 2, - "confHoldTime": 15, - "receivedMessages": 0, - "maxTtlHops": null, - "vrf": "default", - "peerGroup": "IPv4-UNDERLAY-MLAG-PEER", - "noComms": false, - "holdTime": 15, - "peerInUpdateErrors": { - "inUpdErrDisableAfiSafi": 0, - "inUpdErrWithdraw": 0, - "inUpdErrIgnore": 0 - }, - "dropStats": { - "inDropNhAfV6": 0, - "inDropNhLocal": 0, - "inDropEnforceFirstAs": 0, - "inDropOrigId": 0, - "outDropV6LocalAddr": 0, - "prefixLuDroppedV6": 0, - "outDropV4LocalAddr": 0, - "prefixLuDroppedV4": 0, - "inDropNhInvalid": 0, - "inDropAsloop": 0 - }, - "bgpPeerHiscaps": 0, - "bgpPeerOptions": 52428800, - "v4SrTePrefixesReceived": 0, - "keepaliveTime": 5, - "miscFlags": 0, - "v6SrTePrefixesReceived": 0, - "prefixesSent": 50, - "outMessageStats": { - "rtRefreshes": 0, - "notifications": 0, - "queueDepth": 0, - "updates": 0, - "keepalives": 0, - "opens": 0 - }, - "routeMapInfo": { - "filterType": "RouteMap" - }, - "idleReason": "Could not find interface for peer", - "localRouterId": "1.1.0.1", - "peerTcpInfo": { - "inqMaxLen": null, - "sndRttVariance": null, - "delayedAckTimeout": null, - "outqLen": null, - "retransTimeout": null, - "sendWindowScale": null, - "congestionWindow": null, - "slowStartThr": null, - "totalRetrans": null, - "sndRtt": null, - "rcvWindow": null, - "rcvWindowScale": null, - "inqLen": null, - "outqMaxLen": null, - "state": null, - "maxSegmentSize": null, - "rcvRtt": null, - "options": null - } - } - ] - } - } - } - ] -} \ No newline at end of file diff --git a/tests/test_type_check.py b/tests/test_type_check.py index ac1e4ce..8b35909 100644 --- a/tests/test_type_check.py +++ b/tests/test_type_check.py @@ -212,4 +212,4 @@ def test_regex_match(filename, check_args, path, expected_result): actual_results = check.evaluate(value, check_args) assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( output=actual_results, expected_output=expected_result - ) \ No newline at end of file + ) From 83028d2bea24a0360289d6d55d46b88083d6e664 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Tue, 11 Jan 2022 11:39:50 +0100 Subject: [PATCH 04/80] fix logic into regex result --- netcompare/check_type.py | 2 +- netcompare/evaluator.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/netcompare/check_type.py b/netcompare/check_type.py index bd21391..0b2a0cb 100644 --- a/netcompare/check_type.py +++ b/netcompare/check_type.py @@ -117,7 +117,7 @@ def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple ) from error if not all([isinstance(parameter, dict), isinstance(parameter["regex"], str)]): - raise TypeError("check_option must be of type dict() as in example: {'regex': '\d.'}") + raise TypeError("check_option must be of type dict() as in example: {'regex': '.*UNDERLAY.*'}") diff = regex_evaluator(reference_value, parameter) return diff, not diff diff --git a/netcompare/evaluator.py b/netcompare/evaluator.py index 5e380df..9d330e3 100644 --- a/netcompare/evaluator.py +++ b/netcompare/evaluator.py @@ -141,14 +141,13 @@ def regex_evaluator(values: Mapping, parameter: Mapping) -> Dict: if not isinstance(values, list): raise TypeError("Something went wrong during JMSPath parsing. values must be of type list.") - regex_expression = parameter['regex'] + regex_expression = parameter["regex"] for item in values: for founded_value in item.values(): for value in founded_value.values(): match_result = re.search(regex_expression, value) - if match_result: + if not match_result: result.update(item) - return result From 63fcac7af1177fdecdf5d2de296b7d46e2e39c9b Mon Sep 17 00:00:00 2001 From: Network to Code Date: Tue, 11 Jan 2022 11:58:40 +0100 Subject: [PATCH 05/80] remove assertion --- netcompare/check_type.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/netcompare/check_type.py b/netcompare/check_type.py index 0b2a0cb..3fecbc8 100644 --- a/netcompare/check_type.py +++ b/netcompare/check_type.py @@ -99,7 +99,9 @@ def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple parameter = value_to_compare[1] except IndexError as error: raise f"Evaluating parameter must be defined as dict at index 1. You have: {value_to_compare}" from error - assert isinstance(parameter, dict), "check_option must be of type dict()" + if not isinstance(parameter, dict): + raise TypeError("check_option must be of type dict()") + diff = parameter_evaluator(reference_value, parameter) return diff, not diff From e26ee77f277b713b743058798daf3f1c5b298cc8 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Wed, 12 Jan 2022 09:48:34 +0100 Subject: [PATCH 06/80] draft test for range check type --- tests/test_type_check.py | 144 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/tests/test_type_check.py b/tests/test_type_check.py index 8b35909..4e4a19d 100644 --- a/tests/test_type_check.py +++ b/tests/test_type_check.py @@ -213,3 +213,147 @@ def test_regex_match(filename, check_args, path, expected_result): assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( output=actual_results, expected_output=expected_result ) + +range_all_same = ( + "api", + ("range", {"all-same": True}), + "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup,vrf,state]", + ( + {}, #TBD + False, #TBD + ), +) + +range_is_equal = ( + "api", + ("range", {"is-equal": 100}), + "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesReceived]", + ( + {}, #TBD + False, #TBD + ), +) + +range_not_equal = ( + "api", + ("range", {"not-equal": "internal"}), + "result[0].vrfs.default.peerList[*].[$peerAddress$,linkType]", + ( + {}, #TBD + False, #TBD + ), +) + +range_contains = ( + "api", + ("range", {"contains": "EVPN"}), + "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", + ( + {}, #TBD + False, #TBD + ), +) + +range_not_contains = ( + "api", + ("range", {"not-contains": "OVERLAY"}), + "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", + ( + {}, #TBD + False, #TBD + ), +) + +range_is_gt = ( + "api", + ("range", {"is-gt": 70000000}), + "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", + ( + {}, #TBD + False, #TBD + ), +) + +range_is_lt = ( + "api", + ("range", {"is-lt": 80000000}), + "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", + ( + {}, #TBD + False, #TBD + ), +) + +range_in_range = ( + "api", + ("range", {"in-range": (70000000, 80000000)}), + "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", + ( + {}, #TBD + False, #TBD + ), +) + +range_not_range = ( + "api", + ("range", {"not-range": (70000000, 80000000)}), + "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", + ( + {}, #TBD + False, #TBD + ), +) + +range_is_in = ( + "api", + ("range", {"is-in": ("Idle", "Down")}), + "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", + ( + {}, #TBD + False, #TBD + ), +) + +range_not_in = ( + "api", + ("range", {"not-in": ("Idle", "Down")}), + "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", + ( + {}, #TBD + False, #TBD + ), +) +range_all_tests = [ + # type() == str(), int(), float() + range_all_same, + range_is_equal, + range_not_equal, + range_contains, + range_not_contains, + # type() == int(), float() + range_is_gt, + range_is_lt, + range_in_range, + range_not_range, + # type() == str() + range_is_in, + range_not_in, +] + + +@pytest.mark.parametrize("folder_name, check_args, path, expected_result", range_all_tests) +def test_range(folder_name, check_args, path, expected_result): + """Validate all range check types.""" + pre_data, post_data = load_mocks(folder_name) + + check = CheckType.init(*check_args) + pre_data, post_data = load_mocks(folder_name) + pre_value = check.get_value(pre_data, path) + post_value = check.get_value(post_data, path) + actual_results = check.evaluate(pre_value, post_value) + + assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( + output=actual_results, expected_output=expected_result + ) + + From 0b4fc11600b217f74a76fc35730c7b6faccd4d10 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 13 Jan 2022 09:43:29 +0100 Subject: [PATCH 07/80] add match/no-match logic --- netcompare/evaluator.py | 9 +++++++-- tests/test_type_check.py | 22 +++++++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/netcompare/evaluator.py b/netcompare/evaluator.py index 9d330e3..f6a944c 100644 --- a/netcompare/evaluator.py +++ b/netcompare/evaluator.py @@ -136,18 +136,23 @@ def parameter_evaluator(values: Mapping, parameter: Mapping) -> Dict: def regex_evaluator(values: Mapping, parameter: Mapping) -> Dict: """Regex Match evaluator engine.""" # values: [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE'}}] - # parameter: {'regex': '.*UNDERLAY.*'} + # parameter: {'regex': '.*UNDERLAY.*', 'mode': 'include'} result = {} if not isinstance(values, list): raise TypeError("Something went wrong during JMSPath parsing. values must be of type list.") regex_expression = parameter["regex"] + mode = parameter["mode"] for item in values: for founded_value in item.values(): for value in founded_value.values(): match_result = re.search(regex_expression, value) - if not match_result: + # Fail if there is not regex match + if mode == "match" and not match_result: + result.update(item) + # Fail if there is regex match + elif mode == "no-match" and match_result: result.update(item) return result diff --git a/tests/test_type_check.py b/tests/test_type_check.py index 8b35909..09ab20d 100644 --- a/tests/test_type_check.py +++ b/tests/test_type_check.py @@ -189,9 +189,9 @@ def test_param_match(filename, check_args, path, expected_result): ) -regex_match = ( +regex_match_include = ( "pre.json", - ("regex", {"regex": ".*UNDERLAY.*"}), + ("regex", {"regex": ".*UNDERLAY.*", "mode": "match"}), "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", ( { @@ -201,8 +201,24 @@ def test_param_match(filename, check_args, path, expected_result): ), ) +regex_match_exclude = ( + "pre.json", + ("regex", {"regex": ".*UNDERLAY.*", "mode": "no-match"}), + "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", + ( + { + "10.1.0.0": {"peerGroup": "IPv4-UNDERLAY-SPINE"}, + "10.2.0.0": {"peerGroup": "IPv4-UNDERLAY-SPINE"}, + "10.64.207.255": {"peerGroup": "IPv4-UNDERLAY-MLAG-PEER"}, + }, + False, + ), +) + +regex_match = [regex_match_include, regex_match_exclude] + -@pytest.mark.parametrize("filename, check_args, path, expected_result", [regex_match]) +@pytest.mark.parametrize("filename, check_args, path, expected_result", regex_match) def test_regex_match(filename, check_args, path, expected_result): """Validate regex check type.""" check = CheckType.init(*check_args) From a49773da294d02b89c7cd54f57cce56865ee65e9 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 13 Jan 2022 10:33:38 +0100 Subject: [PATCH 08/80] commit save --- netcompare/check_type.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/netcompare/check_type.py b/netcompare/check_type.py index 3fecbc8..982573a 100644 --- a/netcompare/check_type.py +++ b/netcompare/check_type.py @@ -110,7 +110,7 @@ class RegexType(CheckType): """Regex Match class implementation.""" def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple[Mapping, bool]: - """Parameter Match evaluator implementation.""" + """Regex Match evaluator implementation.""" try: parameter = value_to_compare[1] except IndexError as error: @@ -118,6 +118,15 @@ def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple f"Evaluating parameter must be defined as dict at index 1. You have: {value_to_compare}" ) from error + try: + parameter['regex'] + parameter['mode'] + except KeyError as error: + raise KeyError( + f"""Regex check-type requires check-option. Example: dict(regex='.*UNDERLAY.*', mode='no-match') + Read the docs for more information.""" + ) from error + if not all([isinstance(parameter, dict), isinstance(parameter["regex"], str)]): raise TypeError("check_option must be of type dict() as in example: {'regex': '.*UNDERLAY.*'}") @@ -125,6 +134,24 @@ def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple return diff, not diff +class RangeType(CheckType): + """Range Match class implementation.""" + + def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple[Mapping, bool]: + """Range Match evaluator implementation.""" + try: + parameter = value_to_compare[1] + except IndexError as error: + raise IndexError( + f"Evaluating parameter must be defined as dict at index 1. You have: {value_to_compare}" + ) from error + + if not all([isinstance(parameter, dict), isinstance(parameter["regex"], str)]): + raise TypeError("check_option must be of type dict() as in example: {'regex': '.*UNDERLAY.*'}") + + diff = regex_evaluator(reference_value, parameter) + return diff, not diff + # TODO: compare is no longer the entry point, we should use the libary as: # netcompare_check = CheckType.init(check_type_info, options) # pre_result = netcompare_check.get_value(pre_obj, path) From 79ff35ec1eb875e113451313f5f84db620505898 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 13 Jan 2022 10:51:31 +0100 Subject: [PATCH 09/80] add assertion for check option --- netcompare/check_type.py | 20 +++++++++++++++++--- tests/test_type_check.py | 5 ++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/netcompare/check_type.py b/netcompare/check_type.py index 3fecbc8..5796eda 100644 --- a/netcompare/check_type.py +++ b/netcompare/check_type.py @@ -110,7 +110,8 @@ class RegexType(CheckType): """Regex Match class implementation.""" def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple[Mapping, bool]: - """Parameter Match evaluator implementation.""" + """Regex Match evaluator implementation.""" + # Assert that check parameters are at index 1. try: parameter = value_to_compare[1] except IndexError as error: @@ -118,8 +119,21 @@ def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple f"Evaluating parameter must be defined as dict at index 1. You have: {value_to_compare}" ) from error - if not all([isinstance(parameter, dict), isinstance(parameter["regex"], str)]): - raise TypeError("check_option must be of type dict() as in example: {'regex': '.*UNDERLAY.*'}") + # Assert that check parameters are at index 1. + if not all([isinstance(parameter, dict)]): + raise TypeError("check_option must be of type dict().") + + # Assert that check option has 'regex' and 'mode' dict keys. + if "regex" not in parameter and "mode" not in parameter: + raise KeyError( + "Regex check-type requires check-option. Example: dict(regex='.*UNDERLAY.*', mode='no-match')." + ) + + # Assert that check option has 'regex' and 'mode' dict keys.\ + if parameter["mode"] not in ["match", "no-match"]: + raise ValueError( + "Regex check-type requires check-option. Example: dict(regex='.*UNDERLAY.*', mode='no-match')." + ) diff = regex_evaluator(reference_value, parameter) return diff, not diff diff --git a/tests/test_type_check.py b/tests/test_type_check.py index 09ab20d..2a2da11 100644 --- a/tests/test_type_check.py +++ b/tests/test_type_check.py @@ -215,7 +215,10 @@ def test_param_match(filename, check_args, path, expected_result): ), ) -regex_match = [regex_match_include, regex_match_exclude] +regex_match = [ + regex_match_include, + regex_match_exclude, +] @pytest.mark.parametrize("filename, check_args, path, expected_result", regex_match) From 862063b210abde551757f75b996d05646accc331 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Fri, 14 Jan 2022 10:49:42 +0100 Subject: [PATCH 10/80] add assertions for range type --- netcompare/check_type.py | 80 ++++++++++++++++++++++++++++++++++++++-- tests/test_type_check.py | 2 +- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/netcompare/check_type.py b/netcompare/check_type.py index 982573a..80c0352 100644 --- a/netcompare/check_type.py +++ b/netcompare/check_type.py @@ -1,6 +1,6 @@ """CheckType Implementation.""" from typing import Mapping, Tuple, List, Dict, Any -from .evaluator import diff_generator, parameter_evaluator, regex_evaluator +from .evaluator import diff_generator, parameter_evaluator, regex_evaluator, range_evaluator from .runner import extract_values_from_output @@ -26,6 +26,8 @@ def init(*args): return ParameterMatchType(*args) if check_type == "regex": return RegexType(*args) + if check_type == "range": + return RangeType(*args) raise NotImplementedError @staticmethod @@ -139,6 +141,41 @@ class RangeType(CheckType): def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple[Mapping, bool]: """Range Match evaluator implementation.""" + + valid_options = ( + "all-same", + "is-equal", + "not-equal", + "contains", + "not-contains", + "is-gt", + "is-lt", + "in-range", + "not-range", + "is-in", + "not-in" + ) + + bools = ("all-same") + iter = ( + "is-in", + "not-in", + "in-range", + "not-range" + ) + numbers = ( + "is-gt", + "is-lt", + ) + mix = ( + "all-same", + "is-equal", + "not-equal", + "contains", + "not-contains", + ) + + # Assert that check parameters are at index 1. try: parameter = value_to_compare[1] except IndexError as error: @@ -146,10 +183,45 @@ def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple f"Evaluating parameter must be defined as dict at index 1. You have: {value_to_compare}" ) from error - if not all([isinstance(parameter, dict), isinstance(parameter["regex"], str)]): - raise TypeError("check_option must be of type dict() as in example: {'regex': '.*UNDERLAY.*'}") + if not all([isinstance(parameter, dict)]): + raise TypeError("check-option must be of type dict().") - diff = regex_evaluator(reference_value, parameter) + parameter_key = list(parameter.keys())[0] + parameter_value = list(parameter.values())[0] + + # Assert that check option is valid. + if parameter_key not in valid_options: + raise KeyError( + f"Range check-type requires one of the following check-option: {valid_options}" + ) + + # Assert data type for each range option. + if parameter_key in bools: + # "all-same" requires boolean True or False + if not isinstance(parameter_value, bool): + raise ValueError(f"Range check-option {bools} must have value of type bool. i.e: dict(all-same=True)") + + elif parameter_key in iter: + #"in", "not-in", "in-range", "not-range" requires an iterable + if not isinstance(parameter_value, list) or not isinstance(parameter_value, tuple): + raise ValueError(f"Range check-option {iter} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down')") + # "in-range", "not-range" requires int or floar where value at index 0 is lower than value at index 1 + if "range" in parameter_key: + if not (isinstance(parameter_value[0], int) or isinstance(parameter_value[0], float)) and not (isinstance(parameter_value[1], float) or isinstance(parameter_value[1], int)): + raise ValueError(f"Range check-option {iter} must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000)") + if not parameter_value[0] < parameter_value[1]: + raise ValueError(f"'range' and 'not-range' must have value at index 0 lower than value at index 1. i.e: dict(not-range=(70000000, 80000000)") + else: + # "is-in", "not-in" requires iterable of strings + for item in parameter_value.values(): + if not isinstance(item, str): + raise ValueError(f"'is-in' and 'not-in' must be an iterable of strings. i.e: dict(is-in=(Idle, Down)") + + elif parameter_key in numbers: + if not isinstance(parameter_value, float) or not isinstance(parameter_value, int): + raise ValueError(f"Range check-option {numbers} must have value of type float or int. i.e: dict(is-lt=80000000)") + + diff = range_evaluator(reference_value, parameter) return diff, not diff # TODO: compare is no longer the entry point, we should use the libary as: diff --git a/tests/test_type_check.py b/tests/test_type_check.py index c0665a7..cae9854 100644 --- a/tests/test_type_check.py +++ b/tests/test_type_check.py @@ -351,7 +351,7 @@ def test_regex_match(filename, check_args, path, expected_result): range_is_lt, range_in_range, range_not_range, - # type() == str() + # type() == dict() range_is_in, range_not_in, ] From 841aac9c772b4fd3dcb5958d5155c2887ee4c588 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Fri, 14 Jan 2022 11:32:44 +0100 Subject: [PATCH 11/80] add assertion for strings --- netcompare/check_type.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/netcompare/check_type.py b/netcompare/check_type.py index 80c0352..e75920b 100644 --- a/netcompare/check_type.py +++ b/netcompare/check_type.py @@ -167,10 +167,7 @@ def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple "is-gt", "is-lt", ) - mix = ( - "all-same", - "is-equal", - "not-equal", + strings = ( "contains", "not-contains", ) @@ -221,6 +218,11 @@ def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple if not isinstance(parameter_value, float) or not isinstance(parameter_value, int): raise ValueError(f"Range check-option {numbers} must have value of type float or int. i.e: dict(is-lt=80000000)") + elif parameter_key in strings: + if not isinstance(parameter_value, str): + raise ValueError(f"Range check-option {strings} must have value of type string. i.e: dict(contains='EVPN')") + + diff = range_evaluator(reference_value, parameter) return diff, not diff From 3eb0bccf1375836ff7a37a55c5bbdbbf7d94e371 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Mon, 24 Jan 2022 09:34:00 +0100 Subject: [PATCH 12/80] commit save --- netcompare/check_type.py | 198 ++++++++++++++++++++--------------- tests/test_diff_generator.py | 21 ++-- tests/test_type_check.py | 78 +++++++------- 3 files changed, 162 insertions(+), 135 deletions(-) diff --git a/netcompare/check_type.py b/netcompare/check_type.py index e75920b..f33ca81 100644 --- a/netcompare/check_type.py +++ b/netcompare/check_type.py @@ -1,15 +1,48 @@ """CheckType Implementation.""" from typing import Mapping, Tuple, List, Dict, Any -from .evaluator import diff_generator, parameter_evaluator, regex_evaluator, range_evaluator +from .evaluator import diff_generator, parameter_evaluator, regex_evaluator from .runner import extract_values_from_output +from pydantic import BaseModel, ValidationError, validator + class CheckType: """Check Type Class.""" + # class Validation(BaseModel): + # def __init__(self, value_to_compare): + # self.valid_options = ( + # "all-same", + # "is-equal", + # "not-equal", + # "contains", + # "not-contains", + # "is-gt", + # "is-lt", + # "in-range", + # "not-range", + # "is-in", + # "not-in" + # ) + # self.bools = ("all-same") + # self.fiter = ( + # "is-in", + # "not-in", + # "in-range", + # "not-range" + # ) + # self.numbers = ("is-gt","is-lt") + # self.strings = ("contains", "not-contains") + # self.value_to_compare = value_to_compare + + # my_check = CheckType.init(*args) def __init__(self, *args): """Check Type init method.""" + self.validate(*args) + def validate(*args): + pass + @staticmethod def init(*args): """Factory pattern to get the appropriate CheckType implementation. @@ -26,8 +59,8 @@ def init(*args): return ParameterMatchType(*args) if check_type == "regex": return RegexType(*args) - if check_type == "range": - return RangeType(*args) + if check_type == "operator": + return OperatorType(*args) raise NotImplementedError @staticmethod @@ -47,30 +80,44 @@ def evaluate(self, reference_value: Any, value_to_compare: Any) -> Tuple[Dict, b Returns: tuple: Dictionary representing check result, bool indicating if differences are found. """ + self.validate_reference_value(reference_value, value_to_compare) + self.hook_evaluate() + + def hook_evaluate(): + raise NotImplementedError + + def validate_reference_value(reference_value, value_to_compare): raise NotImplementedError class ExactMatchType(CheckType): """Exact Match class docstring.""" - def evaluate(self, reference_value: Any, value_to_compare: Any) -> Tuple[Dict, bool]: + def hook_evaluate(self, reference_value: Any, value_to_compare: Any) -> Tuple[Dict, bool]: """Returns the difference between values and the boolean.""" diff = diff_generator(reference_value, value_to_compare) return diff, not diff + def validate_reference_value(reference_value, value_to_compare): + if type(reference_value) != type(value_to_compare): + raise ValueError + + def validate(*args): + if len(args) > 1: + raise ValueError + class ToleranceType(CheckType): """Tolerance class docstring.""" - - def __init__(self, *args): + def validate(*args): """Tolerance init method.""" try: tolerance = args[1] except IndexError as error: raise f"Tolerance parameter must be defined as float at index 1. You have: {args}" from error - self.tolerance_factor = float(tolerance) / 100 - super().__init__() + if not any(isinstance(tolerance, int), isinstance(tolerance, float)): + raise ValueError def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple[Dict, bool]: """Returns the difference between values and the boolean. Overwrites method in base class.""" @@ -136,41 +183,39 @@ def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple return diff, not diff -class RangeType(CheckType): - """Range Match class implementation.""" +class OperatorType(CheckType): + """Operator class implementation.""" + + + + + # elif parameter_key in iter: + # #"in", "not-in", "in-range", "not-range" requires an iterable + # if not isinstance(parameter_value, list) or not isinstance(parameter_value, tuple): + # raise ValueError(f"Range check-option {iter} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down')") + # # "in-range", "not-range" requires int or floar where value at index 0 is lower than value at index 1 + # if "range" in parameter_key: + # if not (isinstance(parameter_value[0], int) or isinstance(parameter_value[0], float)) and not (isinstance(parameter_value[1], float) or isinstance(parameter_value[1], int)): + # raise ValueError(f"Range check-option {iter} must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000)") + # if not parameter_value[0] < parameter_value[1]: + # raise ValueError(f"'range' and 'not-range' must have value at index 0 lower than value at index 1. i.e: dict(not-range=(70000000, 80000000)") + # else: + # # "is-in", "not-in" requires iterable of strings + # for item in parameter_value.values(): + # if not isinstance(item, str): + # raise ValueError(f"'is-in' and 'not-in' must be an iterable of strings. i.e: dict(is-in=(Idle, Down)") + + # elif parameter_key in numbers: + # if not isinstance(parameter_value, float) or not isinstance(parameter_value, int): + # raise ValueError(f"Range check-option {numbers} must have value of type float or int. i.e: dict(is-lt=80000000)") + + # elif parameter_key in strings: + # if not isinstance(parameter_value, str): + # raise ValueError(f"Range check-option {strings} must have value of type string. i.e: dict(contains='EVPN')") + def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple[Mapping, bool]: - """Range Match evaluator implementation.""" - - valid_options = ( - "all-same", - "is-equal", - "not-equal", - "contains", - "not-contains", - "is-gt", - "is-lt", - "in-range", - "not-range", - "is-in", - "not-in" - ) - - bools = ("all-same") - iter = ( - "is-in", - "not-in", - "in-range", - "not-range" - ) - numbers = ( - "is-gt", - "is-lt", - ) - strings = ( - "contains", - "not-contains", - ) + """Operator evaluator implementation.""" # Assert that check parameters are at index 1. try: @@ -180,53 +225,34 @@ def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple f"Evaluating parameter must be defined as dict at index 1. You have: {value_to_compare}" ) from error - if not all([isinstance(parameter, dict)]): - raise TypeError("check-option must be of type dict().") - - parameter_key = list(parameter.keys())[0] - parameter_value = list(parameter.values())[0] - - # Assert that check option is valid. - if parameter_key not in valid_options: - raise KeyError( - f"Range check-type requires one of the following check-option: {valid_options}" - ) - - # Assert data type for each range option. - if parameter_key in bools: - # "all-same" requires boolean True or False - if not isinstance(parameter_value, bool): - raise ValueError(f"Range check-option {bools} must have value of type bool. i.e: dict(all-same=True)") + parameter: list + @validator('parameter') + def parameter_must_be_dict(cls, v): + if not isinstance(v, list): + raise TypeError("check-option must be of type dict().") + return parameter + + # parameter_key = list(parameter.keys())[0] + # parameter_value = list(parameter.values())[0] + + # parameter_key: list + # parameter_value: list + # @validator(parameter_key) + # def check_option_must_be_legal_option(parameter_key): + # if parameter_key not in self.valid_options: + # raise KeyError( + # f"Range check-type requires one of the following check-option: {self.valid_options}" + # ) + + # # Assert data type for each range option. + # if parameter_key in bools: + # # "all-same" requires boolean True or False + # if not isinstance(parameter_value, bool): + # raise ValueError(f"Range check-option {bools} must have value of type bool. i.e: dict(all-same=True)") - elif parameter_key in iter: - #"in", "not-in", "in-range", "not-range" requires an iterable - if not isinstance(parameter_value, list) or not isinstance(parameter_value, tuple): - raise ValueError(f"Range check-option {iter} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down')") - # "in-range", "not-range" requires int or floar where value at index 0 is lower than value at index 1 - if "range" in parameter_key: - if not (isinstance(parameter_value[0], int) or isinstance(parameter_value[0], float)) and not (isinstance(parameter_value[1], float) or isinstance(parameter_value[1], int)): - raise ValueError(f"Range check-option {iter} must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000)") - if not parameter_value[0] < parameter_value[1]: - raise ValueError(f"'range' and 'not-range' must have value at index 0 lower than value at index 1. i.e: dict(not-range=(70000000, 80000000)") - else: - # "is-in", "not-in" requires iterable of strings - for item in parameter_value.values(): - if not isinstance(item, str): - raise ValueError(f"'is-in' and 'not-in' must be an iterable of strings. i.e: dict(is-in=(Idle, Down)") - - elif parameter_key in numbers: - if not isinstance(parameter_value, float) or not isinstance(parameter_value, int): - raise ValueError(f"Range check-option {numbers} must have value of type float or int. i.e: dict(is-lt=80000000)") - - elif parameter_key in strings: - if not isinstance(parameter_value, str): - raise ValueError(f"Range check-option {strings} must have value of type string. i.e: dict(contains='EVPN')") - - - diff = range_evaluator(reference_value, parameter) - return diff, not diff - # TODO: compare is no longer the entry point, we should use the libary as: +# check_type_info = "regex" +# options = {"regex": ".*UNDERLAY.*", "mode": "no-match"} # netcompare_check = CheckType.init(check_type_info, options) # pre_result = netcompare_check.get_value(pre_obj, path) # post_result = netcompare_check.get_value(post_obj, path) diff --git a/tests/test_diff_generator.py b/tests/test_diff_generator.py index 8ad8ab5..48672cd 100644 --- a/tests/test_diff_generator.py +++ b/tests/test_diff_generator.py @@ -30,7 +30,8 @@ exact_match_of_bgp_neigh_via_textfsm = ( "textfsm", - "result[*].[$bgp_neigh$,state]", + "", + # "result[*].[$bgp_neigh$,state]", [], {"10.17.254.2": {"state": {"new_value": "Up", "old_value": "Idle"}}}, ) @@ -132,16 +133,16 @@ ) eval_tests = [ - exact_match_of_global_peers_via_napalm_getter, - exact_match_of_bgp_peer_caps_via_api, + # exact_match_of_global_peers_via_napalm_getter, + # exact_match_of_bgp_peer_caps_via_api, exact_match_of_bgp_neigh_via_textfsm, - raw_diff_of_interface_ma1_via_api_value_exclude, - raw_diff_of_interface_ma1_via_api_novalue_exclude, - raw_diff_of_interface_ma1_via_api_novalue_noexclude, - exact_match_missing_item, - exact_match_additional_item, - exact_match_changed_item, - exact_match_multi_nested_list, + # raw_diff_of_interface_ma1_via_api_value_exclude, + # raw_diff_of_interface_ma1_via_api_novalue_exclude, + # raw_diff_of_interface_ma1_via_api_novalue_noexclude, + # exact_match_missing_item, + # exact_match_additional_item, + # exact_match_changed_item, + # exact_match_multi_nested_list, ] diff --git a/tests/test_type_check.py b/tests/test_type_check.py index cae9854..6489a05 100644 --- a/tests/test_type_check.py +++ b/tests/test_type_check.py @@ -230,9 +230,9 @@ def test_regex_match(filename, check_args, path, expected_result): output=actual_results, expected_output=expected_result ) -range_all_same = ( +operator_all_same = ( "api", - ("range", {"all-same": True}), + ("operator", {"all-same": True}), "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup,vrf,state]", ( {}, #TBD @@ -240,9 +240,9 @@ def test_regex_match(filename, check_args, path, expected_result): ), ) -range_is_equal = ( +operator_is_equal = ( "api", - ("range", {"is-equal": 100}), + ("operator", {"is-equal": 100}), "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesReceived]", ( {}, #TBD @@ -250,9 +250,9 @@ def test_regex_match(filename, check_args, path, expected_result): ), ) -range_not_equal = ( +operator_not_equal = ( "api", - ("range", {"not-equal": "internal"}), + ("operator", {"not-equal": "internal"}), "result[0].vrfs.default.peerList[*].[$peerAddress$,linkType]", ( {}, #TBD @@ -260,9 +260,9 @@ def test_regex_match(filename, check_args, path, expected_result): ), ) -range_contains = ( +operator_contains = ( "api", - ("range", {"contains": "EVPN"}), + ("operator", {"contains": "EVPN"}), "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", ( {}, #TBD @@ -270,9 +270,9 @@ def test_regex_match(filename, check_args, path, expected_result): ), ) -range_not_contains = ( +operator_not_contains = ( "api", - ("range", {"not-contains": "OVERLAY"}), + ("operator", {"not-contains": "OVERLAY"}), "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", ( {}, #TBD @@ -280,9 +280,9 @@ def test_regex_match(filename, check_args, path, expected_result): ), ) -range_is_gt = ( +operator_is_gt = ( "api", - ("range", {"is-gt": 70000000}), + ("operator", {"is-gt": 70000000}), "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", ( {}, #TBD @@ -290,9 +290,9 @@ def test_regex_match(filename, check_args, path, expected_result): ), ) -range_is_lt = ( +operator_is_lt = ( "api", - ("range", {"is-lt": 80000000}), + ("operator", {"is-lt": 80000000}), "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", ( {}, #TBD @@ -300,9 +300,9 @@ def test_regex_match(filename, check_args, path, expected_result): ), ) -range_in_range = ( +operator_in_operator = ( "api", - ("range", {"in-range": (70000000, 80000000)}), + ("operator", {"in-operator": (70000000, 80000000)}), "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", ( {}, #TBD @@ -310,9 +310,9 @@ def test_regex_match(filename, check_args, path, expected_result): ), ) -range_not_range = ( +operator_not_operator = ( "api", - ("range", {"not-range": (70000000, 80000000)}), + ("operator", {"not-range": (70000000, 80000000)}), "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", ( {}, #TBD @@ -320,9 +320,9 @@ def test_regex_match(filename, check_args, path, expected_result): ), ) -range_is_in = ( +operator_is_in = ( "api", - ("range", {"is-in": ("Idle", "Down")}), + ("operator", {"is-in": ("Idle", "Down")}), "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", ( {}, #TBD @@ -330,36 +330,36 @@ def test_regex_match(filename, check_args, path, expected_result): ), ) -range_not_in = ( +operator_not_in = ( "api", - ("range", {"not-in": ("Idle", "Down")}), + ("operator", {"not-in": ("Idle", "Down")}), "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", ( {}, #TBD False, #TBD ), ) -range_all_tests = [ +operator_all_tests = [ # type() == str(), int(), float() - range_all_same, - range_is_equal, - range_not_equal, - range_contains, - range_not_contains, - # type() == int(), float() - range_is_gt, - range_is_lt, - range_in_range, - range_not_range, - # type() == dict() - range_is_in, - range_not_in, + operator_all_same, + # operator_is_equal, + # operator_not_equal, + # operator_contains, + # operator_not_contains, + # # type() == int(), float() + # operator_is_gt, + # operator_is_lt, + # operator_in_operator, + # operator_not_operator, + # # type() == dict() + # operator_is_in, + # operator_not_in, ] -@pytest.mark.parametrize("folder_name, check_args, path, expected_result", range_all_tests) -def test_range(folder_name, check_args, path, expected_result): - """Validate all range check types.""" +@pytest.mark.parametrize("folder_name, check_args, path, expected_result", operator_all_tests) +def test_operator(folder_name, check_args, path, expected_result): + """Validate all operator check types.""" pre_data, post_data = load_mocks(folder_name) check = CheckType.init(*check_args) From fbfe920c1bfe10a9da0debcaba9056562ed423d9 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 10 Feb 2022 11:11:34 +0100 Subject: [PATCH 13/80] draft operator check type --- netcompare/check_types.py | 112 +++++++++++++++++++++++++++-- tests/test_type_checks.py | 147 +++++++++++++++++++++++++++++++++++++- 2 files changed, 254 insertions(+), 5 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index 5631187..c546e63 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -115,7 +115,7 @@ class ExactMatchType(CheckType): """Exact Match class docstring.""" @staticmethod - def validate(**kwargs): + def validate(**kwargs) -> None: """Method to validate arguments.""" # reference_data = getattr(kwargs, "reference_data") @@ -130,7 +130,7 @@ class ToleranceType(CheckType): """Tolerance class docstring.""" @staticmethod - def validate(**kwargs): + def validate(**kwargs) -> None: """Method to validate arguments.""" # reference_data = getattr(kwargs, "reference_data") tolerance = kwargs.get("tolerance") @@ -169,7 +169,7 @@ class ParameterMatchType(CheckType): """Parameter Match class implementation.""" @staticmethod - def validate(**kwargs): + def validate(**kwargs) -> None: """Method to validate arguments.""" mode_options = ["match", "no-match"] params = kwargs.get("params") @@ -198,7 +198,7 @@ class RegexType(CheckType): """Regex Match class implementation.""" @staticmethod - def validate(**kwargs): + def validate(**kwargs) -> None: """Method to validate arguments.""" mode_options = ["match", "no-match"] regex = kwargs.get("regex") @@ -220,3 +220,107 @@ def evaluate(self, value_to_compare: Mapping, regex: str, mode: str) -> Tuple[Ma self.validate(regex=regex, mode=mode) diff = regex_evaluator(value_to_compare, regex, mode) return diff, not diff + + +class OperatorType(CheckType): + """Operator class implementation.""" + + @staticmethod + def validate(**kwargs) -> None: + ins = ( + "is-in", + "not-in", + "in-range", + "not-range" + ) + # ('all-same', ('is-in', + bools = ("all-same",) + numbers = ("is-gt","is-lt") + equals = ("is-equal", "not-equal") + strings = ("contains", "not-contains") + valid_options = ( + bools, + ins, + numbers, + strings, + equals, + ) + + # Validate "params" argument is not None. + try: + params = kwargs['params'] + except KeyError: + raise KeyError(f"'params' argument must be provided. You have {kwargs}. Read the docs for more info.") + + # Validate "params" value is legal. + if not any(params in operator for operator in valid_options): + raise ValueError(f"'params' value must be one of the following: {[sub_element for element in valid_options for sub_element in element]}" ) + + + + + + + + + + # elif parameter_key in iter: + # #"in", "not-in", "in-range", "not-range" requires an iterable + # if not isinstance(parameter_value, list) or not isinstance(parameter_value, tuple): + # raise ValueError(f"Range check-option {iter} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down')") + # # "in-range", "not-range" requires int or floar where value at index 0 is lower than value at index 1 + # if "range" in parameter_key: + # if not (isinstance(parameter_value[0], int) or isinstance(parameter_value[0], float)) and not (isinstance(parameter_value[1], float) or isinstance(parameter_value[1], int)): + # raise ValueError(f"Range check-option {iter} must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000)") + # if not parameter_value[0] < parameter_value[1]: + # raise ValueError(f"'range' and 'not-range' must have value at index 0 lower than value at index 1. i.e: dict(not-range=(70000000, 80000000)") + # else: + # # "is-in", "not-in" requires iterable of strings + # for item in parameter_value.values(): + # if not isinstance(item, str): + # raise ValueError(f"'is-in' and 'not-in' must be an iterable of strings. i.e: dict(is-in=(Idle, Down)") + + # elif parameter_key in numbers: + # if not isinstance(parameter_value, float) or not isinstance(parameter_value, int): + # raise ValueError(f"Range check-option {numbers} must have value of type float or int. i.e: dict(is-lt=80000000)") + + # elif parameter_key in strings: + # if not isinstance(parameter_value, str): + # raise ValueError(f"Range check-option {strings} must have value of type string. i.e: dict(contains='EVPN')") + + + def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple[Mapping, bool]: + """Operator evaluator implementation.""" + + # Assert that check parameters are at index 1. + try: + parameter = value_to_compare[1] + except IndexError as error: + raise IndexError( + f"Evaluating parameter must be defined as dict at index 1. You have: {value_to_compare}" + ) from error + + parameter: list + @validator('parameter') + def parameter_must_be_dict(cls, v): + if not isinstance(v, list): + raise TypeError("check-option must be of type dict().") + return parameter + + # parameter_key = list(parameter.keys())[0] + # parameter_value = list(parameter.values())[0] + + # parameter_key: list + # parameter_value: list + # @validator(parameter_key) + # def check_option_must_be_legal_option(parameter_key): + # if parameter_key not in self.valid_options: + # raise KeyError( + # f"Range check-type requires one of the following check-option: {self.valid_options}" + # ) + + # # Assert data type for each range option. + # if parameter_key in bools: + # # "all-same" requires boolean True or False + # if not isinstance(parameter_value, bool): + # raise ValueError(f"Range check-option {bools} must have value of type bool. i.e: dict(all-same=True)") \ No newline at end of file diff --git a/tests/test_type_checks.py b/tests/test_type_checks.py index c0e9d25..ae49214 100644 --- a/tests/test_type_checks.py +++ b/tests/test_type_checks.py @@ -1,6 +1,6 @@ """Check Type unit tests.""" import pytest -from netcompare.check_types import CheckType, ExactMatchType, ToleranceType, ParameterMatchType, RegexType +from netcompare.check_types import CheckType, ExactMatchType, OperatorType, ToleranceType, ParameterMatchType, RegexType from .utility import load_json_file, load_mocks, ASSERT_FAIL_MESSAGE @@ -43,6 +43,7 @@ def evaluate(self, *args, **kwargs): ("tolerance", ToleranceType), ("parameter_match", ParameterMatchType), ("regex", RegexType), + ("operator", OperatorType) ], ) def test_check_init(check_type_str, expected_class): @@ -355,3 +356,147 @@ def test_regex_match(filename, check_type_str, evaluate_args, path, expected_res assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( output=actual_results, expected_output=expected_result ) + +operator_all_same = ( + "api", + "operator", + {"params", {"all-same": True}}, + "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup,vrf,state]", + ( + {}, #TBD + False, #TBD + ), +) + +# operator_is_equal = ( +# "api", +# "operator", +# ("operator", {"is-equal": 100}), +# "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesReceived]", +# ( +# {}, #TBD +# False, #TBD +# ), +# ) + +# operator_not_equal = ( +# "api", +# ("operator", {"not-equal": "internal"}), +# "result[0].vrfs.default.peerList[*].[$peerAddress$,linkType]", +# ( +# {}, #TBD +# False, #TBD +# ), +# ) + +# operator_contains = ( +# "api", +# ("operator", {"contains": "EVPN"}), +# "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", +# ( +# {}, #TBD +# False, #TBD +# ), +# ) + +# operator_not_contains = ( +# "api", +# ("operator", {"not-contains": "OVERLAY"}), +# "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", +# ( +# {}, #TBD +# False, #TBD +# ), +# ) + +# operator_is_gt = ( +# "api", +# ("operator", {"is-gt": 70000000}), +# "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", +# ( +# {}, #TBD +# False, #TBD +# ), +# ) + +# operator_is_lt = ( +# "api", +# ("operator", {"is-lt": 80000000}), +# "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", +# ( +# {}, #TBD +# False, #TBD +# ), +# ) + +# operator_in_operator = ( +# "api", +# ("operator", {"in-operator": (70000000, 80000000)}), +# "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", +# ( +# {}, #TBD +# False, #TBD +# ), +# ) + +# operator_not_operator = ( +# "api", +# ("operator", {"not-range": (70000000, 80000000)}), +# "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", +# ( +# {}, #TBD +# False, #TBD +# ), +# ) + +# operator_is_in = ( +# "api", +# ("operator", {"is-in": ("Idle", "Down")}), +# "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", +# ( +# {}, #TBD +# False, #TBD +# ), +# ) + +# operator_not_in = ( +# "api", +# ("operator", {"not-in": ("Idle", "Down")}), +# "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", +# ( +# {}, #TBD +# False, #TBD +# ), +# ) +# operator_all_tests = [ +# # type() == str(), int(), float() +# operator_all_same, +# operator_is_equal, +# operator_not_equal, +# operator_contains, +# operator_not_contains, +# # type() == int(), float() +# operator_is_gt, +# operator_is_lt, +# operator_in_operator, +# operator_not_operator, +# # type() == dict() +# operator_is_in, +# operator_not_in, +# ] + + +# @pytest.mark.parametrize("folder_name, check_args, path, expected_result", operator_all_tests) +# def test_operator(folder_name, check_args, path, expected_result): +# """Validate all operator check types.""" +# pre_data, post_data = load_mocks(folder_name) + +# check = CheckType.init(*check_args) +# pre_data, post_data = load_mocks(folder_name) +# pre_value = check.get_value(pre_data, path) +# post_value = check.get_value(post_data, path) +# actual_results = check.evaluate(pre_value, post_value) + +# assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( +# output=actual_results, expected_output=expected_result +# ) \ No newline at end of file From 84c545c5e3367f5d4d03365442688c8c34b0d979 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Mon, 21 Feb 2022 10:00:45 +0100 Subject: [PATCH 14/80] complete validate method for check_type operator --- netcompare/check_types.py | 113 +++++++++++++++----------------------- tests/test_type_checks.py | 62 +++++++++++---------- 2 files changed, 76 insertions(+), 99 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index c546e63..43520bc 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -233,94 +233,69 @@ def validate(**kwargs) -> None: "in-range", "not-range" ) - # ('all-same', ('is-in', bools = ("all-same",) numbers = ("is-gt","is-lt") - equals = ("is-equal", "not-equal") + # "equals" is redundant with check type "exact_match" an "parameter_match" + # equals = ("is-equal", "not-equal") strings = ("contains", "not-contains") valid_options = ( bools, ins, numbers, strings, - equals, + # equals, ) # Validate "params" argument is not None. try: - params = kwargs['params'] + kwargs['params'] except KeyError: raise KeyError(f"'params' argument must be provided. You have {kwargs}. Read the docs for more info.") - # Validate "params" value is legal. - if not any(params in operator for operator in valid_options): - raise ValueError(f"'params' value must be one of the following: {[sub_element for element in valid_options for sub_element in element]}" ) + params_key = kwargs['params'].keys()[0] + params_value = kwargs['params'].values()[0] + # Validate "params" value is legal. + if not any(params_key in operator for operator in valid_options): + raise ValueError(f"'params' value must be one of the following: {[sub_element for element in valid_options for sub_element in element]}") + if params_key in ins: + #"is-in", "not-in", "in-range", "not-range" requires an iterable + if not isinstance(params_value, list) or not isinstance(params_value, tuple): + raise ValueError(f"Range check-option {ins} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down')") + # "in-range", "not-range" requires int or float where value at index 0 is lower than value at index 1 + if params_key in ('in-range', 'not-range'): + if not (isinstance(params_value[0], int) or isinstance(params_value[0], float)) and not (isinstance(params_value[1], float) or isinstance(params_value[1], int)): + raise ValueError(f"Range check-option {params_key} must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000)") + if not params_value[0] < params_value[1]: + raise ValueError(f"'range' and 'not-range' must have value at index 0 lower than value at index 1. i.e: dict(not-range=(70000000, 80000000)") + # "is-in", "not-in" requires iterable of strings + elif params_key in ('is-in', 'not-in'): + for item in params_value: + if not isinstance(item, str): + raise ValueError(f"'is-in' and 'not-in' must be an iterable of strings. i.e: dict(is-in=(Idle, Down)") + + # "is-gt","is-lt" require either int() or float() + elif params_key in numbers: + if not isinstance(params_value, float) or not isinstance(params_value, int): + raise ValueError(f"Range check-option {numbers} must have value of type float or int. i.e: dict(is-lt=80000000)") + + # "contains", "not-contains" require string. + elif params_key in strings: + if not isinstance(params_value, str): + raise ValueError(f"Range check-option {strings} must have value of type string. i.e: dict(contains='EVPN')") + + # "all-same" requires boolean True or False + elif params_key in bools: + if not isinstance(params_value, bool): + raise ValueError(f"Range check-option {bools} must have value of type bool. i.e: dict(all-same=True)") - - - - - # elif parameter_key in iter: - # #"in", "not-in", "in-range", "not-range" requires an iterable - # if not isinstance(parameter_value, list) or not isinstance(parameter_value, tuple): - # raise ValueError(f"Range check-option {iter} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down')") - # # "in-range", "not-range" requires int or floar where value at index 0 is lower than value at index 1 - # if "range" in parameter_key: - # if not (isinstance(parameter_value[0], int) or isinstance(parameter_value[0], float)) and not (isinstance(parameter_value[1], float) or isinstance(parameter_value[1], int)): - # raise ValueError(f"Range check-option {iter} must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000)") - # if not parameter_value[0] < parameter_value[1]: - # raise ValueError(f"'range' and 'not-range' must have value at index 0 lower than value at index 1. i.e: dict(not-range=(70000000, 80000000)") - # else: - # # "is-in", "not-in" requires iterable of strings - # for item in parameter_value.values(): - # if not isinstance(item, str): - # raise ValueError(f"'is-in' and 'not-in' must be an iterable of strings. i.e: dict(is-in=(Idle, Down)") - - # elif parameter_key in numbers: - # if not isinstance(parameter_value, float) or not isinstance(parameter_value, int): - # raise ValueError(f"Range check-option {numbers} must have value of type float or int. i.e: dict(is-lt=80000000)") - - # elif parameter_key in strings: - # if not isinstance(parameter_value, str): - # raise ValueError(f"Range check-option {strings} must have value of type string. i.e: dict(contains='EVPN')") - - - def evaluate(self, reference_value: Mapping, value_to_compare: Mapping) -> Tuple[Mapping, bool]: + def evaluate(self, value_to_compare: Any, reference_data: Any, params: Any) -> Tuple[Mapping, bool]: """Operator evaluator implementation.""" - # Assert that check parameters are at index 1. - try: - parameter = value_to_compare[1] - except IndexError as error: - raise IndexError( - f"Evaluating parameter must be defined as dict at index 1. You have: {value_to_compare}" - ) from error - - parameter: list - @validator('parameter') - def parameter_must_be_dict(cls, v): - if not isinstance(v, list): - raise TypeError("check-option must be of type dict().") - return parameter - - # parameter_key = list(parameter.keys())[0] - # parameter_value = list(parameter.values())[0] - - # parameter_key: list - # parameter_value: list - # @validator(parameter_key) - # def check_option_must_be_legal_option(parameter_key): - # if parameter_key not in self.valid_options: - # raise KeyError( - # f"Range check-type requires one of the following check-option: {self.valid_options}" - # ) - - # # Assert data type for each range option. - # if parameter_key in bools: - # # "all-same" requires boolean True or False - # if not isinstance(parameter_value, bool): - # raise ValueError(f"Range check-option {bools} must have value of type bool. i.e: dict(all-same=True)") \ No newline at end of file + self.validate(params=params) + evaluation_result = diff_generator(reference_data, value_to_compare, params) + return evaluation_result, not evaluation_result + diff --git a/tests/test_type_checks.py b/tests/test_type_checks.py index ae49214..d7f7a09 100644 --- a/tests/test_type_checks.py +++ b/tests/test_type_checks.py @@ -360,7 +360,7 @@ def test_regex_match(filename, check_type_str, evaluate_args, path, expected_res operator_all_same = ( "api", "operator", - {"params", {"all-same": True}}, + {"params": {"all-same": True}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup,vrf,state]", ( {}, #TBD @@ -368,6 +368,37 @@ def test_regex_match(filename, check_type_str, evaluate_args, path, expected_res ), ) +operator_all_tests = [ +# # type() == str(), int(), float() + operator_all_same, +# operator_is_equal, +# operator_not_equal, +# operator_contains, +# operator_not_contains, +# # type() == int(), float() +# operator_is_gt, +# operator_is_lt, +# operator_in_operator, +# operator_not_operator, +# # type() == dict() +# operator_is_in, +# operator_not_in, +] + +@pytest.mark.parametrize("filename, check_type_str, evaluate_args, path, expected_result", operator_all_tests) +def test_operator(folder_name, check_args, path, expected_result): + """Validate all operator check types.""" + pre_data, post_data = load_mocks(folder_name) + + check = CheckType.init(*check_args) + pre_data, post_data = load_mocks(folder_name) + pre_value = check.get_value(pre_data, path) + post_value = check.get_value(post_data, path) + actual_results = check.evaluate(pre_value, post_value) + + assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( + output=actual_results, expected_output=expected_result + ) # operator_is_equal = ( # "api", # "operator", @@ -468,35 +499,6 @@ def test_regex_match(filename, check_type_str, evaluate_args, path, expected_res # False, #TBD # ), # ) -# operator_all_tests = [ -# # type() == str(), int(), float() -# operator_all_same, -# operator_is_equal, -# operator_not_equal, -# operator_contains, -# operator_not_contains, -# # type() == int(), float() -# operator_is_gt, -# operator_is_lt, -# operator_in_operator, -# operator_not_operator, -# # type() == dict() -# operator_is_in, -# operator_not_in, -# ] - -# @pytest.mark.parametrize("folder_name, check_args, path, expected_result", operator_all_tests) -# def test_operator(folder_name, check_args, path, expected_result): -# """Validate all operator check types.""" -# pre_data, post_data = load_mocks(folder_name) -# check = CheckType.init(*check_args) -# pre_data, post_data = load_mocks(folder_name) -# pre_value = check.get_value(pre_data, path) -# post_value = check.get_value(post_data, path) -# actual_results = check.evaluate(pre_value, post_value) -# assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( -# output=actual_results, expected_output=expected_result -# ) \ No newline at end of file From abbb498935bc9b2c6ff7415458c2e28e4439ae02 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Tue, 22 Feb 2022 10:03:58 +0100 Subject: [PATCH 15/80] work on oeprator_evaluator method --- netcompare/check_types.py | 31 ++--- netcompare/evaluators.py | 6 + tests/test_type_checks.py | 239 ++++++++++++++++++-------------------- 3 files changed, 133 insertions(+), 143 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index 43520bc..ea8e711 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -1,5 +1,6 @@ """CheckType Implementation.""" import re +import pdb from typing import Mapping, Tuple, List, Dict, Any, Union from abc import ABC, abstractmethod import jmespath @@ -13,7 +14,7 @@ keys_values_zipper, ) from .utils.data_normalization import exclude_filter, flatten_list -from .evaluators import diff_generator, parameter_evaluator, regex_evaluator +from .evaluators import diff_generator, parameter_evaluator, regex_evaluator, operator_evaluator # pylint: disable=arguments-differ @@ -30,12 +31,14 @@ def init(check_type: str): """ if check_type == "exact_match": return ExactMatchType() - if check_type == "tolerance": + elif check_type == "tolerance": return ToleranceType() - if check_type == "parameter_match": + elif check_type == "parameter_match": return ParameterMatchType() - if check_type == "regex": + elif check_type == "regex": return RegexType() + elif check_type == "operator": + return OperatorType() raise NotImplementedError @@ -247,17 +250,14 @@ def validate(**kwargs) -> None: ) # Validate "params" argument is not None. - try: - kwargs['params'] - except KeyError: + if not kwargs: raise KeyError(f"'params' argument must be provided. You have {kwargs}. Read the docs for more info.") - params_key = kwargs['params'].keys()[0] - params_value = kwargs['params'].values()[0] - + params_key = list(kwargs.keys())[0] + params_value = list(kwargs.values())[0] # Validate "params" value is legal. if not any(params_key in operator for operator in valid_options): - raise ValueError(f"'params' value must be one of the following: {[sub_element for element in valid_options for sub_element in element]}") + raise ValueError(f"'params' value must be one of the following: {[sub_element for element in valid_options for sub_element in element]}. You have: {params_key}") if params_key in ins: #"is-in", "not-in", "in-range", "not-range" requires an iterable @@ -292,10 +292,11 @@ def validate(**kwargs) -> None: raise ValueError(f"Range check-option {bools} must have value of type bool. i.e: dict(all-same=True)") - def evaluate(self, value_to_compare: Any, reference_data: Any, params: Any) -> Tuple[Mapping, bool]: + def evaluate(self, value_to_compare: Any, params: Any) -> Tuple[Mapping, bool]: """Operator evaluator implementation.""" - # Assert that check parameters are at index 1. - self.validate(params=params) - evaluation_result = diff_generator(reference_data, value_to_compare, params) + self.validate(**params) + # For naming consistency + reference_data=params + evaluation_result = operator_evaluator(reference_data, value_to_compare) return evaluation_result, not evaluation_result diff --git a/netcompare/evaluators.py b/netcompare/evaluators.py index 71a5363..9f6d7cd 100644 --- a/netcompare/evaluators.py +++ b/netcompare/evaluators.py @@ -1,5 +1,6 @@ """Evaluators.""" import re +import pdb from typing import Any, Mapping, Dict from deepdiff import DeepDiff from .utils.diff_helpers import get_diff_iterables_items, fix_deepdiff_key_names @@ -99,3 +100,8 @@ def regex_evaluator(values: Mapping, regex_expression: str, mode: str) -> Dict: result.update(item) return result + + +def operator_evaluator(referance_data: Mapping, value_to_compare: Mapping) -> Dict: + pdb.set_trace() + pass diff --git a/tests/test_type_checks.py b/tests/test_type_checks.py index d7f7a09..52568cc 100644 --- a/tests/test_type_checks.py +++ b/tests/test_type_checks.py @@ -2,7 +2,7 @@ import pytest from netcompare.check_types import CheckType, ExactMatchType, OperatorType, ToleranceType, ParameterMatchType, RegexType from .utility import load_json_file, load_mocks, ASSERT_FAIL_MESSAGE - +import pdb def test_child_class_raises_exception(): """Tests that exception is raised for child class when abstract methods are not implemented.""" @@ -54,8 +54,6 @@ def test_check_init(check_type_str, expected_class): exception_tests_init = [ ("does_not_exist", NotImplementedError, ""), ] - - @pytest.mark.parametrize("check_type_str, exception_type, expected_in_output", exception_tests_init) def tests_exceptions_init(check_type_str, exception_type, expected_in_output): """Tests exceptions when check object is initialized.""" @@ -78,8 +76,6 @@ def tests_exceptions_init(check_type_str, exception_type, expected_in_output): "Mode argument should be", ), ] - - @pytest.mark.parametrize("check_type_str, evaluate_args, exception_type, expected_in_output", exception_tests_eval) def tests_exceptions_eval(check_type_str, evaluate_args, exception_type, expected_in_output): """Tests exceptions when calling .evaluate() method.""" @@ -96,7 +92,6 @@ def tests_exceptions_eval(check_type_str, evaluate_args, exception_type, expecte "result[0].vrfs.default.peerList[*].[$peerAddress$,establishedTransitions]", ({}, True), ) - exact_match_test_values_changed = ( "exact_match", {}, @@ -110,7 +105,6 @@ def tests_exceptions_eval(check_type_str, evaluate_args, exception_type, expecte False, ), ) - tolerance_test_values_no_change = ( "tolerance", {"tolerance": 10}, @@ -118,7 +112,6 @@ def tests_exceptions_eval(check_type_str, evaluate_args, exception_type, expecte "result[0].vrfs.default.peerList[*].[$peerAddress$,establishedTransitions]", ({}, True), ) - tolerance_test_values_within_threshold = ( "tolerance", {"tolerance": 10}, @@ -126,7 +119,6 @@ def tests_exceptions_eval(check_type_str, evaluate_args, exception_type, expecte "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ({}, True), ) - tolerance_test_values_beyond_threshold = ( "tolerance", {"tolerance": 10}, @@ -140,7 +132,6 @@ def tests_exceptions_eval(check_type_str, evaluate_args, exception_type, expecte False, ), ) - check_type_tests = [ exact_match_test_values_no_change, exact_match_test_values_changed, @@ -148,8 +139,6 @@ def tests_exceptions_eval(check_type_str, evaluate_args, exception_type, expecte tolerance_test_values_within_threshold, tolerance_test_values_beyond_threshold, ] - - @pytest.mark.parametrize("check_type_str, evaluate_args, folder_name, path, expected_results", check_type_tests) def test_check_type_results(check_type_str, evaluate_args, folder_name, path, expected_results): """Validate that CheckType.evaluate returns the expected_results.""" @@ -178,7 +167,6 @@ def test_check_type_results(check_type_str, evaluate_args, folder_name, path, ex False, ), ) - napalm_bgp_neighbor_prefixes_ipv4 = ( "napalm_get_bgp_neighbors", "tolerance", @@ -186,7 +174,6 @@ def test_check_type_results(check_type_str, evaluate_args, folder_name, path, ex "global.$peers$.*.*.ipv4.[accepted_prefixes,received_prefixes,sent_prefixes]", ({"10.1.0.0": {"accepted_prefixes": {"new_value": 900, "old_value": 1000}}}, False), ) - napalm_bgp_neighbor_prefixes_ipv6 = ( "napalm_get_bgp_neighbors", "tolerance", @@ -194,7 +181,6 @@ def test_check_type_results(check_type_str, evaluate_args, folder_name, path, ex "global.$peers$.*.*.ipv6.[accepted_prefixes,received_prefixes,sent_prefixes]", ({"10.64.207.255": {"received_prefixes": {"new_value": 1100, "old_value": 1000}}}, False), ) - napalm_get_lldp_neighbors_exact_raw = ( "napalm_get_lldp_neighbors", "exact_match", @@ -216,7 +202,6 @@ def test_check_type_results(check_type_str, evaluate_args, folder_name, path, ex False, ), ) - tolerance_no_path = ( "tolerance", "tolerance", @@ -358,7 +343,7 @@ def test_regex_match(filename, check_type_str, evaluate_args, path, expected_res ) operator_all_same = ( - "api", + "pre.json", "operator", {"params": {"all-same": True}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup,vrf,state]", @@ -367,7 +352,6 @@ def test_regex_match(filename, check_type_str, evaluate_args, path, expected_res False, #TBD ), ) - operator_all_tests = [ # # type() == str(), int(), float() operator_all_same, @@ -386,119 +370,118 @@ def test_regex_match(filename, check_type_str, evaluate_args, path, expected_res ] @pytest.mark.parametrize("filename, check_type_str, evaluate_args, path, expected_result", operator_all_tests) -def test_operator(folder_name, check_args, path, expected_result): +def test_operator(filename, check_type_str, evaluate_args, path, expected_result): """Validate all operator check types.""" - pre_data, post_data = load_mocks(folder_name) - - check = CheckType.init(*check_args) - pre_data, post_data = load_mocks(folder_name) - pre_value = check.get_value(pre_data, path) - post_value = check.get_value(post_data, path) - actual_results = check.evaluate(pre_value, post_value) - + check = CheckType.init(check_type_str) + # There is not concept of "pre" and "post" in operator. + data = load_json_file("api", filename) + value = check.get_value(data, path) + actual_results = check.evaluate(value, **evaluate_args) assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( - output=actual_results, expected_output=expected_result + output=actual_results, + expected_output=expected_result ) -# operator_is_equal = ( -# "api", -# "operator", -# ("operator", {"is-equal": 100}), -# "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesReceived]", -# ( -# {}, #TBD -# False, #TBD -# ), -# ) - -# operator_not_equal = ( -# "api", -# ("operator", {"not-equal": "internal"}), -# "result[0].vrfs.default.peerList[*].[$peerAddress$,linkType]", -# ( -# {}, #TBD -# False, #TBD -# ), -# ) - -# operator_contains = ( -# "api", -# ("operator", {"contains": "EVPN"}), -# "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", -# ( -# {}, #TBD -# False, #TBD -# ), -# ) - -# operator_not_contains = ( -# "api", -# ("operator", {"not-contains": "OVERLAY"}), -# "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", -# ( -# {}, #TBD -# False, #TBD -# ), -# ) - -# operator_is_gt = ( -# "api", -# ("operator", {"is-gt": 70000000}), -# "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", -# ( -# {}, #TBD -# False, #TBD -# ), -# ) - -# operator_is_lt = ( -# "api", -# ("operator", {"is-lt": 80000000}), -# "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", -# ( -# {}, #TBD -# False, #TBD -# ), -# ) - -# operator_in_operator = ( -# "api", -# ("operator", {"in-operator": (70000000, 80000000)}), -# "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", -# ( -# {}, #TBD -# False, #TBD -# ), -# ) - -# operator_not_operator = ( -# "api", -# ("operator", {"not-range": (70000000, 80000000)}), -# "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", -# ( -# {}, #TBD -# False, #TBD -# ), -# ) - -# operator_is_in = ( -# "api", -# ("operator", {"is-in": ("Idle", "Down")}), -# "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", -# ( -# {}, #TBD -# False, #TBD -# ), -# ) - -# operator_not_in = ( -# "api", -# ("operator", {"not-in": ("Idle", "Down")}), -# "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", -# ( -# {}, #TBD -# False, #TBD -# ), -# ) + +# # operator_is_equal = ( +# # "api", +# # "operator", +# # ("operator", {"is-equal": 100}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesReceived]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_not_equal = ( +# # "api", +# # ("operator", {"not-equal": "internal"}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,linkType]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_contains = ( +# # "api", +# # ("operator", {"contains": "EVPN"}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_not_contains = ( +# # "api", +# # ("operator", {"not-contains": "OVERLAY"}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_is_gt = ( +# # "api", +# # ("operator", {"is-gt": 70000000}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_is_lt = ( +# # "api", +# # ("operator", {"is-lt": 80000000}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_in_operator = ( +# # "api", +# # ("operator", {"in-operator": (70000000, 80000000)}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_not_operator = ( +# # "api", +# # ("operator", {"not-range": (70000000, 80000000)}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_is_in = ( +# # "api", +# # ("operator", {"is-in": ("Idle", "Down")}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_not_in = ( +# # "api", +# # ("operator", {"not-in": ("Idle", "Down")}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) From e05432379b934d161866d0e6da51cc4ac1d025af Mon Sep 17 00:00:00 2001 From: Network to Code Date: Wed, 23 Feb 2022 10:30:16 +0100 Subject: [PATCH 16/80] drat operator all-same --- netcompare/evaluators.py | 14 +++++++++---- netcompare/operator.py | 45 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 netcompare/operator.py diff --git a/netcompare/evaluators.py b/netcompare/evaluators.py index 9f6d7cd..54ab4de 100644 --- a/netcompare/evaluators.py +++ b/netcompare/evaluators.py @@ -1,10 +1,11 @@ """Evaluators.""" +import imp import re import pdb from typing import Any, Mapping, Dict from deepdiff import DeepDiff -from .utils.diff_helpers import get_diff_iterables_items, fix_deepdiff_key_names - +from .utils.diff_helpers import get_diff_iterables_items, fix_deepdiff_key_name +from operator import Operator def diff_generator(pre_result: Any, post_result: Any) -> Dict: """Generates diff between pre and post data based on check definition. @@ -103,5 +104,10 @@ def regex_evaluator(values: Mapping, regex_expression: str, mode: str) -> Dict: def operator_evaluator(referance_data: Mapping, value_to_compare: Mapping) -> Dict: - pdb.set_trace() - pass + # referance_data + # {'all-same': True} + operator_type = list(referance_data.keys())[0].replace('-', '_') + operator = Operator(referance_data, value_to_compare) + + getattr(operator, operator_type)() + diff --git a/netcompare/operator.py b/netcompare/operator.py new file mode 100644 index 0000000..88ebe6f --- /dev/null +++ b/netcompare/operator.py @@ -0,0 +1,45 @@ +from unittest import result + + +class Operator(): + + def __init__(self, referance_data, value_to_compare) -> None: + + self.referance_data_type = list(referance_data.keys())[0] + self.referance_data_value = list(referance_data.values())[0] + self.value_to_compare = value_to_compare + + + def all_same(self): + + # [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, + # {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}] + + index_key = dict() + list_of_values = list() + result = list() + + for number, item in enumerate(self.value_to_compare): + for key, value in item.keys(): + # Create a mapping between index and key: i.e. {'1': '7.7.7.7', '2': '10.1.0.0'}. We will later use to build the result. + index_key[number] = key + # Create a list for compare valiues. + list_of_values.append(value) + + for number,element in enumerate(list_of_values): + if not element == list_of_values[0]: + result.append({index_key[number]: element}) + + if self.referance_data_value and result: + return (False, result) + elif self.referance_data_value and not result: + return (True, result) + elif not self.referance_data_value and result: + return (True, result) + elif not self.referance_data_value and not result: + return (False, result) + + + + else: + pass From 0e01f34e2fe9eab8d8b649054c88f9ab31d1bbba Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 24 Feb 2022 10:19:52 +0100 Subject: [PATCH 17/80] add all-same operator --- netcompare/evaluators.py | 9 +- netcompare/operator.py | 46 +++++----- tests/test_operators.py | 179 ++++++++++++++++++++++++++++++++++++++ tests/test_type_checks.py | 144 ------------------------------ 4 files changed, 204 insertions(+), 174 deletions(-) create mode 100644 tests/test_operators.py diff --git a/netcompare/evaluators.py b/netcompare/evaluators.py index 54ab4de..ad77655 100644 --- a/netcompare/evaluators.py +++ b/netcompare/evaluators.py @@ -1,11 +1,11 @@ """Evaluators.""" -import imp import re + import pdb from typing import Any, Mapping, Dict from deepdiff import DeepDiff -from .utils.diff_helpers import get_diff_iterables_items, fix_deepdiff_key_name -from operator import Operator +from .utils.diff_helpers import get_diff_iterables_items +from .operator import Operator def diff_generator(pre_result: Any, post_result: Any) -> Dict: """Generates diff between pre and post data based on check definition. @@ -109,5 +109,6 @@ def operator_evaluator(referance_data: Mapping, value_to_compare: Mapping) -> Di operator_type = list(referance_data.keys())[0].replace('-', '_') operator = Operator(referance_data, value_to_compare) - getattr(operator, operator_type)() + result = getattr(operator, operator_type)() + return result diff --git a/netcompare/operator.py b/netcompare/operator.py index 88ebe6f..d27c7bb 100644 --- a/netcompare/operator.py +++ b/netcompare/operator.py @@ -1,6 +1,5 @@ -from unittest import result - - +import pdb +from collections import defaultdict class Operator(): def __init__(self, referance_data, value_to_compare) -> None: @@ -11,35 +10,30 @@ def __init__(self, referance_data, value_to_compare) -> None: def all_same(self): - # [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, - # {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}] - - index_key = dict() + # {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, + # {'10.2.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, + # {'10.64.207.255': {'peerGroup': 'IPv4-UNDERLAY-MLAG-PEER', 'vrf': 'default', 'state': 'Idle'}}] list_of_values = list() result = list() - for number, item in enumerate(self.value_to_compare): - for key, value in item.keys(): - # Create a mapping between index and key: i.e. {'1': '7.7.7.7', '2': '10.1.0.0'}. We will later use to build the result. - index_key[number] = key + for item in self.value_to_compare: + for value in item.values(): # Create a list for compare valiues. list_of_values.append(value) - for number,element in enumerate(list_of_values): + for element in list_of_values: if not element == list_of_values[0]: - result.append({index_key[number]: element}) - - if self.referance_data_value and result: - return (False, result) - elif self.referance_data_value and not result: - return (True, result) - elif not self.referance_data_value and result: - return (True, result) - elif not self.referance_data_value and not result: - return (False, result) - - + result.append(False) + else: + result.append(True) + + if self.referance_data_value and not all(result): + return (False, self.value_to_compare) + elif self.referance_data_value and all(result): + return (True, self.value_to_compare) + elif not self.referance_data_value and not all(result): + return (True, self.value_to_compare) + elif not self.referance_data_value and all(result): + return (False, self.value_to_compare) - else: - pass diff --git a/tests/test_operators.py b/tests/test_operators.py new file mode 100644 index 0000000..0460de6 --- /dev/null +++ b/tests/test_operators.py @@ -0,0 +1,179 @@ +import pytest +from netcompare.check_types import CheckType, ExactMatchType, OperatorType, ToleranceType, ParameterMatchType, RegexType +from .utility import load_json_file, ASSERT_FAIL_MESSAGE +import pdb + +operator_all_same = ( + "pre.json", + "operator", + {"params": {"all-same": True}}, + "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup,vrf,state]", + ( + ( + False, + [ + { + "7.7.7.7": { + "peerGroup": "EVPN-OVERLAY-SPINE", + "state": "Idle", + "vrf": "default", + } + }, + { + "10.1.0.0": { + "peerGroup": "IPv4-UNDERLAY-SPINE", + "state": "Idle", + "vrf": "default", + } + }, + { + "10.2.0.0": { + "peerGroup": "IPv4-UNDERLAY-SPINE", + "state": "Idle", + "vrf": "default", + } + }, + { + "10.64.207.255": { + "peerGroup": "IPv4-UNDERLAY-MLAG-PEER", + "state": "Idle", + "vrf": "default", + } + }, + ], + ), + False, + ), +) +operator_all_tests = [ + operator_all_same, +# operator_is_equal, +# operator_not_equal, +# operator_contains, +# operator_not_contains, +# # type() == int(), float() +# operator_is_gt, +# operator_is_lt, +# operator_in_operator, +# operator_not_operator, +# # type() == dict() +# operator_is_in, +# operator_not_in, +] + +@pytest.mark.parametrize("filename, check_type_str, evaluate_args, path, expected_result", operator_all_tests) +def test_operator(filename, check_type_str, evaluate_args, path, expected_result): + """Validate all operator check types.""" + check = CheckType.init(check_type_str) + # There is not concept of "pre" and "post" in operator. + data = load_json_file("api", filename) + value = check.get_value(data, path) + actual_results = check.evaluate(value, **evaluate_args) + assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( + output=actual_results, + expected_output=expected_result + ) + +# # operator_is_equal = ( +# # "api", +# # "operator", +# # ("operator", {"is-equal": 100}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesReceived]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_not_equal = ( +# # "api", +# # ("operator", {"not-equal": "internal"}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,linkType]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_contains = ( +# # "api", +# # ("operator", {"contains": "EVPN"}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_not_contains = ( +# # "api", +# # ("operator", {"not-contains": "OVERLAY"}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_is_gt = ( +# # "api", +# # ("operator", {"is-gt": 70000000}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_is_lt = ( +# # "api", +# # ("operator", {"is-lt": 80000000}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_in_operator = ( +# # "api", +# # ("operator", {"in-operator": (70000000, 80000000)}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_not_operator = ( +# # "api", +# # ("operator", {"not-range": (70000000, 80000000)}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_is_in = ( +# # "api", +# # ("operator", {"is-in": ("Idle", "Down")}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + +# # operator_not_in = ( +# # "api", +# # ("operator", {"not-in": ("Idle", "Down")}), +# # "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", +# # ( +# # {}, #TBD +# # False, #TBD +# # ), +# # ) + + + diff --git a/tests/test_type_checks.py b/tests/test_type_checks.py index 52568cc..e9e0caa 100644 --- a/tests/test_type_checks.py +++ b/tests/test_type_checks.py @@ -341,147 +341,3 @@ def test_regex_match(filename, check_type_str, evaluate_args, path, expected_res assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( output=actual_results, expected_output=expected_result ) - -operator_all_same = ( - "pre.json", - "operator", - {"params": {"all-same": True}}, - "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup,vrf,state]", - ( - {}, #TBD - False, #TBD - ), -) -operator_all_tests = [ -# # type() == str(), int(), float() - operator_all_same, -# operator_is_equal, -# operator_not_equal, -# operator_contains, -# operator_not_contains, -# # type() == int(), float() -# operator_is_gt, -# operator_is_lt, -# operator_in_operator, -# operator_not_operator, -# # type() == dict() -# operator_is_in, -# operator_not_in, -] - -@pytest.mark.parametrize("filename, check_type_str, evaluate_args, path, expected_result", operator_all_tests) -def test_operator(filename, check_type_str, evaluate_args, path, expected_result): - """Validate all operator check types.""" - check = CheckType.init(check_type_str) - # There is not concept of "pre" and "post" in operator. - data = load_json_file("api", filename) - value = check.get_value(data, path) - actual_results = check.evaluate(value, **evaluate_args) - assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( - output=actual_results, - expected_output=expected_result - ) - -# # operator_is_equal = ( -# # "api", -# # "operator", -# # ("operator", {"is-equal": 100}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesReceived]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_not_equal = ( -# # "api", -# # ("operator", {"not-equal": "internal"}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,linkType]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_contains = ( -# # "api", -# # ("operator", {"contains": "EVPN"}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_not_contains = ( -# # "api", -# # ("operator", {"not-contains": "OVERLAY"}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_is_gt = ( -# # "api", -# # ("operator", {"is-gt": 70000000}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_is_lt = ( -# # "api", -# # ("operator", {"is-lt": 80000000}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_in_operator = ( -# # "api", -# # ("operator", {"in-operator": (70000000, 80000000)}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_not_operator = ( -# # "api", -# # ("operator", {"not-range": (70000000, 80000000)}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_is_in = ( -# # "api", -# # ("operator", {"is-in": ("Idle", "Down")}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_not_in = ( -# # "api", -# # ("operator", {"not-in": ("Idle", "Down")}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - - - From cb7e507cbc9bbdcde1e1d625a5c4d8707a57ab42 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Fri, 25 Feb 2022 10:07:33 +0100 Subject: [PATCH 18/80] add contains operator --- netcompare/operator.py | 20 ++++++++++++++++++-- tests/test_operators.py | 33 +++++++++++++++++++++------------ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/netcompare/operator.py b/netcompare/operator.py index d27c7bb..4e412c6 100644 --- a/netcompare/operator.py +++ b/netcompare/operator.py @@ -3,8 +3,6 @@ class Operator(): def __init__(self, referance_data, value_to_compare) -> None: - - self.referance_data_type = list(referance_data.keys())[0] self.referance_data_value = list(referance_data.values())[0] self.value_to_compare = value_to_compare @@ -37,3 +35,21 @@ def all_same(self): elif not self.referance_data_value and all(result): return (False, self.value_to_compare) + + def contains(self): + result = list() + for item in self.value_to_compare: + for value in item.values(): + for evaluated_value in value.values(): + if self.referance_data_value not in evaluated_value: + # Create a list for compare valiues. + result.append(item) + + if result: + return (False, result) + else: + return (True, result) + + def not_contains(self): + pass + diff --git a/tests/test_operators.py b/tests/test_operators.py index 0460de6..679d807 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -45,11 +45,28 @@ False, ), ) +operator_contains = ( + "pre.json", + "operator", + {"params": {"contains": "EVPN"}}, + "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", + ( + ( + False, + [ + {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE'}}, + {'10.2.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE'}}, + {'10.64.207.255': {'peerGroup': 'IPv4-UNDERLAY-MLAG-PEER'}} + ] + ), + False + ) +) + + operator_all_tests = [ operator_all_same, -# operator_is_equal, -# operator_not_equal, -# operator_contains, + operator_contains, # operator_not_contains, # # type() == int(), float() # operator_is_gt, @@ -95,15 +112,7 @@ def test_operator(filename, check_type_str, evaluate_args, path, expected_result # # ), # # ) -# # operator_contains = ( -# # "api", -# # ("operator", {"contains": "EVPN"}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) + # # operator_not_contains = ( # # "api", From 03005eccbb1bd0d9251c8325e21dfffed0e70a8d Mon Sep 17 00:00:00 2001 From: Network to Code Date: Mon, 28 Feb 2022 09:27:11 +0100 Subject: [PATCH 19/80] add not-contains. Update contains logic --- netcompare/operator.py | 22 +++++++++++++++++----- tests/test_operators.py | 19 +++++++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/netcompare/operator.py b/netcompare/operator.py index 4e412c6..89bdf44 100644 --- a/netcompare/operator.py +++ b/netcompare/operator.py @@ -41,15 +41,27 @@ def contains(self): for item in self.value_to_compare: for value in item.values(): for evaluated_value in value.values(): - if self.referance_data_value not in evaluated_value: - # Create a list for compare valiues. + if self.referance_data_value in evaluated_value: + # Create a list for compare valiues. result.append(item) if result: - return (False, result) - else: return (True, result) + else: + return (False, result) + def not_contains(self): - pass + result = list() + for item in self.value_to_compare: + for value in item.values(): + for evaluated_value in value.values(): + if self.referance_data_value not in evaluated_value: + # Create a list for compare valiues. + result.append(item) + + if result: + return (True, result) + else: + return (False, result) diff --git a/tests/test_operators.py b/tests/test_operators.py index 679d807..7e78ba0 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -52,7 +52,20 @@ "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", ( ( - False, + True, + [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE'}}] + ), + False + ) +) +operator_not_contains = ( + "pre.json", + "operator", + {"params": {"not-contains": "EVPN"}}, + "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", + ( + ( + True, [ {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE'}}, {'10.2.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE'}}, @@ -63,12 +76,10 @@ ) ) - operator_all_tests = [ operator_all_same, operator_contains, -# operator_not_contains, -# # type() == int(), float() + operator_not_contains, # operator_is_gt, # operator_is_lt, # operator_in_operator, From 18bcf7551a876cfa9798422a7da4d44cc7efcff9 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Mon, 28 Feb 2022 09:51:35 +0100 Subject: [PATCH 20/80] add is-gt and is-lt operators --- netcompare/check_types.py | 8 ++++---- netcompare/operator.py | 37 +++++++++++++++++++++++++++++------- tests/test_operators.py | 40 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index ea8e711..a91b2e4 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -274,12 +274,12 @@ def validate(**kwargs) -> None: elif params_key in ('is-in', 'not-in'): for item in params_value: if not isinstance(item, str): - raise ValueError(f"'is-in' and 'not-in' must be an iterable of strings. i.e: dict(is-in=(Idle, Down)") + raise ValueError(f"'is-in' and 'not-in' must be an iterable of strings. i.e: dict(is-in=(Idle, Down). You have: {item} of type {type(item)}") # "is-gt","is-lt" require either int() or float() elif params_key in numbers: - if not isinstance(params_value, float) or not isinstance(params_value, int): - raise ValueError(f"Range check-option {numbers} must have value of type float or int. i.e: dict(is-lt=80000000)") + if not isinstance(params_value, float) and not isinstance(params_value, int): + raise ValueError(f"Check-option {numbers} must have value of type float or int. i.e: dict(is-lt=50). You have: {params_value} of type {type(params_value)}") # "contains", "not-contains" require string. elif params_key in strings: @@ -289,7 +289,7 @@ def validate(**kwargs) -> None: # "all-same" requires boolean True or False elif params_key in bools: if not isinstance(params_value, bool): - raise ValueError(f"Range check-option {bools} must have value of type bool. i.e: dict(all-same=True)") + raise ValueError(f"Range check-option {bools} must have value of type bool. i.e: dict(all-same=True). You have: {params_value} of type {type(params_value)}") def evaluate(self, value_to_compare: Any, params: Any) -> Tuple[Mapping, bool]: diff --git a/netcompare/operator.py b/netcompare/operator.py index 89bdf44..5c1b1d3 100644 --- a/netcompare/operator.py +++ b/netcompare/operator.py @@ -1,17 +1,15 @@ -import pdb -from collections import defaultdict class Operator(): def __init__(self, referance_data, value_to_compare) -> None: + # [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, + # {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, + # {'10.2.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, + # {'10.64.207.255': {'peerGroup': 'IPv4-UNDERLAY-MLAG-PEER', 'vrf': 'default', 'state': 'Idle'}}] self.referance_data_value = list(referance_data.values())[0] self.value_to_compare = value_to_compare def all_same(self): - # [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, - # {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, - # {'10.2.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, - # {'10.64.207.255': {'peerGroup': 'IPv4-UNDERLAY-MLAG-PEER', 'vrf': 'default', 'state': 'Idle'}}] list_of_values = list() result = list() @@ -44,7 +42,6 @@ def contains(self): if self.referance_data_value in evaluated_value: # Create a list for compare valiues. result.append(item) - if result: return (True, result) else: @@ -59,9 +56,35 @@ def not_contains(self): if self.referance_data_value not in evaluated_value: # Create a list for compare valiues. result.append(item) + if result: + return (True, result) + else: + return (False, result) + + def is_gt(self): + result = list() + for item in self.value_to_compare: + for value in item.values(): + for evaluated_value in value.values(): + if evaluated_value > self.referance_data_value: + # Create a list for compare valiues. + result.append(item) if result: return (True, result) else: return (False, result) + + def is_lt(self): + result = list() + for item in self.value_to_compare: + for value in item.values(): + for evaluated_value in value.values(): + if evaluated_value < self.referance_data_value: + # Create a list for compare valiues. + result.append(item) + if result: + return (True, result) + else: + return (False, result) diff --git a/tests/test_operators.py b/tests/test_operators.py index 7e78ba0..13c4387 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -75,13 +75,49 @@ False ) ) +operator_is_gt = ( + "pre.json", + "operator", + {"params": {"is-gt": 20}}, + "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", + ( + ( + True, + [ + {'7.7.7.7': {'prefixesSent': 50}}, + {'10.1.0.0': {'prefixesSent': 50}}, + {'10.2.0.0': {'prefixesSent': 50}}, + {'10.64.207.255': {'prefixesSent': 50}} + ], + ), + False + ) +) +operator_is_lt = ( + "pre.json", + "operator", + {"params": {"is-lt": 60}}, + "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", + ( + ( + True, + [ + {'7.7.7.7': {'prefixesSent': 50}}, + {'10.1.0.0': {'prefixesSent': 50}}, + {'10.2.0.0': {'prefixesSent': 50}}, + {'10.64.207.255': {'prefixesSent': 50}} + ], + ), + False + ) +) operator_all_tests = [ operator_all_same, operator_contains, operator_not_contains, -# operator_is_gt, -# operator_is_lt, + operator_is_gt, + operator_is_lt, # operator_in_operator, # operator_not_operator, # # type() == dict() From 16cb326d17cdbf64e13411c9aeff3dbf624baf2c Mon Sep 17 00:00:00 2001 From: Network to Code Date: Mon, 28 Feb 2022 10:20:48 +0100 Subject: [PATCH 21/80] add range operators --- netcompare/check_types.py | 17 +++----- netcompare/operator.py | 56 +++++++++++++++++++++++++++ tests/test_operators.py | 81 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 138 insertions(+), 16 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index a91b2e4..c352458 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -261,21 +261,16 @@ def validate(**kwargs) -> None: if params_key in ins: #"is-in", "not-in", "in-range", "not-range" requires an iterable - if not isinstance(params_value, list) or not isinstance(params_value, tuple): - raise ValueError(f"Range check-option {ins} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down')") + if not isinstance(params_value, list) and not isinstance(params_value, tuple): + raise ValueError(f"Range check-option {ins} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down'). You have: {params_value} of type {type(params_value)}You have: {params_value} of type {type(params_value)}") # "in-range", "not-range" requires int or float where value at index 0 is lower than value at index 1 if params_key in ('in-range', 'not-range'): if not (isinstance(params_value[0], int) or isinstance(params_value[0], float)) and not (isinstance(params_value[1], float) or isinstance(params_value[1], int)): - raise ValueError(f"Range check-option {params_key} must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000)") + raise ValueError(f"Range check-option {params_key} must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000). You have: {params_value} of type {type(params_value)}") if not params_value[0] < params_value[1]: - raise ValueError(f"'range' and 'not-range' must have value at index 0 lower than value at index 1. i.e: dict(not-range=(70000000, 80000000)") - # "is-in", "not-in" requires iterable of strings - elif params_key in ('is-in', 'not-in'): - for item in params_value: - if not isinstance(item, str): - raise ValueError(f"'is-in' and 'not-in' must be an iterable of strings. i.e: dict(is-in=(Idle, Down). You have: {item} of type {type(item)}") - + raise ValueError(f"'range' and 'not-range' must have value at index 0 lower than value at index 1. i.e: dict(not-range=(70000000, 80000000). You have: {params_value} of type {type(params_value)}") + # "is-gt","is-lt" require either int() or float() elif params_key in numbers: if not isinstance(params_value, float) and not isinstance(params_value, int): @@ -284,7 +279,7 @@ def validate(**kwargs) -> None: # "contains", "not-contains" require string. elif params_key in strings: if not isinstance(params_value, str): - raise ValueError(f"Range check-option {strings} must have value of type string. i.e: dict(contains='EVPN')") + raise ValueError(f"Range check-option {strings} must have value of type string. i.e: dict(contains='EVPN'). You have: {params_value} of type {type(params_value)}") # "all-same" requires boolean True or False elif params_key in bools: diff --git a/netcompare/operator.py b/netcompare/operator.py index 5c1b1d3..7eb8891 100644 --- a/netcompare/operator.py +++ b/netcompare/operator.py @@ -88,3 +88,59 @@ def is_lt(self): return (True, result) else: return (False, result) + + + def is_in(self): + result = list() + for item in self.value_to_compare: + for value in item.values(): + for evaluated_value in value.values(): + if evaluated_value in self.referance_data_value: + # Create a list for compare valiues. + result.append(item) + if result: + return (True, result) + else: + return (False, result) + + + def not_in(self): + result = list() + for item in self.value_to_compare: + for value in item.values(): + for evaluated_value in value.values(): + if evaluated_value not in self.referance_data_value: + # Create a list for compare valiues. + result.append(item) + if result: + return (True, result) + else: + return (False, result) + + def in_range(self): + result = list() + for item in self.value_to_compare: + for value in item.values(): + for evaluated_value in value.values(): + if self.referance_data_value[0] < evaluated_value < self.referance_data_value[1]: + # Create a list for compare valiues. + result.append(item) + + if result: + return (True, result) + else: + return (False, result) + + def not_range(self): + result = list() + for item in self.value_to_compare: + for value in item.values(): + for evaluated_value in value.values(): + if not self.referance_data_value[0] < evaluated_value < self.referance_data_value[1]: + # Create a list for compare valiues. + result.append(item) + + if result: + return (True, result) + else: + return (False, result) \ No newline at end of file diff --git a/tests/test_operators.py b/tests/test_operators.py index 13c4387..d6a11fd 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -111,6 +111,78 @@ False ) ) +operator_is_in = ( + "pre.json", + "operator", + {"params": {"is-in": [20, 40, 50]}}, + "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", + ( + ( + True, + [ + {'7.7.7.7': {'prefixesSent': 50}}, + {'10.1.0.0': {'prefixesSent': 50}}, + {'10.2.0.0': {'prefixesSent': 50}}, + {'10.64.207.255': {'prefixesSent': 50}} + ], + ), + False + ) +) +operator_not_in = ( + "pre.json", + "operator", + {"params": {"not-in": [20, 40, 60]}}, + "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", + ( + ( + True, + [ + {'7.7.7.7': {'prefixesSent': 50}}, + {'10.1.0.0': {'prefixesSent': 50}}, + {'10.2.0.0': {'prefixesSent': 50}}, + {'10.64.207.255': {'prefixesSent': 50}} + ], + ), + False + ) +) +operator_in_range = ( + "pre.json", + "operator", + {"params": {"in-range": (20, 60)}}, + "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", + ( + ( + True, + [ + {'7.7.7.7': {'prefixesSent': 50}}, + {'10.1.0.0': {'prefixesSent': 50}}, + {'10.2.0.0': {'prefixesSent': 50}}, + {'10.64.207.255': {'prefixesSent': 50}} + ], + ), + False + ) +) +operator_not_in_range = ( + "pre.json", + "operator", + {"params": {"not-range": (20, 40)}}, + "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", + ( + ( + True, + [ + {'7.7.7.7': {'prefixesSent': 50}}, + {'10.1.0.0': {'prefixesSent': 50}}, + {'10.2.0.0': {'prefixesSent': 50}}, + {'10.64.207.255': {'prefixesSent': 50}} + ], + ), + False + ) +) operator_all_tests = [ operator_all_same, @@ -118,11 +190,10 @@ operator_not_contains, operator_is_gt, operator_is_lt, -# operator_in_operator, -# operator_not_operator, -# # type() == dict() -# operator_is_in, -# operator_not_in, + operator_is_in, + operator_not_in, + operator_in_range, + operator_not_in_range, ] @pytest.mark.parametrize("filename, check_type_str, evaluate_args, path, expected_result", operator_all_tests) From 1c8e939c26f407de45967489e58a8a8279aa2e02 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Wed, 2 Mar 2022 10:20:59 +0100 Subject: [PATCH 22/80] add operator lib for DRY code --- netcompare/check_types.py | 68 +++++++------ netcompare/evaluators.py | 8 +- netcompare/operator.py | 155 +++++++++++------------------- tests/test_operators.py | 195 +++++++++----------------------------- tests/test_type_checks.py | 10 +- 5 files changed, 147 insertions(+), 289 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index c352458..f1b8f24 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -1,11 +1,8 @@ """CheckType Implementation.""" import re -import pdb from typing import Mapping, Tuple, List, Dict, Any, Union from abc import ABC, abstractmethod import jmespath - - from .utils.jmespath_parsers import ( jmespath_value_parser, jmespath_refkey_parser, @@ -31,13 +28,13 @@ def init(check_type: str): """ if check_type == "exact_match": return ExactMatchType() - elif check_type == "tolerance": + if check_type == "tolerance": return ToleranceType() - elif check_type == "parameter_match": + if check_type == "parameter_match": return ParameterMatchType() - elif check_type == "regex": + if check_type == "regex": return RegexType() - elif check_type == "operator": + if check_type == "operator": return OperatorType() raise NotImplementedError @@ -230,15 +227,10 @@ class OperatorType(CheckType): @staticmethod def validate(**kwargs) -> None: - ins = ( - "is-in", - "not-in", - "in-range", - "not-range" - ) + ins = ("is-in", "not-in", "in-range", "not-range") bools = ("all-same",) - numbers = ("is-gt","is-lt") - # "equals" is redundant with check type "exact_match" an "parameter_match" + numbers = ("is-gt", "is-lt") + # "equals" is redundant with check type "exact_match" an "parameter_match" # equals = ("is-equal", "not-equal") strings = ("contains", "not-contains") valid_options = ( @@ -257,41 +249,55 @@ def validate(**kwargs) -> None: params_value = list(kwargs.values())[0] # Validate "params" value is legal. if not any(params_key in operator for operator in valid_options): - raise ValueError(f"'params' value must be one of the following: {[sub_element for element in valid_options for sub_element in element]}. You have: {params_key}") + raise ValueError( + f"'params' value must be one of the following: {[sub_element for element in valid_options for sub_element in element]}. You have: {params_key}" + ) if params_key in ins: - #"is-in", "not-in", "in-range", "not-range" requires an iterable + # "is-in", "not-in", "in-range", "not-range" requires an iterable if not isinstance(params_value, list) and not isinstance(params_value, tuple): - raise ValueError(f"Range check-option {ins} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down'). You have: {params_value} of type {type(params_value)}You have: {params_value} of type {type(params_value)}") - + raise ValueError( + f"Range check-option {ins} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down'). You have: {params_value} of type {type(params_value)}You have: {params_value} of type {type(params_value)}" + ) + # "in-range", "not-range" requires int or float where value at index 0 is lower than value at index 1 - if params_key in ('in-range', 'not-range'): - if not (isinstance(params_value[0], int) or isinstance(params_value[0], float)) and not (isinstance(params_value[1], float) or isinstance(params_value[1], int)): - raise ValueError(f"Range check-option {params_key} must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000). You have: {params_value} of type {type(params_value)}") + if params_key in ("in-range", "not-range"): + if not (isinstance(params_value[0], int) or isinstance(params_value[0], float)) and not ( + isinstance(params_value[1], float) or isinstance(params_value[1], int) + ): + raise ValueError( + f"Range check-option {params_key} must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000). You have: {params_value} of type {type(params_value)}" + ) if not params_value[0] < params_value[1]: - raise ValueError(f"'range' and 'not-range' must have value at index 0 lower than value at index 1. i.e: dict(not-range=(70000000, 80000000). You have: {params_value} of type {type(params_value)}") + raise ValueError( + f"'range' and 'not-range' must have value at index 0 lower than value at index 1. i.e: dict(not-range=(70000000, 80000000). You have: {params_value} of type {type(params_value)}" + ) # "is-gt","is-lt" require either int() or float() elif params_key in numbers: if not isinstance(params_value, float) and not isinstance(params_value, int): - raise ValueError(f"Check-option {numbers} must have value of type float or int. i.e: dict(is-lt=50). You have: {params_value} of type {type(params_value)}") - + raise ValueError( + f"Check-option {numbers} must have value of type float or int. i.e: dict(is-lt=50). You have: {params_value} of type {type(params_value)}" + ) + # "contains", "not-contains" require string. elif params_key in strings: if not isinstance(params_value, str): - raise ValueError(f"Range check-option {strings} must have value of type string. i.e: dict(contains='EVPN'). You have: {params_value} of type {type(params_value)}") - + raise ValueError( + f"Range check-option {strings} must have value of type string. i.e: dict(contains='EVPN'). You have: {params_value} of type {type(params_value)}" + ) + # "all-same" requires boolean True or False elif params_key in bools: if not isinstance(params_value, bool): - raise ValueError(f"Range check-option {bools} must have value of type bool. i.e: dict(all-same=True). You have: {params_value} of type {type(params_value)}") - + raise ValueError( + f"Range check-option {bools} must have value of type bool. i.e: dict(all-same=True). You have: {params_value} of type {type(params_value)}" + ) def evaluate(self, value_to_compare: Any, params: Any) -> Tuple[Mapping, bool]: """Operator evaluator implementation.""" self.validate(**params) # For naming consistency - reference_data=params + reference_data = params evaluation_result = operator_evaluator(reference_data, value_to_compare) return evaluation_result, not evaluation_result - diff --git a/netcompare/evaluators.py b/netcompare/evaluators.py index ad77655..d51fc4b 100644 --- a/netcompare/evaluators.py +++ b/netcompare/evaluators.py @@ -1,12 +1,11 @@ """Evaluators.""" import re - -import pdb from typing import Any, Mapping, Dict from deepdiff import DeepDiff -from .utils.diff_helpers import get_diff_iterables_items +from .utils.diff_helpers import get_diff_iterables_items, fix_deepdiff_key_names from .operator import Operator + def diff_generator(pre_result: Any, post_result: Any) -> Dict: """Generates diff between pre and post data based on check definition. @@ -106,9 +105,8 @@ def regex_evaluator(values: Mapping, regex_expression: str, mode: str) -> Dict: def operator_evaluator(referance_data: Mapping, value_to_compare: Mapping) -> Dict: # referance_data # {'all-same': True} - operator_type = list(referance_data.keys())[0].replace('-', '_') + operator_type = list(referance_data.keys())[0].replace("-", "_") operator = Operator(referance_data, value_to_compare) result = getattr(operator, operator_type)() return result - diff --git a/netcompare/operator.py b/netcompare/operator.py index 7eb8891..966dad4 100644 --- a/netcompare/operator.py +++ b/netcompare/operator.py @@ -1,14 +1,53 @@ -class Operator(): - +import operator + + +class Operator: def __init__(self, referance_data, value_to_compare) -> None: - # [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, - # {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, - # {'10.2.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, + # [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, + # {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, + # {'10.2.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, # {'10.64.207.255': {'peerGroup': 'IPv4-UNDERLAY-MLAG-PEER', 'vrf': 'default', 'state': 'Idle'}}] self.referance_data_value = list(referance_data.values())[0] self.value_to_compare = value_to_compare - + def _loop_through(self, call_ops): + ops = { + ">": operator.gt, + "<": operator.lt, + "is_in": operator.contains, + "not_in": operator.contains, + "contains": operator.contains, + "not_contains": operator.contains, + } + + result = list() + for item in self.value_to_compare: + for value in item.values(): + for evaluated_value in value.values(): + # reverse operands (??? WHY ???) https://docs.python.org/3.8/library/operator.html#operator.contains + if call_ops == "is_in": + if ops[call_ops](self.referance_data_value, evaluated_value): + result.append(item) + elif call_ops == "not_contains": + if not ops[call_ops](evaluated_value, self.referance_data_value): + result.append(item) + elif call_ops == "not_in": + if not ops[call_ops](self.referance_data_value, evaluated_value): + result.append(item) + elif call_ops == "in_range": + if self.referance_data_value[0] < evaluated_value < self.referance_data_value[1]: + result.append(item) + elif call_ops == "not_range": + if not self.referance_data_value[0] < evaluated_value < self.referance_data_value[1]: + result.append(item) + else: + if ops[call_ops](evaluated_value, self.referance_data_value): + result.append(item) + if result: + return (True, result) + else: + return (False, result) + def all_same(self): list_of_values = list() result = list() @@ -33,114 +72,26 @@ def all_same(self): elif not self.referance_data_value and all(result): return (False, self.value_to_compare) - def contains(self): - result = list() - for item in self.value_to_compare: - for value in item.values(): - for evaluated_value in value.values(): - if self.referance_data_value in evaluated_value: - # Create a list for compare valiues. - result.append(item) - if result: - return (True, result) - else: - return (False, result) - + return self._loop_through("contains") def not_contains(self): - result = list() - for item in self.value_to_compare: - for value in item.values(): - for evaluated_value in value.values(): - if self.referance_data_value not in evaluated_value: - # Create a list for compare valiues. - result.append(item) - if result: - return (True, result) - else: - return (False, result) - + return self._loop_through("not_contains") def is_gt(self): - result = list() - for item in self.value_to_compare: - for value in item.values(): - for evaluated_value in value.values(): - if evaluated_value > self.referance_data_value: - # Create a list for compare valiues. - result.append(item) - if result: - return (True, result) - else: - return (False, result) - + return self._loop_through(">") def is_lt(self): - result = list() - for item in self.value_to_compare: - for value in item.values(): - for evaluated_value in value.values(): - if evaluated_value < self.referance_data_value: - # Create a list for compare valiues. - result.append(item) - if result: - return (True, result) - else: - return (False, result) - + return self._loop_through("<") def is_in(self): - result = list() - for item in self.value_to_compare: - for value in item.values(): - for evaluated_value in value.values(): - if evaluated_value in self.referance_data_value: - # Create a list for compare valiues. - result.append(item) - if result: - return (True, result) - else: - return (False, result) - + return self._loop_through("is_in") def not_in(self): - result = list() - for item in self.value_to_compare: - for value in item.values(): - for evaluated_value in value.values(): - if evaluated_value not in self.referance_data_value: - # Create a list for compare valiues. - result.append(item) - if result: - return (True, result) - else: - return (False, result) + return self._loop_through("not_in") def in_range(self): - result = list() - for item in self.value_to_compare: - for value in item.values(): - for evaluated_value in value.values(): - if self.referance_data_value[0] < evaluated_value < self.referance_data_value[1]: - # Create a list for compare valiues. - result.append(item) - - if result: - return (True, result) - else: - return (False, result) + return self._loop_through("in_range") def not_range(self): - result = list() - for item in self.value_to_compare: - for value in item.values(): - for evaluated_value in value.values(): - if not self.referance_data_value[0] < evaluated_value < self.referance_data_value[1]: - # Create a list for compare valiues. - result.append(item) - - if result: - return (True, result) - else: - return (False, result) \ No newline at end of file + return self._loop_through("not_range") diff --git a/tests/test_operators.py b/tests/test_operators.py index d6a11fd..c65b89e 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -1,7 +1,6 @@ import pytest -from netcompare.check_types import CheckType, ExactMatchType, OperatorType, ToleranceType, ParameterMatchType, RegexType +from netcompare.check_types import CheckType from .utility import load_json_file, ASSERT_FAIL_MESSAGE -import pdb operator_all_same = ( "pre.json", @@ -50,13 +49,7 @@ "operator", {"params": {"contains": "EVPN"}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", - ( - ( - True, - [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE'}}] - ), - False - ) + ((True, [{"7.7.7.7": {"peerGroup": "EVPN-OVERLAY-SPINE"}}]), False), ) operator_not_contains = ( "pre.json", @@ -67,13 +60,13 @@ ( True, [ - {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE'}}, - {'10.2.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE'}}, - {'10.64.207.255': {'peerGroup': 'IPv4-UNDERLAY-MLAG-PEER'}} - ] + {"10.1.0.0": {"peerGroup": "IPv4-UNDERLAY-SPINE"}}, + {"10.2.0.0": {"peerGroup": "IPv4-UNDERLAY-SPINE"}}, + {"10.64.207.255": {"peerGroup": "IPv4-UNDERLAY-MLAG-PEER"}}, + ], ), - False - ) + False, + ), ) operator_is_gt = ( "pre.json", @@ -84,14 +77,14 @@ ( True, [ - {'7.7.7.7': {'prefixesSent': 50}}, - {'10.1.0.0': {'prefixesSent': 50}}, - {'10.2.0.0': {'prefixesSent': 50}}, - {'10.64.207.255': {'prefixesSent': 50}} + {"7.7.7.7": {"prefixesSent": 50}}, + {"10.1.0.0": {"prefixesSent": 50}}, + {"10.2.0.0": {"prefixesSent": 50}}, + {"10.64.207.255": {"prefixesSent": 50}}, ], ), - False - ) + False, + ), ) operator_is_lt = ( "pre.json", @@ -102,14 +95,14 @@ ( True, [ - {'7.7.7.7': {'prefixesSent': 50}}, - {'10.1.0.0': {'prefixesSent': 50}}, - {'10.2.0.0': {'prefixesSent': 50}}, - {'10.64.207.255': {'prefixesSent': 50}} + {"7.7.7.7": {"prefixesSent": 50}}, + {"10.1.0.0": {"prefixesSent": 50}}, + {"10.2.0.0": {"prefixesSent": 50}}, + {"10.64.207.255": {"prefixesSent": 50}}, ], ), - False - ) + False, + ), ) operator_is_in = ( "pre.json", @@ -120,14 +113,14 @@ ( True, [ - {'7.7.7.7': {'prefixesSent': 50}}, - {'10.1.0.0': {'prefixesSent': 50}}, - {'10.2.0.0': {'prefixesSent': 50}}, - {'10.64.207.255': {'prefixesSent': 50}} + {"7.7.7.7": {"prefixesSent": 50}}, + {"10.1.0.0": {"prefixesSent": 50}}, + {"10.2.0.0": {"prefixesSent": 50}}, + {"10.64.207.255": {"prefixesSent": 50}}, ], ), - False - ) + False, + ), ) operator_not_in = ( "pre.json", @@ -138,14 +131,14 @@ ( True, [ - {'7.7.7.7': {'prefixesSent': 50}}, - {'10.1.0.0': {'prefixesSent': 50}}, - {'10.2.0.0': {'prefixesSent': 50}}, - {'10.64.207.255': {'prefixesSent': 50}} + {"7.7.7.7": {"prefixesSent": 50}}, + {"10.1.0.0": {"prefixesSent": 50}}, + {"10.2.0.0": {"prefixesSent": 50}}, + {"10.64.207.255": {"prefixesSent": 50}}, ], ), - False - ) + False, + ), ) operator_in_range = ( "pre.json", @@ -156,14 +149,14 @@ ( True, [ - {'7.7.7.7': {'prefixesSent': 50}}, - {'10.1.0.0': {'prefixesSent': 50}}, - {'10.2.0.0': {'prefixesSent': 50}}, - {'10.64.207.255': {'prefixesSent': 50}} + {"7.7.7.7": {"prefixesSent": 50}}, + {"10.1.0.0": {"prefixesSent": 50}}, + {"10.2.0.0": {"prefixesSent": 50}}, + {"10.64.207.255": {"prefixesSent": 50}}, ], ), - False - ) + False, + ), ) operator_not_in_range = ( "pre.json", @@ -174,14 +167,14 @@ ( True, [ - {'7.7.7.7': {'prefixesSent': 50}}, - {'10.1.0.0': {'prefixesSent': 50}}, - {'10.2.0.0': {'prefixesSent': 50}}, - {'10.64.207.255': {'prefixesSent': 50}} + {"7.7.7.7": {"prefixesSent": 50}}, + {"10.1.0.0": {"prefixesSent": 50}}, + {"10.2.0.0": {"prefixesSent": 50}}, + {"10.64.207.255": {"prefixesSent": 50}}, ], ), - False - ) + False, + ), ) operator_all_tests = [ @@ -196,6 +189,7 @@ operator_not_in_range, ] + @pytest.mark.parametrize("filename, check_type_str, evaluate_args, path, expected_result", operator_all_tests) def test_operator(filename, check_type_str, evaluate_args, path, expected_result): """Validate all operator check types.""" @@ -205,102 +199,5 @@ def test_operator(filename, check_type_str, evaluate_args, path, expected_result value = check.get_value(data, path) actual_results = check.evaluate(value, **evaluate_args) assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( - output=actual_results, - expected_output=expected_result + output=actual_results, expected_output=expected_result ) - -# # operator_is_equal = ( -# # "api", -# # "operator", -# # ("operator", {"is-equal": 100}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesReceived]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_not_equal = ( -# # "api", -# # ("operator", {"not-equal": "internal"}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,linkType]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - - - -# # operator_not_contains = ( -# # "api", -# # ("operator", {"not-contains": "OVERLAY"}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_is_gt = ( -# # "api", -# # ("operator", {"is-gt": 70000000}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_is_lt = ( -# # "api", -# # ("operator", {"is-lt": 80000000}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_in_operator = ( -# # "api", -# # ("operator", {"in-operator": (70000000, 80000000)}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_not_operator = ( -# # "api", -# # ("operator", {"not-range": (70000000, 80000000)}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,bgpPeerCaps]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_is_in = ( -# # "api", -# # ("operator", {"is-in": ("Idle", "Down")}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - -# # operator_not_in = ( -# # "api", -# # ("operator", {"not-in": ("Idle", "Down")}), -# # "result[0].vrfs.default.peerList[*].[$peerAddress$,state]", -# # ( -# # {}, #TBD -# # False, #TBD -# # ), -# # ) - - - diff --git a/tests/test_type_checks.py b/tests/test_type_checks.py index e9e0caa..b0152b8 100644 --- a/tests/test_type_checks.py +++ b/tests/test_type_checks.py @@ -2,7 +2,7 @@ import pytest from netcompare.check_types import CheckType, ExactMatchType, OperatorType, ToleranceType, ParameterMatchType, RegexType from .utility import load_json_file, load_mocks, ASSERT_FAIL_MESSAGE -import pdb + def test_child_class_raises_exception(): """Tests that exception is raised for child class when abstract methods are not implemented.""" @@ -43,7 +43,7 @@ def evaluate(self, *args, **kwargs): ("tolerance", ToleranceType), ("parameter_match", ParameterMatchType), ("regex", RegexType), - ("operator", OperatorType) + ("operator", OperatorType), ], ) def test_check_init(check_type_str, expected_class): @@ -54,6 +54,8 @@ def test_check_init(check_type_str, expected_class): exception_tests_init = [ ("does_not_exist", NotImplementedError, ""), ] + + @pytest.mark.parametrize("check_type_str, exception_type, expected_in_output", exception_tests_init) def tests_exceptions_init(check_type_str, exception_type, expected_in_output): """Tests exceptions when check object is initialized.""" @@ -76,6 +78,8 @@ def tests_exceptions_init(check_type_str, exception_type, expected_in_output): "Mode argument should be", ), ] + + @pytest.mark.parametrize("check_type_str, evaluate_args, exception_type, expected_in_output", exception_tests_eval) def tests_exceptions_eval(check_type_str, evaluate_args, exception_type, expected_in_output): """Tests exceptions when calling .evaluate() method.""" @@ -139,6 +143,8 @@ def tests_exceptions_eval(check_type_str, evaluate_args, exception_type, expecte tolerance_test_values_within_threshold, tolerance_test_values_beyond_threshold, ] + + @pytest.mark.parametrize("check_type_str, evaluate_args, folder_name, path, expected_results", check_type_tests) def test_check_type_results(check_type_str, evaluate_args, folder_name, path, expected_results): """Validate that CheckType.evaluate returns the expected_results.""" From 3cbef979fd375ed25f20f3da1006c8badd7419c5 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Wed, 2 Mar 2022 10:29:59 +0100 Subject: [PATCH 23/80] fix some lint errors --- netcompare/check_types.py | 3 +-- netcompare/evaluators.py | 1 + netcompare/operator.py | 28 ++++++++++++++++++++-------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index f1b8f24..1d7274b 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -13,9 +13,8 @@ from .utils.data_normalization import exclude_filter, flatten_list from .evaluators import diff_generator, parameter_evaluator, regex_evaluator, operator_evaluator -# pylint: disable=arguments-differ - +# pylint: disable=arguments-differ class CheckType(ABC): """Check Type Base Abstract Class.""" diff --git a/netcompare/evaluators.py b/netcompare/evaluators.py index d51fc4b..b605f90 100644 --- a/netcompare/evaluators.py +++ b/netcompare/evaluators.py @@ -103,6 +103,7 @@ def regex_evaluator(values: Mapping, regex_expression: str, mode: str) -> Dict: def operator_evaluator(referance_data: Mapping, value_to_compare: Mapping) -> Dict: + """Operator evaluator call.""" # referance_data # {'all-same': True} operator_type = list(referance_data.keys())[0].replace("-", "_") diff --git a/netcompare/operator.py b/netcompare/operator.py index 966dad4..66e8328 100644 --- a/netcompare/operator.py +++ b/netcompare/operator.py @@ -1,7 +1,10 @@ +"""Operator diff.""" import operator class Operator: + """Operator class implementation.""" + def __init__(self, referance_data, value_to_compare) -> None: # [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, # {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, @@ -11,6 +14,7 @@ def __init__(self, referance_data, value_to_compare) -> None: self.value_to_compare = value_to_compare def _loop_through(self, call_ops): + """Method for operator evaluation.""" ops = { ">": operator.gt, "<": operator.lt, @@ -20,7 +24,7 @@ def _loop_through(self, call_ops): "not_contains": operator.contains, } - result = list() + result = [] for item in self.value_to_compare: for value in item.values(): for evaluated_value in value.values(): @@ -45,12 +49,12 @@ def _loop_through(self, call_ops): result.append(item) if result: return (True, result) - else: - return (False, result) + return (False, result) def all_same(self): - list_of_values = list() - result = list() + """All same operator implementation.""" + list_of_values = [] + result = [] for item in self.value_to_compare: for value in item.values(): @@ -65,33 +69,41 @@ def all_same(self): if self.referance_data_value and not all(result): return (False, self.value_to_compare) - elif self.referance_data_value and all(result): + if self.referance_data_value and all(result): return (True, self.value_to_compare) - elif not self.referance_data_value and not all(result): + if not self.referance_data_value and not all(result): return (True, self.value_to_compare) - elif not self.referance_data_value and all(result): + if not self.referance_data_value and all(result): return (False, self.value_to_compare) def contains(self): + """Contains operator implementation.""" return self._loop_through("contains") def not_contains(self): + """Not contains operator implementation.""" return self._loop_through("not_contains") def is_gt(self): + """Is greather than operator implementation.""" return self._loop_through(">") def is_lt(self): + """Is lower than operator implementation.""" return self._loop_through("<") def is_in(self): + """Is in operator implementation.""" return self._loop_through("is_in") def not_in(self): + """Is not in operator implementation.""" return self._loop_through("not_in") def in_range(self): + """Is in range operator implementation.""" return self._loop_through("in_range") def not_range(self): + """Is not in range operator implementation.""" return self._loop_through("not_range") From 4c2ddc04c072bd1a7ee79822e7951f0666b4f1fd Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 3 Mar 2022 09:36:23 +0100 Subject: [PATCH 24/80] Update netcompare/operator.py Not sure about this...if you look at the if statements, your suggestion would brake the logic...or am I missing something? Co-authored-by: Patryk Szulczewski --- netcompare/operator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netcompare/operator.py b/netcompare/operator.py index 66e8328..2a8422f 100644 --- a/netcompare/operator.py +++ b/netcompare/operator.py @@ -44,7 +44,8 @@ def _loop_through(self, call_ops): elif call_ops == "not_range": if not self.referance_data_value[0] < evaluated_value < self.referance_data_value[1]: result.append(item) - else: + elif ops[call_ops](evaluated_value, self.referance_data_value): + result.append(item) if ops[call_ops](evaluated_value, self.referance_data_value): result.append(item) if result: From 45e0655b0ca74efa6ccec93c68d33b85bfef1f9e Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 3 Mar 2022 10:27:15 +0100 Subject: [PATCH 25/80] work on comments --- netcompare/check_types.py | 38 +++++++++++++++++++------------------- netcompare/operator.py | 24 +++++++++++++++++------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index 1d7274b..85640d5 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -226,18 +226,18 @@ class OperatorType(CheckType): @staticmethod def validate(**kwargs) -> None: - ins = ("is-in", "not-in", "in-range", "not-range") - bools = ("all-same",) - numbers = ("is-gt", "is-lt") + in_operators = ("is-in", "not-in", "in-range", "not-range") + bool_operators = ("all-same",) + number_operators = ("is-gt", "is-lt") # "equals" is redundant with check type "exact_match" an "parameter_match" - # equals = ("is-equal", "not-equal") - strings = ("contains", "not-contains") + # equal_operators = ("is-equal", "not-equal") + string_operators = ("contains", "not-contains") valid_options = ( - bools, - ins, - numbers, - strings, - # equals, + in_operators, + bool_operators, + number_operators, + string_operators, + # equal_operators, ) # Validate "params" argument is not None. @@ -247,16 +247,16 @@ def validate(**kwargs) -> None: params_key = list(kwargs.keys())[0] params_value = list(kwargs.values())[0] # Validate "params" value is legal. - if not any(params_key in operator for operator in valid_options): + if all(params_key in operator for operator in valid_options): raise ValueError( f"'params' value must be one of the following: {[sub_element for element in valid_options for sub_element in element]}. You have: {params_key}" ) - if params_key in ins: + if params_key in in_operators: # "is-in", "not-in", "in-range", "not-range" requires an iterable if not isinstance(params_value, list) and not isinstance(params_value, tuple): raise ValueError( - f"Range check-option {ins} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down'). You have: {params_value} of type {type(params_value)}You have: {params_value} of type {type(params_value)}" + f"Range check-option {in_operators} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down'). You have: {params_value} of type {type(params_value)}You have: {params_value} of type {type(params_value)}" ) # "in-range", "not-range" requires int or float where value at index 0 is lower than value at index 1 @@ -273,24 +273,24 @@ def validate(**kwargs) -> None: ) # "is-gt","is-lt" require either int() or float() - elif params_key in numbers: + elif params_key in number_operators: if not isinstance(params_value, float) and not isinstance(params_value, int): raise ValueError( - f"Check-option {numbers} must have value of type float or int. i.e: dict(is-lt=50). You have: {params_value} of type {type(params_value)}" + f"Check-option {number_operators} must have value of type float or int. i.e: dict(is-lt=50). You have: {params_value} of type {type(params_value)}" ) # "contains", "not-contains" require string. - elif params_key in strings: + elif params_key in string_operators: if not isinstance(params_value, str): raise ValueError( - f"Range check-option {strings} must have value of type string. i.e: dict(contains='EVPN'). You have: {params_value} of type {type(params_value)}" + f"Range check-option {string_operators} must have value of type string. i.e: dict(contains='EVPN'). You have: {params_value} of type {type(params_value)}" ) # "all-same" requires boolean True or False - elif params_key in bools: + elif params_key in bool_operators: if not isinstance(params_value, bool): raise ValueError( - f"Range check-option {bools} must have value of type bool. i.e: dict(all-same=True). You have: {params_value} of type {type(params_value)}" + f"Range check-option {bool_operators} must have value of type bool. i.e: dict(all-same=True). You have: {params_value} of type {type(params_value)}" ) def evaluate(self, value_to_compare: Any, params: Any) -> Tuple[Mapping, bool]: diff --git a/netcompare/operator.py b/netcompare/operator.py index 66e8328..648473a 100644 --- a/netcompare/operator.py +++ b/netcompare/operator.py @@ -44,8 +44,8 @@ def _loop_through(self, call_ops): elif call_ops == "not_range": if not self.referance_data_value[0] < evaluated_value < self.referance_data_value[1]: result.append(item) - else: - if ops[call_ops](evaluated_value, self.referance_data_value): + # "<", ">", "contains" + elif ops[call_ops](evaluated_value, self.referance_data_value): result.append(item) if result: return (True, result) @@ -62,19 +62,29 @@ def all_same(self): list_of_values.append(value) for element in list_of_values: - if not element == list_of_values[0]: + if element != list_of_values[0]: result.append(False) else: result.append(True) + # if self.referance_data_value and not all(result): + # return (False, self.value_to_compare) + # if self.referance_data_value and all(result): + # return (True, self.value_to_compare) + # if not self.referance_data_value and not all(result): + # return (True, self.value_to_compare) + # if not self.referance_data_value and all(result): + # return (False, self.value_to_compare) + + if self.referance_data_value and not all(result): return (False, self.value_to_compare) - if self.referance_data_value and all(result): + if self.referance_data_value: return (True, self.value_to_compare) - if not self.referance_data_value and not all(result): + if not all(result): return (True, self.value_to_compare) - if not self.referance_data_value and all(result): - return (False, self.value_to_compare) + return (False, self.value_to_compare) + def contains(self): """Contains operator implementation.""" From fa4a34fdbb2c7889294a9b7d892a8107c75d0edd Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 3 Mar 2022 11:12:11 +0100 Subject: [PATCH 26/80] working on operator args --- netcompare/check_types.py | 6 +++-- netcompare/evaluators.py | 8 +++---- netcompare/operator.py | 47 ++++++++++++++++----------------------- tests/test_operators.py | 16 ++++++------- 4 files changed, 35 insertions(+), 42 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index fa2dc57..27eb0b8 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -248,12 +248,14 @@ def validate(**kwargs) -> None: # equal_operators, ) + import pdb + pdb.set_trace() # Validate "params" argument is not None. if not kwargs: raise KeyError(f"'params' argument must be provided. You have {kwargs}. Read the docs for more info.") - params_key = list(kwargs.keys())[0] - params_value = list(kwargs.values())[0] + params_key = kwargs['mode'] + params_value = kwargs['operator_data'] # Validate "params" value is legal. if all(params_key in operator for operator in valid_options): raise ValueError( diff --git a/netcompare/evaluators.py b/netcompare/evaluators.py index b605f90..3fbdf94 100644 --- a/netcompare/evaluators.py +++ b/netcompare/evaluators.py @@ -105,9 +105,9 @@ def regex_evaluator(values: Mapping, regex_expression: str, mode: str) -> Dict: def operator_evaluator(referance_data: Mapping, value_to_compare: Mapping) -> Dict: """Operator evaluator call.""" # referance_data - # {'all-same': True} - operator_type = list(referance_data.keys())[0].replace("-", "_") - operator = Operator(referance_data, value_to_compare) + # {'mode': 'all-same', 'operator_data': True} + operator_mode = referance_data['mode'].replace("-", "_") + operator = Operator(referance_data['operator_data'], value_to_compare) - result = getattr(operator, operator_type)() + result = getattr(operator, operator_mode)() return result diff --git a/netcompare/operator.py b/netcompare/operator.py index 648473a..1b94714 100644 --- a/netcompare/operator.py +++ b/netcompare/operator.py @@ -10,11 +10,11 @@ def __init__(self, referance_data, value_to_compare) -> None: # {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, # {'10.2.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, # {'10.64.207.255': {'peerGroup': 'IPv4-UNDERLAY-MLAG-PEER', 'vrf': 'default', 'state': 'Idle'}}] - self.referance_data_value = list(referance_data.values())[0] + self.referance_data = referance_data self.value_to_compare = value_to_compare - def _loop_through(self, call_ops): - """Method for operator evaluation.""" + def _loop_through_wrapper(self, call_ops): + """Wrappoer method for operator evaluation.""" ops = { ">": operator.gt, "<": operator.lt, @@ -30,22 +30,22 @@ def _loop_through(self, call_ops): for evaluated_value in value.values(): # reverse operands (??? WHY ???) https://docs.python.org/3.8/library/operator.html#operator.contains if call_ops == "is_in": - if ops[call_ops](self.referance_data_value, evaluated_value): + if ops[call_ops](self.referance_data, evaluated_value): result.append(item) elif call_ops == "not_contains": - if not ops[call_ops](evaluated_value, self.referance_data_value): + if not ops[call_ops](evaluated_value, self.referance_data): result.append(item) elif call_ops == "not_in": - if not ops[call_ops](self.referance_data_value, evaluated_value): + if not ops[call_ops](self.referance_data, evaluated_value): result.append(item) elif call_ops == "in_range": - if self.referance_data_value[0] < evaluated_value < self.referance_data_value[1]: + if self.referance_data[0] < evaluated_value < self.referance_data[1]: result.append(item) elif call_ops == "not_range": - if not self.referance_data_value[0] < evaluated_value < self.referance_data_value[1]: + if not self.referance_data[0] < evaluated_value < self.referance_data[1]: result.append(item) # "<", ">", "contains" - elif ops[call_ops](evaluated_value, self.referance_data_value): + elif ops[call_ops](evaluated_value, self.referance_data): result.append(item) if result: return (True, result) @@ -67,19 +67,10 @@ def all_same(self): else: result.append(True) - # if self.referance_data_value and not all(result): - # return (False, self.value_to_compare) - # if self.referance_data_value and all(result): - # return (True, self.value_to_compare) - # if not self.referance_data_value and not all(result): - # return (True, self.value_to_compare) - # if not self.referance_data_value and all(result): - # return (False, self.value_to_compare) - - if self.referance_data_value and not all(result): + if self.referance_data and not all(result): return (False, self.value_to_compare) - if self.referance_data_value: + if self.referance_data: return (True, self.value_to_compare) if not all(result): return (True, self.value_to_compare) @@ -88,32 +79,32 @@ def all_same(self): def contains(self): """Contains operator implementation.""" - return self._loop_through("contains") + return self._loop_through_wrapper("contains") def not_contains(self): """Not contains operator implementation.""" - return self._loop_through("not_contains") + return self._loop_through_wrapper("not_contains") def is_gt(self): """Is greather than operator implementation.""" - return self._loop_through(">") + return self._loop_through_wrapper(">") def is_lt(self): """Is lower than operator implementation.""" - return self._loop_through("<") + return self._loop_through_wrapper("<") def is_in(self): """Is in operator implementation.""" - return self._loop_through("is_in") + return self._loop_through_wrapper("is_in") def not_in(self): """Is not in operator implementation.""" - return self._loop_through("not_in") + return self._loop_through_wrapper("not_in") def in_range(self): """Is in range operator implementation.""" - return self._loop_through("in_range") + return self._loop_through_wrapper("in_range") def not_range(self): """Is not in range operator implementation.""" - return self._loop_through("not_range") + return self._loop_through_wrapper("not_range") diff --git a/tests/test_operators.py b/tests/test_operators.py index c65b89e..8ce8860 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -5,7 +5,7 @@ operator_all_same = ( "pre.json", "operator", - {"params": {"all-same": True}}, + {"params": {"mode": "all-same", "operator_data": True}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup,vrf,state]", ( ( @@ -47,7 +47,7 @@ operator_contains = ( "pre.json", "operator", - {"params": {"contains": "EVPN"}}, + {"params": {"mode": "contains", "operator_data": "EVPN"}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", ((True, [{"7.7.7.7": {"peerGroup": "EVPN-OVERLAY-SPINE"}}]), False), ) @@ -71,7 +71,7 @@ operator_is_gt = ( "pre.json", "operator", - {"params": {"is-gt": 20}}, + {"params": {"mode": "is-gt", "operator_data": 20}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( ( @@ -89,7 +89,7 @@ operator_is_lt = ( "pre.json", "operator", - {"params": {"is-lt": 60}}, + {"params": {"mode": "is-lt", "operator_data": 60}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( ( @@ -107,7 +107,7 @@ operator_is_in = ( "pre.json", "operator", - {"params": {"is-in": [20, 40, 50]}}, + {"params": {"mode": "is-in", "operator_data": [20, 40, 50]}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( ( @@ -125,7 +125,7 @@ operator_not_in = ( "pre.json", "operator", - {"params": {"not-in": [20, 40, 60]}}, + {"params": {"mode": "not-in", "operator_data": [20, 40, 60]}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( ( @@ -143,7 +143,7 @@ operator_in_range = ( "pre.json", "operator", - {"params": {"in-range": (20, 60)}}, + {"params": {"mode": "in-range", "operator_data":(20, 60)}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( ( @@ -161,7 +161,7 @@ operator_not_in_range = ( "pre.json", "operator", - {"params": {"not-range": (20, 40)}}, + {"params": {"mode": "not-range", "operator_data": (20, 40)}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( ( From 82b72ef00e590b15558e84f550576f419630b81f Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 3 Mar 2022 16:57:24 +0100 Subject: [PATCH 27/80] fix tests --- netcompare/check_types.py | 11 ++++------- netcompare/evaluators.py | 4 ++-- netcompare/operator.py | 5 ++--- pyproject.toml | 4 +++- tests/test_operators.py | 5 +++-- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index 27eb0b8..c0925c6 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -234,6 +234,7 @@ class OperatorType(CheckType): @staticmethod def validate(**kwargs) -> None: + """Validate operator parameters.""" in_operators = ("is-in", "not-in", "in-range", "not-range") bool_operators = ("all-same",) number_operators = ("is-gt", "is-lt") @@ -248,14 +249,12 @@ def validate(**kwargs) -> None: # equal_operators, ) - import pdb - pdb.set_trace() # Validate "params" argument is not None. if not kwargs: raise KeyError(f"'params' argument must be provided. You have {kwargs}. Read the docs for more info.") - params_key = kwargs['mode'] - params_value = kwargs['operator_data'] + params_key = kwargs["mode"] + params_value = kwargs["operator_data"] # Validate "params" value is legal. if all(params_key in operator for operator in valid_options): raise ValueError( @@ -271,9 +270,7 @@ def validate(**kwargs) -> None: # "in-range", "not-range" requires int or float where value at index 0 is lower than value at index 1 if params_key in ("in-range", "not-range"): - if not (isinstance(params_value[0], int) or isinstance(params_value[0], float)) and not ( - isinstance(params_value[1], float) or isinstance(params_value[1], int) - ): + if not isinstance(params_value[0], (int, float)) and not isinstance(params_value[1], float, int): raise ValueError( f"Range check-option {params_key} must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000). You have: {params_value} of type {type(params_value)}" ) diff --git a/netcompare/evaluators.py b/netcompare/evaluators.py index 3fbdf94..360ff06 100644 --- a/netcompare/evaluators.py +++ b/netcompare/evaluators.py @@ -106,8 +106,8 @@ def operator_evaluator(referance_data: Mapping, value_to_compare: Mapping) -> Di """Operator evaluator call.""" # referance_data # {'mode': 'all-same', 'operator_data': True} - operator_mode = referance_data['mode'].replace("-", "_") - operator = Operator(referance_data['operator_data'], value_to_compare) + operator_mode = referance_data["mode"].replace("-", "_") + operator = Operator(referance_data["operator_data"], value_to_compare) result = getattr(operator, operator_mode)() return result diff --git a/netcompare/operator.py b/netcompare/operator.py index 1b94714..b3df677 100644 --- a/netcompare/operator.py +++ b/netcompare/operator.py @@ -6,6 +6,7 @@ class Operator: """Operator class implementation.""" def __init__(self, referance_data, value_to_compare) -> None: + """__init__ method.""" # [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, # {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, # {'10.2.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}, @@ -46,7 +47,7 @@ def _loop_through_wrapper(self, call_ops): result.append(item) # "<", ">", "contains" elif ops[call_ops](evaluated_value, self.referance_data): - result.append(item) + result.append(item) if result: return (True, result) return (False, result) @@ -67,7 +68,6 @@ def all_same(self): else: result.append(True) - if self.referance_data and not all(result): return (False, self.value_to_compare) if self.referance_data: @@ -76,7 +76,6 @@ def all_same(self): return (True, self.value_to_compare) return (False, self.value_to_compare) - def contains(self): """Contains operator implementation.""" return self._loop_through_wrapper("contains") diff --git a/pyproject.toml b/pyproject.toml index 83e962b..fd1b6b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,9 @@ no-docstring-rgx="^(_|test_|Meta$)" disable = """, line-too-long, bad-continuation, - E5110 + E5110, + R0912, + R0801 """ [tool.pylint.miscellaneous] diff --git a/tests/test_operators.py b/tests/test_operators.py index 8ce8860..05dd8d7 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -1,3 +1,4 @@ +"""Unit tests for operator check-type.""" import pytest from netcompare.check_types import CheckType from .utility import load_json_file, ASSERT_FAIL_MESSAGE @@ -54,7 +55,7 @@ operator_not_contains = ( "pre.json", "operator", - {"params": {"not-contains": "EVPN"}}, + {"params": {"mode": "not-contains", "operator_data": "EVPN"}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", ( ( @@ -143,7 +144,7 @@ operator_in_range = ( "pre.json", "operator", - {"params": {"mode": "in-range", "operator_data":(20, 60)}}, + {"params": {"mode": "in-range", "operator_data": (20, 60)}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( ( From eba9972b80114d434764b43646b65f83ac6df639 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Fri, 4 Mar 2022 10:00:07 +0100 Subject: [PATCH 28/80] improve validator switch logic --- netcompare/check_types.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index c0925c6..1c24bf9 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -263,7 +263,7 @@ def validate(**kwargs) -> None: if params_key in in_operators: # "is-in", "not-in", "in-range", "not-range" requires an iterable - if not isinstance(params_value, list) and not isinstance(params_value, tuple): + if not isinstance(params_value, (list, tuple)): raise ValueError( f"Range check-option {in_operators} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down'). You have: {params_value} of type {type(params_value)}You have: {params_value} of type {type(params_value)}" ) @@ -280,25 +280,22 @@ def validate(**kwargs) -> None: ) # "is-gt","is-lt" require either int() or float() - elif params_key in number_operators: - if not isinstance(params_value, float) and not isinstance(params_value, int): - raise ValueError( - f"Check-option {number_operators} must have value of type float or int. i.e: dict(is-lt=50). You have: {params_value} of type {type(params_value)}" - ) + elif params_key in number_operators and not isinstance(params_value, (float, int)): + raise ValueError( + f"Check-option {number_operators} must have value of type float or int. i.e: dict(is-lt=50). You have: {params_value} of type {type(params_value)}" + ) # "contains", "not-contains" require string. - elif params_key in string_operators: - if not isinstance(params_value, str): - raise ValueError( - f"Range check-option {string_operators} must have value of type string. i.e: dict(contains='EVPN'). You have: {params_value} of type {type(params_value)}" - ) + elif params_key in string_operators and not isinstance(params_value, str): + raise ValueError( + f"Range check-option {string_operators} must have value of type string. i.e: dict(contains='EVPN'). You have: {params_value} of type {type(params_value)}" + ) # "all-same" requires boolean True or False - elif params_key in bool_operators: - if not isinstance(params_value, bool): - raise ValueError( - f"Range check-option {bool_operators} must have value of type bool. i.e: dict(all-same=True). You have: {params_value} of type {type(params_value)}" - ) + elif params_key in bool_operators and not isinstance(params_value, bool): + raise ValueError( + f"Range check-option {bool_operators} must have value of type bool. i.e: dict(all-same=True). You have: {params_value} of type {type(params_value)}" + ) def evaluate(self, value_to_compare: Any, params: Any) -> Tuple[Mapping, bool]: """Operator evaluator implementation.""" From 158d16b1a4cba9ffe1226b59428f1f4325c60b95 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Fri, 4 Mar 2022 10:22:26 +0100 Subject: [PATCH 29/80] fix pytests --- tests/test_diff_generator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_diff_generator.py b/tests/test_diff_generator.py index d95a11c..63c5fa8 100644 --- a/tests/test_diff_generator.py +++ b/tests/test_diff_generator.py @@ -30,8 +30,7 @@ exact_match_of_bgp_neigh_via_textfsm = ( "textfsm", - "", - # "result[*].[$bgp_neigh$,state]", + "result[*].[$bgp_neigh$,state]", [], {"10.17.254.2": {"state": {"new_value": "Up", "old_value": "Idle"}}}, ) From 53118540837ece6460bff062e74d0d066a71700e Mon Sep 17 00:00:00 2001 From: Network to Code Date: Mon, 7 Mar 2022 12:16:47 +0100 Subject: [PATCH 30/80] tolerance validate tests --- netcompare/check_types.py | 4 ++-- tests/test_validates.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 tests/test_validates.py diff --git a/netcompare/check_types.py b/netcompare/check_types.py index 1c24bf9..e3d3872 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -134,9 +134,9 @@ def validate(**kwargs) -> None: # reference_data = getattr(kwargs, "reference_data") tolerance = kwargs.get("tolerance") if not tolerance: - raise ValueError("Tolerance argument is mandatory for Tolerance Check Type.") + raise ValueError("'tolerance' argument is mandatory for Tolerance Check Type.") if not isinstance(tolerance, int): - raise ValueError(f"Tolerance argument must be an integer, and it's {type(tolerance)}.") + raise ValueError(f"Tolerance argument's value must be an integer. You have: {type(tolerance)}.") def evaluate(self, value_to_compare: Any, reference_data: Any, tolerance: int) -> Tuple[Dict, bool]: """Returns the difference between values and the boolean. Overwrites method in base class.""" diff --git a/tests/test_validates.py b/tests/test_validates.py new file mode 100644 index 0000000..d9057dd --- /dev/null +++ b/tests/test_validates.py @@ -0,0 +1,35 @@ +"""Unit tests for validator CheckType method.""" +import pytest +from netcompare.check_types import CheckType +from .utility import load_mocks + +tolerance_wrong_argumet = ( + "tolerance", + {"gt": 10}, + "'tolerance' argument is mandatory for Tolerance Check Type.", +) +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [tolerance_wrong_argumet]) +def test_tolerance_key_name(check_type_str, evaluate_args,expected_results): + """Validate that CheckType tolerance has `tolerance` key.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +tolerance_wrong_value = ( + "tolerance", + {"tolerance": "10"}, + "Tolerance argument's value must be an integer. You have: .", +) +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [tolerance_wrong_value]) +def test_tolerance_value_type(check_type_str, evaluate_args,expected_results): + """Validate that CheckType tolerance has `tolerance` value of type int""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results From 9d3fd1135e561a7c276b235c4e2e003bf3f10206 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Mon, 7 Mar 2022 14:34:51 +0100 Subject: [PATCH 31/80] add validate parameter test --- netcompare/check_types.py | 10 +++--- tests/test_validates.py | 65 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index e3d3872..e89e5f9 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -181,17 +181,15 @@ def validate(**kwargs) -> None: mode_options = ["match", "no-match"] params = kwargs.get("params") if not params: - raise ValueError("Params argument is mandatory for ParameterMatch Check Type.") + raise ValueError("'params' argument is mandatory for ParameterMatch Check Type.") if not isinstance(params, dict): - raise ValueError(f"Params argument must be a dict, and it's {type(params)}.") + raise ValueError(f"'params' argument must be a dict. You have: {type(params)}.") mode = kwargs.get("mode") if not mode: - raise ValueError("Mode argument is mandatory for ParameterMatch Check Type.") - if not isinstance(mode, str): - raise ValueError(f"Mode argument must be a string, and it's {type(mode)}.") + raise ValueError("'mode' argument is mandatory for ParameterMatch Check Type.") if mode not in mode_options: - raise ValueError(f"Mode argument should be {mode_options}, and it's {mode}") + raise ValueError(f"'mode' argument should be one of the following: {', '.join(mode_options)}. You have: {mode}") def evaluate(self, value_to_compare: Mapping, params: Dict, mode: str) -> Tuple[Dict, bool]: """Parameter Match evaluator implementation.""" diff --git a/tests/test_validates.py b/tests/test_validates.py index d9057dd..031a2ac 100644 --- a/tests/test_validates.py +++ b/tests/test_validates.py @@ -33,3 +33,68 @@ def test_tolerance_value_type(check_type_str, evaluate_args,expected_results): check.validate(**evaluate_args) assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + + +parameter_no_params = ( + "parameter_match", + {"mode": "match", "wrong_key": {"localAsn": "65130.1100", "linkType": "external"}}, + "'params' argument is mandatory for ParameterMatch Check Type.", +) +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [parameter_no_params]) +def test_parameter_param(check_type_str, evaluate_args,expected_results): + """Validate that CheckType parameter has 'params' key.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +parameter_wrong_type = ( + "parameter_match", + {"mode": "match", "params": [{"localAsn": "65130.1100", "linkType": "external"}]}, + "'params' argument must be a dict. You have: .", +) +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [parameter_wrong_type]) +def test_parameter_value_type(check_type_str, evaluate_args, expected_results): + """Validate that CheckType parameter 'params' value type.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +parameter_no_mode = ( + "parameter_match", + {"mode-no-mode": "match", "params": {"localAsn": "65130.1100", "linkType": "external"}}, + "'mode' argument is mandatory for ParameterMatch Check Type.", +) +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [parameter_no_mode]) +def test_parameter_mode(check_type_str, evaluate_args, expected_results): + """Validate that CheckType parameter has mode key.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +parameter_no_mode = ( + "parameter_match", + {"mode": ["match"], "params": {"localAsn": "65130.1100", "linkType": "external"}}, + "'mode' argument should be one of the following: match, no-match. You have: ['match']", +) +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [parameter_no_mode]) +def test_parameter_mode_value(check_type_str, evaluate_args, expected_results): + """Validate that CheckType parameter 'mode' has value of typ str.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results \ No newline at end of file From 3eecb341b5439894b0ce65246e4481409193d71b Mon Sep 17 00:00:00 2001 From: Network to Code Date: Mon, 7 Mar 2022 14:52:52 +0100 Subject: [PATCH 32/80] add mode to param match check type --- netcompare/check_types.py | 2 +- netcompare/evaluators.py | 7 +++++-- tests/test_type_checks.py | 43 ++++++++++++++------------------------- 3 files changed, 21 insertions(+), 31 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index e89e5f9..6fc04f5 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -195,7 +195,7 @@ def evaluate(self, value_to_compare: Mapping, params: Dict, mode: str) -> Tuple[ """Parameter Match evaluator implementation.""" self.validate(params=params, mode=mode) # TODO: we don't use the mode? - evaluation_result = parameter_evaluator(value_to_compare, params) + evaluation_result = parameter_evaluator(value_to_compare, params, mode) return evaluation_result, not evaluation_result diff --git a/netcompare/evaluators.py b/netcompare/evaluators.py index 360ff06..114da7d 100644 --- a/netcompare/evaluators.py +++ b/netcompare/evaluators.py @@ -36,7 +36,7 @@ def diff_generator(pre_result: Any, post_result: Any) -> Dict: return fix_deepdiff_key_names(result) -def parameter_evaluator(values: Mapping, parameters: Mapping) -> Dict: +def parameter_evaluator(values: Mapping, parameters: Mapping, mode: str) -> Dict: """Parameter Match evaluator engine. Args: @@ -71,8 +71,11 @@ def parameter_evaluator(values: Mapping, parameters: Mapping) -> Dict: inner_value = list(value.values())[0] for parameter_key, parameter_value in parameters.items(): - if inner_value[parameter_key] != parameter_value: + if mode == 'match' and inner_value[parameter_key] != parameter_value: result_item[parameter_key] = inner_value[parameter_key] + elif mode == 'no-match' and inner_value[parameter_key] == parameter_value: + result_item[parameter_key] = inner_value[parameter_key] + if result_item: result[inner_key] = result_item diff --git a/tests/test_type_checks.py b/tests/test_type_checks.py index b0152b8..59c8f00 100644 --- a/tests/test_type_checks.py +++ b/tests/test_type_checks.py @@ -64,31 +64,6 @@ def tests_exceptions_init(check_type_str, exception_type, expected_in_output): assert expected_in_output in error.value.__str__() -exception_tests_eval = [ - ( - "parameter_match", - {"value_to_compare": {}, "mode": "some mode", "params": {"some": "thing"}}, - ValueError, - "Mode argument should be", - ), - ( - "regex", - {"value_to_compare": {}, "mode": "some mode", "regex": "some regex"}, - ValueError, - "Mode argument should be", - ), -] - - -@pytest.mark.parametrize("check_type_str, evaluate_args, exception_type, expected_in_output", exception_tests_eval) -def tests_exceptions_eval(check_type_str, evaluate_args, exception_type, expected_in_output): - """Tests exceptions when calling .evaluate() method.""" - with pytest.raises(exception_type) as error: - check = CheckType.init(check_type_str) - check.evaluate(**evaluate_args) - assert expected_in_output in error.value.__str__() - - exact_match_test_values_no_change = ( "exact_match", {}, @@ -287,9 +262,21 @@ def test_checks(folder_name, check_type_str, evaluate_args, path, expected_resul False, ), ) - - -@pytest.mark.parametrize("filename, check_type_str, evaluate_args, path, expected_result", [parameter_match_api]) +parameter_no_match_api = ( + "pre.json", + "parameter_match", + {"mode": "no-match", "params": {"localAsn": "65130.1100", "linkType": "external"}}, + "result[0].vrfs.default.peerList[*].[$peerAddress$,localAsn,linkType]", + ( + { + '10.1.0.0': {'linkType': 'external'}, + '10.2.0.0': {'localAsn': '65130.1100', 'linkType': 'external'}, + '10.64.207.255': {'localAsn': '65130.1100', 'linkType': 'external'} + }, + False, + ), +) +@pytest.mark.parametrize("filename, check_type_str, evaluate_args, path, expected_result", [parameter_match_api, parameter_no_match_api]) def test_param_match(filename, check_type_str, evaluate_args, path, expected_result): """Validate parameter_match check type.""" check = CheckType.init(check_type_str) From 7829b322c15083106c6ced2c610b9920c4e9c177 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Mon, 7 Mar 2022 15:14:07 +0100 Subject: [PATCH 33/80] add regex validate tests --- netcompare/check_types.py | 10 +++--- tests/test_validates.py | 68 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index 6fc04f5..a6519ac 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -208,17 +208,15 @@ def validate(**kwargs) -> None: mode_options = ["match", "no-match"] regex = kwargs.get("regex") if not regex: - raise ValueError("Params argument is mandatory for Regex Match Check Type.") + raise ValueError("'regex' argument is mandatory for Regex Check Type.") if not isinstance(regex, str): - raise ValueError(f"Params argument must be a string, and it's {type(regex)}.") + raise ValueError(f"'regex' argument must be a string. You have: {type(regex)}.") mode = kwargs.get("mode") if not mode: - raise ValueError("Mode argument is mandatory for Regex Match Check Type.") - if not isinstance(mode, str): - raise ValueError(f"Mode argument must be a string, and it's {type(mode)}.") + raise ValueError("'mode' argument is mandatory for Regex Check Type.") if mode not in mode_options: - raise ValueError(f"Mode argument should be {mode_options}, and it's {mode}") + raise ValueError(f"'mode' argument should be {mode_options}. You have: {mode}") def evaluate(self, value_to_compare: Mapping, regex: str, mode: str) -> Tuple[Mapping, bool]: """Regex Match evaluator implementation.""" diff --git a/tests/test_validates.py b/tests/test_validates.py index 031a2ac..3e14e35 100644 --- a/tests/test_validates.py +++ b/tests/test_validates.py @@ -84,16 +84,80 @@ def test_parameter_mode(check_type_str, evaluate_args, expected_results): assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results -parameter_no_mode = ( +parameter_mode_value = ( "parameter_match", {"mode": ["match"], "params": {"localAsn": "65130.1100", "linkType": "external"}}, "'mode' argument should be one of the following: match, no-match. You have: ['match']", ) -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [parameter_no_mode]) +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [parameter_mode_value]) def test_parameter_mode_value(check_type_str, evaluate_args, expected_results): """Validate that CheckType parameter 'mode' has value of typ str.""" check = CheckType.init(check_type_str) + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +regex_no_params = ( + "regex", + {"regexregex": ".*UNDERLAY.*", "mode": "match"}, + "'regex' argument is mandatory for Regex Check Type.", +) +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [regex_no_params]) +def test_regex_param(check_type_str, evaluate_args,expected_results): + """Validate that CheckType regex has 'params' key.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +regex_wrong_type = ( + "regex", + {"regex": [".*UNDERLAY.*"], "mode-no-mode": "match"}, + "'regex' argument must be a string. You have: .", +) +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [regex_wrong_type]) +def test_regex_value_type(check_type_str, evaluate_args, expected_results): + """Validate that CheckType regex 'params' value type.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +regex_no_mode = ( + "regex", + {"regex": ".*UNDERLAY.*", "mode-no-mode": "match"}, + "'mode' argument is mandatory for Regex Check Type.", +) +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [regex_no_mode]) +def test_regex_mode(check_type_str, evaluate_args, expected_results): + """Validate that CheckType regex has 'mode' key.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +regex_mode_value = ( + "regex", + {"regex": ".*UNDERLAY.*", "mode": "match-no-match"}, + "'mode' argument should be ['match', 'no-match']. You have: match-no-match", +) +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [regex_mode_value]) +def test_regex_mode_value(check_type_str, evaluate_args, expected_results): + """Validate that CheckType regex 'mode' has value of typ str.""" + check = CheckType.init(check_type_str) + with pytest.raises(ValueError) as exc_info: check.validate(**evaluate_args) From f8af8d30f8be2fcc05bd1e0e941eba0844ed2169 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Tue, 8 Mar 2022 10:25:51 +0100 Subject: [PATCH 34/80] working on operator validato tests --- netcompare/check_types.py | 18 ++++++++------ tests/test_operators.py | 2 +- tests/test_validates.py | 49 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index a6519ac..d3d77f5 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -246,13 +246,17 @@ def validate(**kwargs) -> None: ) # Validate "params" argument is not None. - if not kwargs: - raise KeyError(f"'params' argument must be provided. You have {kwargs}. Read the docs for more info.") + if not kwargs or list(kwargs.keys())[0] != 'params': + raise ValueError(f"'params' argument must be provided. You have: {list(kwargs.keys())[0]}.") + + params_key = kwargs['params'].get("mode") + params_value = kwargs['params'].get("operator_data") + + if not params_key or not params_value: + raise ValueError(f"'mode' and 'operator_data' arguments must be provided. You have: {list(kwargs['params'].keys())}.") - params_key = kwargs["mode"] - params_value = kwargs["operator_data"] # Validate "params" value is legal. - if all(params_key in operator for operator in valid_options): + if not all(params_key in operator for operator in valid_options): raise ValueError( f"'params' value must be one of the following: {[sub_element for element in valid_options for sub_element in element]}. You have: {params_key}" ) @@ -261,7 +265,7 @@ def validate(**kwargs) -> None: # "is-in", "not-in", "in-range", "not-range" requires an iterable if not isinstance(params_value, (list, tuple)): raise ValueError( - f"Range check-option {in_operators} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down'). You have: {params_value} of type {type(params_value)}You have: {params_value} of type {type(params_value)}" + f"'range' check-option {in_operators} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down'). You have: {params_value} of type {type(params_value)}You have: {params_value} of type {type(params_value)}" ) # "in-range", "not-range" requires int or float where value at index 0 is lower than value at index 1 @@ -298,5 +302,5 @@ def evaluate(self, value_to_compare: Any, params: Any) -> Tuple[Mapping, bool]: self.validate(**params) # For naming consistency reference_data = params - evaluation_result = operator_evaluator(reference_data, value_to_compare) + evaluation_result = operator_evaluator(reference_data['params'], value_to_compare) return evaluation_result, not evaluation_result diff --git a/tests/test_operators.py b/tests/test_operators.py index 05dd8d7..9d3b5ab 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -198,7 +198,7 @@ def test_operator(filename, check_type_str, evaluate_args, path, expected_result # There is not concept of "pre" and "post" in operator. data = load_json_file("api", filename) value = check.get_value(data, path) - actual_results = check.evaluate(value, **evaluate_args) + actual_results = check.evaluate(value, evaluate_args) assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( output=actual_results, expected_output=expected_result ) diff --git a/tests/test_validates.py b/tests/test_validates.py index 3e14e35..59ef294 100644 --- a/tests/test_validates.py +++ b/tests/test_validates.py @@ -1,7 +1,6 @@ """Unit tests for validator CheckType method.""" import pytest from netcompare.check_types import CheckType -from .utility import load_mocks tolerance_wrong_argumet = ( "tolerance", @@ -158,6 +157,54 @@ def test_regex_mode_value(check_type_str, evaluate_args, expected_results): """Validate that CheckType regex 'mode' has value of typ str.""" check = CheckType.init(check_type_str) + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +operator_params = ( + "operator", + {"my_params": {"mode": "not-in", "operator_data": [20, 40, 60]}}, + "'params' argument must be provided. You have: my_params.", +) +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params]) +def test_operator_params(check_type_str, evaluate_args, expected_results): + """Validate that CheckType operator if has 'params' argument.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +operator_params_mode = ( + "operator", + {"params": {"no-mode": "not-in", "not-operator_data": [20, 40, 60]}}, + "'mode' and 'operator_data' arguments must be provided. You have: ['no-mode', 'not-operator_data'].", +) +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_mode]) +def test_operator_params_mode(check_type_str, evaluate_args, expected_results): + """Validate that CheckType operator if has 'mode' and 'operator_data' arguments.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +operator_params_wrong_operator = ( + "operator", + {"params": {"mode": "random", "operator_data": [20, 40, 60]}}, + "'params' value must be one of the following: ['is-in', 'not-in', 'in-range', 'not-range', 'all-same', 'is-gt', 'is-lt', 'contains', 'not-contains']. You have: random", +) +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_wrong_operator]) +def test_operator_params_wrong_operator(check_type_str, evaluate_args, expected_results): + """Validate that CheckType operator if has 'mode' and 'operator_data' arguments.""" + check = CheckType.init(check_type_str) + with pytest.raises(ValueError) as exc_info: check.validate(**evaluate_args) From 5cc0382bb24186bb9033daeb9a53cc13fd43982b Mon Sep 17 00:00:00 2001 From: Network to Code Date: Tue, 8 Mar 2022 11:56:00 +0100 Subject: [PATCH 35/80] complete validate tests for operator --- netcompare/check_types.py | 36 ++++--- netcompare/evaluators.py | 5 +- tests/test_type_checks.py | 12 ++- tests/test_validates.py | 213 +++++++++++++++++++++++++++++++------- 4 files changed, 205 insertions(+), 61 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index d3d77f5..c18e0cc 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -189,7 +189,9 @@ def validate(**kwargs) -> None: if not mode: raise ValueError("'mode' argument is mandatory for ParameterMatch Check Type.") if mode not in mode_options: - raise ValueError(f"'mode' argument should be one of the following: {', '.join(mode_options)}. You have: {mode}") + raise ValueError( + f"'mode' argument should be one of the following: {', '.join(mode_options)}. You have: {mode}" + ) def evaluate(self, value_to_compare: Mapping, params: Dict, mode: str) -> Tuple[Dict, bool]: """Parameter Match evaluator implementation.""" @@ -246,17 +248,19 @@ def validate(**kwargs) -> None: ) # Validate "params" argument is not None. - if not kwargs or list(kwargs.keys())[0] != 'params': + if not kwargs or list(kwargs.keys())[0] != "params": raise ValueError(f"'params' argument must be provided. You have: {list(kwargs.keys())[0]}.") - params_key = kwargs['params'].get("mode") - params_value = kwargs['params'].get("operator_data") + params_key = kwargs["params"].get("mode") + params_value = kwargs["params"].get("operator_data") if not params_key or not params_value: - raise ValueError(f"'mode' and 'operator_data' arguments must be provided. You have: {list(kwargs['params'].keys())}.") + raise ValueError( + f"'mode' and 'operator_data' arguments must be provided. You have: {list(kwargs['params'].keys())}." + ) # Validate "params" value is legal. - if not all(params_key in operator for operator in valid_options): + if not any(params_key in sub_element for element in valid_options for sub_element in element): raise ValueError( f"'params' value must be one of the following: {[sub_element for element in valid_options for sub_element in element]}. You have: {params_key}" ) @@ -265,36 +269,40 @@ def validate(**kwargs) -> None: # "is-in", "not-in", "in-range", "not-range" requires an iterable if not isinstance(params_value, (list, tuple)): raise ValueError( - f"'range' check-option {in_operators} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down'). You have: {params_value} of type {type(params_value)}You have: {params_value} of type {type(params_value)}" + f"check options {in_operators} must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down'). You have: {params_value} of type {type(params_value)}." ) # "in-range", "not-range" requires int or float where value at index 0 is lower than value at index 1 if params_key in ("in-range", "not-range"): - if not isinstance(params_value[0], (int, float)) and not isinstance(params_value[1], float, int): + if ( + not len(params_value) == 2 + or not isinstance(params_value[0], (int, float)) + or not isinstance(params_value[1], (float, int)) + ): raise ValueError( - f"Range check-option {params_key} must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000). You have: {params_value} of type {type(params_value)}" + f"'range' check-option {params_key} must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000). You have: {params_value}." ) if not params_value[0] < params_value[1]: raise ValueError( - f"'range' and 'not-range' must have value at index 0 lower than value at index 1. i.e: dict(not-range=(70000000, 80000000). You have: {params_value} of type {type(params_value)}" + f"'range' and 'not-range' must have value at index 0 lower than value at index 1. i.e: dict(not-range=(70000000, 80000000). You have: {params_value}." ) # "is-gt","is-lt" require either int() or float() elif params_key in number_operators and not isinstance(params_value, (float, int)): raise ValueError( - f"Check-option {number_operators} must have value of type float or int. i.e: dict(is-lt=50). You have: {params_value} of type {type(params_value)}" + f"check options {number_operators} must have value of type float or int. You have: {params_value} of type {type(params_value)}" ) # "contains", "not-contains" require string. elif params_key in string_operators and not isinstance(params_value, str): raise ValueError( - f"Range check-option {string_operators} must have value of type string. i.e: dict(contains='EVPN'). You have: {params_value} of type {type(params_value)}" + f"check options {string_operators} must have value of type string. You have: {params_value} of type {type(params_value)}" ) # "all-same" requires boolean True or False elif params_key in bool_operators and not isinstance(params_value, bool): raise ValueError( - f"Range check-option {bool_operators} must have value of type bool. i.e: dict(all-same=True). You have: {params_value} of type {type(params_value)}" + f"check option all-same must have value of type bool. You have: {params_value} of type {type(params_value)}" ) def evaluate(self, value_to_compare: Any, params: Any) -> Tuple[Mapping, bool]: @@ -302,5 +310,5 @@ def evaluate(self, value_to_compare: Any, params: Any) -> Tuple[Mapping, bool]: self.validate(**params) # For naming consistency reference_data = params - evaluation_result = operator_evaluator(reference_data['params'], value_to_compare) + evaluation_result = operator_evaluator(reference_data["params"], value_to_compare) return evaluation_result, not evaluation_result diff --git a/netcompare/evaluators.py b/netcompare/evaluators.py index 114da7d..93106b7 100644 --- a/netcompare/evaluators.py +++ b/netcompare/evaluators.py @@ -71,12 +71,11 @@ def parameter_evaluator(values: Mapping, parameters: Mapping, mode: str) -> Dict inner_value = list(value.values())[0] for parameter_key, parameter_value in parameters.items(): - if mode == 'match' and inner_value[parameter_key] != parameter_value: + if mode == "match" and inner_value[parameter_key] != parameter_value: result_item[parameter_key] = inner_value[parameter_key] - elif mode == 'no-match' and inner_value[parameter_key] == parameter_value: + elif mode == "no-match" and inner_value[parameter_key] == parameter_value: result_item[parameter_key] = inner_value[parameter_key] - if result_item: result[inner_key] = result_item diff --git a/tests/test_type_checks.py b/tests/test_type_checks.py index 59c8f00..a2a30ce 100644 --- a/tests/test_type_checks.py +++ b/tests/test_type_checks.py @@ -269,14 +269,18 @@ def test_checks(folder_name, check_type_str, evaluate_args, path, expected_resul "result[0].vrfs.default.peerList[*].[$peerAddress$,localAsn,linkType]", ( { - '10.1.0.0': {'linkType': 'external'}, - '10.2.0.0': {'localAsn': '65130.1100', 'linkType': 'external'}, - '10.64.207.255': {'localAsn': '65130.1100', 'linkType': 'external'} + "10.1.0.0": {"linkType": "external"}, + "10.2.0.0": {"localAsn": "65130.1100", "linkType": "external"}, + "10.64.207.255": {"localAsn": "65130.1100", "linkType": "external"}, }, False, ), ) -@pytest.mark.parametrize("filename, check_type_str, evaluate_args, path, expected_result", [parameter_match_api, parameter_no_match_api]) + + +@pytest.mark.parametrize( + "filename, check_type_str, evaluate_args, path, expected_result", [parameter_match_api, parameter_no_match_api] +) def test_param_match(filename, check_type_str, evaluate_args, path, expected_result): """Validate parameter_match check type.""" check = CheckType.init(check_type_str) diff --git a/tests/test_validates.py b/tests/test_validates.py index 59ef294..d7d2c3e 100644 --- a/tests/test_validates.py +++ b/tests/test_validates.py @@ -7,15 +7,17 @@ {"gt": 10}, "'tolerance' argument is mandatory for Tolerance Check Type.", ) + + @pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [tolerance_wrong_argumet]) -def test_tolerance_key_name(check_type_str, evaluate_args,expected_results): +def test_tolerance_key_name(check_type_str, evaluate_args, expected_results): """Validate that CheckType tolerance has `tolerance` key.""" check = CheckType.init(check_type_str) - + with pytest.raises(ValueError) as exc_info: check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results tolerance_wrong_value = ( @@ -23,16 +25,17 @@ def test_tolerance_key_name(check_type_str, evaluate_args,expected_results): {"tolerance": "10"}, "Tolerance argument's value must be an integer. You have: .", ) + + @pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [tolerance_wrong_value]) -def test_tolerance_value_type(check_type_str, evaluate_args,expected_results): +def test_tolerance_value_type(check_type_str, evaluate_args, expected_results): """Validate that CheckType tolerance has `tolerance` value of type int""" check = CheckType.init(check_type_str) - + with pytest.raises(ValueError) as exc_info: check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results parameter_no_params = ( @@ -40,15 +43,17 @@ def test_tolerance_value_type(check_type_str, evaluate_args,expected_results): {"mode": "match", "wrong_key": {"localAsn": "65130.1100", "linkType": "external"}}, "'params' argument is mandatory for ParameterMatch Check Type.", ) + + @pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [parameter_no_params]) -def test_parameter_param(check_type_str, evaluate_args,expected_results): +def test_parameter_param(check_type_str, evaluate_args, expected_results): """Validate that CheckType parameter has 'params' key.""" check = CheckType.init(check_type_str) - + with pytest.raises(ValueError) as exc_info: check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results parameter_wrong_type = ( @@ -56,15 +61,17 @@ def test_parameter_param(check_type_str, evaluate_args,expected_results): {"mode": "match", "params": [{"localAsn": "65130.1100", "linkType": "external"}]}, "'params' argument must be a dict. You have: .", ) + + @pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [parameter_wrong_type]) def test_parameter_value_type(check_type_str, evaluate_args, expected_results): """Validate that CheckType parameter 'params' value type.""" check = CheckType.init(check_type_str) - + with pytest.raises(ValueError) as exc_info: check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results parameter_no_mode = ( @@ -72,15 +79,17 @@ def test_parameter_value_type(check_type_str, evaluate_args, expected_results): {"mode-no-mode": "match", "params": {"localAsn": "65130.1100", "linkType": "external"}}, "'mode' argument is mandatory for ParameterMatch Check Type.", ) + + @pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [parameter_no_mode]) def test_parameter_mode(check_type_str, evaluate_args, expected_results): """Validate that CheckType parameter has mode key.""" check = CheckType.init(check_type_str) - + with pytest.raises(ValueError) as exc_info: check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results parameter_mode_value = ( @@ -88,15 +97,17 @@ def test_parameter_mode(check_type_str, evaluate_args, expected_results): {"mode": ["match"], "params": {"localAsn": "65130.1100", "linkType": "external"}}, "'mode' argument should be one of the following: match, no-match. You have: ['match']", ) + + @pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [parameter_mode_value]) def test_parameter_mode_value(check_type_str, evaluate_args, expected_results): """Validate that CheckType parameter 'mode' has value of typ str.""" check = CheckType.init(check_type_str) - + with pytest.raises(ValueError) as exc_info: check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results regex_no_params = ( @@ -104,15 +115,17 @@ def test_parameter_mode_value(check_type_str, evaluate_args, expected_results): {"regexregex": ".*UNDERLAY.*", "mode": "match"}, "'regex' argument is mandatory for Regex Check Type.", ) + + @pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [regex_no_params]) -def test_regex_param(check_type_str, evaluate_args,expected_results): +def test_regex_param(check_type_str, evaluate_args, expected_results): """Validate that CheckType regex has 'params' key.""" check = CheckType.init(check_type_str) - + with pytest.raises(ValueError) as exc_info: check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results regex_wrong_type = ( @@ -120,15 +133,17 @@ def test_regex_param(check_type_str, evaluate_args,expected_results): {"regex": [".*UNDERLAY.*"], "mode-no-mode": "match"}, "'regex' argument must be a string. You have: .", ) + + @pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [regex_wrong_type]) def test_regex_value_type(check_type_str, evaluate_args, expected_results): """Validate that CheckType regex 'params' value type.""" check = CheckType.init(check_type_str) - + with pytest.raises(ValueError) as exc_info: check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results regex_no_mode = ( @@ -136,15 +151,17 @@ def test_regex_value_type(check_type_str, evaluate_args, expected_results): {"regex": ".*UNDERLAY.*", "mode-no-mode": "match"}, "'mode' argument is mandatory for Regex Check Type.", ) + + @pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [regex_no_mode]) def test_regex_mode(check_type_str, evaluate_args, expected_results): """Validate that CheckType regex has 'mode' key.""" check = CheckType.init(check_type_str) - + with pytest.raises(ValueError) as exc_info: check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results regex_mode_value = ( @@ -152,15 +169,17 @@ def test_regex_mode(check_type_str, evaluate_args, expected_results): {"regex": ".*UNDERLAY.*", "mode": "match-no-match"}, "'mode' argument should be ['match', 'no-match']. You have: match-no-match", ) + + @pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [regex_mode_value]) def test_regex_mode_value(check_type_str, evaluate_args, expected_results): """Validate that CheckType regex 'mode' has value of typ str.""" check = CheckType.init(check_type_str) - + with pytest.raises(ValueError) as exc_info: check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results operator_params = ( @@ -168,6 +187,8 @@ def test_regex_mode_value(check_type_str, evaluate_args, expected_results): {"my_params": {"mode": "not-in", "operator_data": [20, 40, 60]}}, "'params' argument must be provided. You have: my_params.", ) + + @pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params]) def test_operator_params(check_type_str, evaluate_args, expected_results): """Validate that CheckType operator if has 'params' argument.""" @@ -175,8 +196,8 @@ def test_operator_params(check_type_str, evaluate_args, expected_results): with pytest.raises(ValueError) as exc_info: check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results operator_params_mode = ( @@ -184,6 +205,8 @@ def test_operator_params(check_type_str, evaluate_args, expected_results): {"params": {"no-mode": "not-in", "not-operator_data": [20, 40, 60]}}, "'mode' and 'operator_data' arguments must be provided. You have: ['no-mode', 'not-operator_data'].", ) + + @pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_mode]) def test_operator_params_mode(check_type_str, evaluate_args, expected_results): """Validate that CheckType operator if has 'mode' and 'operator_data' arguments.""" @@ -191,8 +214,8 @@ def test_operator_params_mode(check_type_str, evaluate_args, expected_results): with pytest.raises(ValueError) as exc_info: check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results operator_params_wrong_operator = ( @@ -200,6 +223,8 @@ def test_operator_params_mode(check_type_str, evaluate_args, expected_results): {"params": {"mode": "random", "operator_data": [20, 40, 60]}}, "'params' value must be one of the following: ['is-in', 'not-in', 'in-range', 'not-range', 'all-same', 'is-gt', 'is-lt', 'contains', 'not-contains']. You have: random", ) + + @pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_wrong_operator]) def test_operator_params_wrong_operator(check_type_str, evaluate_args, expected_results): """Validate that CheckType operator if has 'mode' and 'operator_data' arguments.""" @@ -207,5 +232,113 @@ def test_operator_params_wrong_operator(check_type_str, evaluate_args, expected_ with pytest.raises(ValueError) as exc_info: check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results \ No newline at end of file + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +operator_params_in = ( + "operator", + {"params": {"mode": "in-range", "operator_data": "string"}}, + "check options ('is-in', 'not-in', 'in-range', 'not-range') must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down'). You have: string of type .", +) + + +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_in]) +def test_operator_params_in(check_type_str, evaluate_args, expected_results): + """Validate that CheckType operator if has 'mode' and 'operator_data' arguments.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +operator_params_in_range = ( + "operator", + {"params": {"mode": "in-range", "operator_data": (0, "1")}}, + "'range' check-option in-range must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000). You have: (0, '1').", +) + + +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_in_range]) +def test_operator_params_in_range(check_type_str, evaluate_args, expected_results): + """Validate that CheckType operator if has 'mode' and 'operator_data' arguments.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +operator_params_in_range_lower_than = ( + "operator", + {"params": {"mode": "in-range", "operator_data": (1, 0)}}, + "'range' and 'not-range' must have value at index 0 lower than value at index 1. i.e: dict(not-range=(70000000, 80000000). You have: (1, 0).", +) + + +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_in_range_lower_than]) +def test_operator_params_in_range_lower_than(check_type_str, evaluate_args, expected_results): + """Validate that CheckType operator if has 'mode' and 'operator_data' arguments.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +operator_params_number = ( + "operator", + {"params": {"mode": "is-gt", "operator_data": "1"}}, + "check options ('is-gt', 'is-lt') must have value of type float or int. You have: 1 of type ", +) + + +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_number]) +def test_operator_params_in_params_number(check_type_str, evaluate_args, expected_results): + """Validate that CheckType operator if has 'mode' and 'operator_data' arguments.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +operator_params_contains = ( + "operator", + {"params": {"mode": "contains", "operator_data": 1}}, + "check options ('contains', 'not-contains') must have value of type string. You have: 1 of type ", +) + + +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_contains]) +def test_operator_params_contains(check_type_str, evaluate_args, expected_results): + """Validate that CheckType operator if has 'mode' and 'operator_data' arguments.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results + + +operator_params_bool = ( + "operator", + {"params": {"mode": "all-same", "operator_data": 1}}, + "check option all-same must have value of type bool. You have: 1 of type ", +) + + +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_bool]) +def test_operator_params_bool(check_type_str, evaluate_args, expected_results): + """Validate that CheckType operator if has 'mode' and 'operator_data' arguments.""" + check = CheckType.init(check_type_str) + + with pytest.raises(ValueError) as exc_info: + check.validate(**evaluate_args) + + assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results From 1fdea996f1da2e09d7f8baa226daac06f07063fa Mon Sep 17 00:00:00 2001 From: Network to Code Date: Wed, 9 Mar 2022 09:23:10 +0100 Subject: [PATCH 36/80] paramet tests --- tests/test_validates.py | 264 ++++------------------------------------ 1 file changed, 26 insertions(+), 238 deletions(-) diff --git a/tests/test_validates.py b/tests/test_validates.py index 98361b6..4a87557 100644 --- a/tests/test_validates.py +++ b/tests/test_validates.py @@ -7,335 +7,123 @@ {"gt": 10}, "'tolerance' argument is mandatory for Tolerance Check Type.", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [tolerance_wrong_argumet]) -def test_tolerance_key_name(check_type_str, evaluate_args, expected_results): - """Validate that CheckType tolerance has `tolerance` key.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - tolerance_wrong_value = ( "tolerance", {"tolerance": "10"}, "Tolerance argument's value must be an integer. You have: .", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [tolerance_wrong_value]) -def test_tolerance_value_type(check_type_str, evaluate_args, expected_results): - """Validate that CheckType tolerance has `tolerance` value of type int""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - parameter_no_params = ( "parameter_match", {"mode": "match", "wrong_key": {"localAsn": "65130.1100", "linkType": "external"}}, "'params' argument is mandatory for ParameterMatch Check Type.", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [parameter_no_params]) -def test_parameter_param(check_type_str, evaluate_args, expected_results): - """Validate that CheckType parameter has 'params' key.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - parameter_wrong_type = ( "parameter_match", {"mode": "match", "params": [{"localAsn": "65130.1100", "linkType": "external"}]}, "'params' argument must be a dict. You have: .", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [parameter_wrong_type]) -def test_parameter_value_type(check_type_str, evaluate_args, expected_results): - """Validate that CheckType parameter 'params' value type.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - parameter_no_mode = ( "parameter_match", {"mode-no-mode": "match", "params": {"localAsn": "65130.1100", "linkType": "external"}}, "'mode' argument is mandatory for ParameterMatch Check Type.", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [parameter_no_mode]) -def test_parameter_mode(check_type_str, evaluate_args, expected_results): - """Validate that CheckType parameter has mode key.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - parameter_mode_value = ( "parameter_match", {"mode": ["match"], "params": {"localAsn": "65130.1100", "linkType": "external"}}, "'mode' argument should be one of the following: match, no-match. You have: ['match']", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [parameter_mode_value]) -def test_parameter_mode_value(check_type_str, evaluate_args, expected_results): - """Validate that CheckType parameter 'mode' has value of typ str.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - regex_no_params = ( "regex", {"regexregex": ".*UNDERLAY.*", "mode": "match"}, "'regex' argument is mandatory for Regex Check Type.", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [regex_no_params]) -def test_regex_param(check_type_str, evaluate_args, expected_results): - """Validate that CheckType regex has 'params' key.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - regex_wrong_type = ( "regex", {"regex": [".*UNDERLAY.*"], "mode-no-mode": "match"}, "'regex' argument must be a string. You have: .", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [regex_wrong_type]) -def test_regex_value_type(check_type_str, evaluate_args, expected_results): - """Validate that CheckType regex 'params' value type.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - regex_no_mode = ( "regex", {"regex": ".*UNDERLAY.*", "mode-no-mode": "match"}, "'mode' argument is mandatory for Regex Check Type.", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [regex_no_mode]) -def test_regex_mode(check_type_str, evaluate_args, expected_results): - """Validate that CheckType regex has 'mode' key.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - regex_mode_value = ( "regex", {"regex": ".*UNDERLAY.*", "mode": "match-no-match"}, "'mode' argument should be ['match', 'no-match']. You have: match-no-match", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [regex_mode_value]) -def test_regex_mode_value(check_type_str, evaluate_args, expected_results): - """Validate that CheckType regex 'mode' has value of typ str.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - operator_params = ( "operator", {"my_params": {"mode": "not-in", "operator_data": [20, 40, 60]}}, "'params' argument must be provided. You have: my_params.", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params]) -def test_operator_params(check_type_str, evaluate_args, expected_results): - """Validate that CheckType operator if has 'params' argument.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - operator_params_mode = ( "operator", {"params": {"no-mode": "not-in", "not-operator_data": [20, 40, 60]}}, "'mode' and 'operator_data' arguments must be provided. You have: ['no-mode', 'not-operator_data'].", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_mode]) -def test_operator_params_mode(check_type_str, evaluate_args, expected_results): - """Validate that CheckType operator if has 'mode' and 'operator_data' arguments.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - operator_params_wrong_operator = ( "operator", {"params": {"mode": "random", "operator_data": [20, 40, 60]}}, "'params' value must be one of the following: ['is-in', 'not-in', 'in-range', 'not-range', 'all-same', 'is-gt', 'is-lt', 'contains', 'not-contains']. You have: random", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_wrong_operator]) -def test_operator_params_wrong_operator(check_type_str, evaluate_args, expected_results): - """Validate that CheckType operator 'mode' and 'operator_data' values arguments.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - operator_params_in = ( "operator", {"params": {"mode": "in-range", "operator_data": "string"}}, "check options ('is-in', 'not-in', 'in-range', 'not-range') must have value of type list or tuple. i.e: dict(not-in=('Idle', 'Down'). You have: string of type .", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_in]) -def test_operator_params_in(check_type_str, evaluate_args, expected_results): - """Validate that CheckType operator range arguments data-type.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - operator_params_in_range = ( "operator", {"params": {"mode": "in-range", "operator_data": (0, "1")}}, "'range' check-option in-range must have value of type list or tuple with items of type float or int. i.e: dict(not-range=(70000000, 80000000). You have: (0, '1').", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_in_range]) -def test_operator_params_in_range(check_type_str, evaluate_args, expected_results): - """Validate that CheckType operator range values data-type.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - operator_params_in_range_lower_than = ( "operator", {"params": {"mode": "in-range", "operator_data": (1, 0)}}, "'range' and 'not-range' must have value at index 0 lower than value at index 1. i.e: dict(not-range=(70000000, 80000000). You have: (1, 0).", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_in_range_lower_than]) -def test_operator_params_in_range_lower_than(check_type_str, evaluate_args, expected_results): - """Validate that CheckType operator range values order.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - operator_params_number = ( "operator", {"params": {"mode": "is-gt", "operator_data": "1"}}, "check options ('is-gt', 'is-lt') must have value of type float or int. You have: 1 of type ", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_number]) -def test_operator_params_in_params_number(check_type_str, evaluate_args, expected_results): - """Validate that CheckType operator gt/lt data-type.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - operator_params_contains = ( "operator", {"params": {"mode": "contains", "operator_data": 1}}, "check options ('contains', 'not-contains') must have value of type string. You have: 1 of type ", ) - - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_contains]) -def test_operator_params_contains(check_type_str, evaluate_args, expected_results): - """Validate that CheckType operator contains data-type.""" - check = CheckType.init(check_type_str) - - with pytest.raises(ValueError) as exc_info: - check.validate(**evaluate_args) - - assert exc_info.type is ValueError and exc_info.value.args[0] == expected_results - - operator_params_bool = ( "operator", {"params": {"mode": "all-same", "operator_data": 1}}, "check option all-same must have value of type bool. You have: 1 of type ", ) - -@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", [operator_params_bool]) -def test_operator_params_bool(check_type_str, evaluate_args, expected_results): - """Validate that CheckType operator all-same data-type.""" +all_tests = [ + tolerance_wrong_argumet, + tolerance_wrong_value, + parameter_no_params, + parameter_wrong_type, + parameter_no_mode, + parameter_mode_value, + regex_no_params, + regex_wrong_type, + regex_no_mode, + regex_mode_value, + operator_params, + operator_params_mode, + operator_params_wrong_operator, + operator_params_in, + operator_params_in_range, + operator_params_in_range_lower_than, + operator_params_number, + operator_params_contains, + operator_params_bool, +] + + +@pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", all_tests) +def test_tolerance_key_name(check_type_str, evaluate_args, expected_results): + """Validate that CheckType tolerance has `tolerance` key.""" check = CheckType.init(check_type_str) with pytest.raises(ValueError) as exc_info: From ea94bd3542624c0d1589894e95f890658a004c60 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 10 Mar 2022 09:07:12 +0100 Subject: [PATCH 37/80] Update as per comments --- netcompare/check_types.py | 2 +- tests/test_validates.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index c18e0cc..9e7cb61 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -275,7 +275,7 @@ def validate(**kwargs) -> None: # "in-range", "not-range" requires int or float where value at index 0 is lower than value at index 1 if params_key in ("in-range", "not-range"): if ( - not len(params_value) == 2 + len(params_value) != 2 or not isinstance(params_value[0], (int, float)) or not isinstance(params_value[1], (float, int)) ): diff --git a/tests/test_validates.py b/tests/test_validates.py index 4a87557..90507f1 100644 --- a/tests/test_validates.py +++ b/tests/test_validates.py @@ -123,7 +123,7 @@ @pytest.mark.parametrize("check_type_str, evaluate_args, expected_results", all_tests) def test_tolerance_key_name(check_type_str, evaluate_args, expected_results): - """Validate that CheckType tolerance has `tolerance` key.""" + """Test CheckType validate method for each check-type.""" check = CheckType.init(check_type_str) with pytest.raises(ValueError) as exc_info: From 30eacc3732f7108c4585505a72ec94de9afbcac6 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 10 Mar 2022 10:23:14 +0100 Subject: [PATCH 38/80] fix as in comment PR --- netcompare/check_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index 9e7cb61..9ff0177 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -260,7 +260,7 @@ def validate(**kwargs) -> None: ) # Validate "params" value is legal. - if not any(params_key in sub_element for element in valid_options for sub_element in element): + if all(params_key not in sub_element for element in valid_options for sub_element in element): raise ValueError( f"'params' value must be one of the following: {[sub_element for element in valid_options for sub_element in element]}. You have: {params_key}" ) From b90c6d6f5dfc0eb202ac3c91c3728af19d5406e0 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 17 Mar 2022 09:41:10 +0100 Subject: [PATCH 39/80] up to JMSPATH --- README.md | 119 ++++++++++++++++++++++++++++++++++++++++++-- docs/images/hld.png | Bin 0 -> 103309 bytes 2 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 docs/images/hld.png diff --git a/README.md b/README.md index 17bbd59..4a44c16 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,125 @@ # netcompare -netcompare is a python library targeted at intelligently deep diffing structured data objects of different types. In addition, netcompare provides some basic tests of keys and values within the data structure. Ultimately, this library is meant to be a light-weight way to compare structured output from network device 'show' commands. +This library is meant to be a light-weight way to compare structured output from network device `show` commands. `netcompare` is a python library targeted at intelligently deep diffing structured data objects of json type. In addition, `netcompare` provides some basic tests of keys and values within the data structure. + +The libraly heavely rely on [jmspath](https://jmespath.org/) for traversing the json object and find the wanted value to be evaluated. More on that later. ## Use Case -netcompare enables an easy and direct way to see the outcome of network change windows. The intended usage is to collect raw show command output before and after a change window. Prior to closing the change window, the results are compared to help determine if the change was successful and if the network is in an acceptable state. The output can be stored with the change's documentation for easy reference and proof of completion. +`netcompare` enables an easy and direct way to see the outcome of network configuration or operational status change. The intended usage is to collect structured `show` command output before and after a change window. Prior to closing the change window, the results are compared to help determine if the change was successful as intended and if the network is in an acceptable state. The output can be stored with the change's documentation for easy reference and proof of completion. + +## Library Architecture + +![netcompare HLD](./docs/images/hld.png) + +As first thing, an instance of `CheckType` object must be created passing one of the below check types as argument: + +- `exact_match` +- `tolerance` +- `parameters` +- `regex` +- `operator` + + +```python +my_check = "exact_match" +check = CheckType.init(my_check) +``` + +We would then need to define our json object used as reference data, as well as a JMSPATH expression to extract the value wanted and pass them to `get_value` method. Be aware! `netcompare` works with a customized version of JMSPATH. More on that later. + +```python +bgp_pre_change = "./pre/bgp.json" +bgp_jmspath_exp = "result[0].vrfs.default.peerList[*].[$peerAddress$,establishedTransitions]" +pre_value = check.get_value(bgp_pre_change, bgp_jmspath_exp) +``` + +Once extracted our pre-change value, we would need to evaluate it against our post-change value. In case of chec-type `exact_match` our post-value would be another json object: + +```python +bgp_post_change = "./post/bgp.json" +post_value = check.get_value(bgp_post_change, bgp_jmspath_exp) +``` + +Every check type expect different type of arguments. For example, in case of check type `tolerance` we would also need to pass `tolerance` argument; `parameters` expect only a dictionary... + +Now that we have pre and post data, we just need to compare them with `evaluate` method which will return our evaluation result. + +```python +results = check.evaluate(post_value, pre_value, **evaluate_args) +``` + +## Customized JMSPATH + +Since `netcompare` work with json object as data inputs, JMSPATH was the obvous choise for traversing the data and extract the value wanted from it. + +However, JMSPATH comes with a limitation where is not possible to define a `key` to which the `value` belongs to. + +Let's have a look to the below `show bgp` output example. + +```json +{ + "result": [ + { + "vrfs": { + "default": { + "peerList": [ + { + "linkType": "external", + "localAsn": "65130.1100", + "prefixesSent": 50, + "receivedUpdates": 0, + "peerAddress": "7.7.7.7", + "state": "Idle", + "updownTime": 1394, + "asn": "1.2354", + "routerId": "0.0.0.0" + }, + { + "linkType": "external", + "localAsn": "65130.1100", + "receivedUpdates": 0, + "peerAddress": "10.1.0.0", + "state": "Connected", + "updownTime": 1394, + "asn": "1.2354", + "routerId": "0.0.0.0" + } + ] + } + } + } + ] +} +``` +If we would define a JMSPATH expression to extract `state` we would have something like... + +```python +"result[0].vrfs.default.peerList[*].state +``` + +...which will return + +```python +["Idle", "Connected"] +``` + +How can we understand that `Idle` is relative to peer 7.7.7.7 and `Connected` to peer `10.1.0.0` ? +We could index the output but that would require some post-processing data. For that reason, `netcompare` use a customized version of JMSPATH where is possible to define a reference key for the value(s) wanted. The reference key must be within `$` sign anchors and defined in a list, together with the value(s): + +```python +"result[0].vrfs.default.peerList[*].[$peerAddress$,state] +``` + +That would give us... + +```python +{"7.7.7.7": ["Idle"], "10.1.0.0": ["Connected"]} + + +``` +## check typea explained -## Check Types ### exact_match diff --git a/docs/images/hld.png b/docs/images/hld.png new file mode 100644 index 0000000000000000000000000000000000000000..d9b9b2101b547422b3dfab9c8245e4064bf689ff GIT binary patch literal 103309 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 literal 0 HcmV?d00001 From c95162fc75ea729b44924bbd174fede4dec86576 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Fri, 18 Mar 2022 14:12:21 +0100 Subject: [PATCH 40/80] assert data type for exclude list --- netcompare/check_types.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index 9ff0177..b1fa8b8 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -57,6 +57,9 @@ def get_value(output: Union[Mapping, List], path: str, exclude: List = None) -> Evaluated data, may be anything depending on JMESPath used. """ if exclude and isinstance(output, Dict): + if not isinstance(exclude, list): + raise ValueError(f"Exclude list must be defined as a list. You have {type(exclude)}") + exclude_filter(output, exclude) # exclude unwanted elements if not path: From 475532bfe6c91c85a659a1e8f78db91b7a965c41 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Fri, 18 Mar 2022 14:12:37 +0100 Subject: [PATCH 41/80] update Readme up to exact-match --- README.md | 115 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 95 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 4a44c16..112e5d0 100644 --- a/README.md +++ b/README.md @@ -116,35 +116,110 @@ That would give us... ```python {"7.7.7.7": ["Idle"], "10.1.0.0": ["Connected"]} - ``` -## check typea explained +## Check types explained ### exact_match -exact_match is concerned about the value of the elements within the data structure. The keys and values should match between the pre and post values. +Check type `exact_match` is concerned about the value of the elements within the data structure. The keys and values should match between the pre and post values. A diff is generated between the two data sets. +As some outputs might be too verbose or includes fields that constantly change (i.e. interface counter), is possible to exclude a portion of data traversed by JMSPATH, defining a list of keys that we want to exclude. -``` -PASS --------------------- -pre: [{"A": 1}] -post: [{"A": 1}] -``` +Examples: -``` -FAIL --------------------- -pre: [{"A": 1}] -post: [{"A": 2}] -``` +```python +>>> from netcompare import CheckType +>>> pre_data = { + "jsonrpc": "2.0", + "id": "EapiExplorer-1", + "result": [ + { + "interfaces": { + "Management1": { + "lastStatusChangeTimestamp": 1626247820.0720868, + "lanes": 0, + "name": "Management1", + "interfaceStatus": "connected", + "autoNegotiate": "success", + "burnedInAddress": "08:00:27:e6:b2:f8", + "loopbackMode": "loopbackNone", + "interfaceStatistics": { + "inBitsRate": 3582.5323982177174, + "inPktsRate": 3.972702352461616, + "outBitsRate": 17327.65267220522, + "updateInterval": 300, + "outPktsRate": 2.216220664406746 + } + } + } + } + ] + } +>>> post_data = { + "jsonrpc": "2.0", + "id": "EapiExplorer-1", + "result": [ + { + "interfaces": { + "Management1": { + "lastStatusChangeTimestamp": 1626247821.123456, + "lanes": 0, + "name": "Management1", + "interfaceStatus": "down", + "autoNegotiate": "success", + "burnedInAddress": "08:00:27:e6:b2:f8", + "loopbackMode": "loopbackNone", + "interfaceStatistics": { + "inBitsRate": 3403.4362520883615, + "inPktsRate": 3.7424095978179257, + "outBitsRate": 16249.69114419833, + "updateInterval": 300, + "outPktsRate": 2.1111866059750692 + } + } + } + } + ] + } +>>> my_jmspath = "result[*]" +>>> exclude_fields = ["interfaceStatistics", "lastStatusChangeTimestamp"] +>>> # Create an instance of CheckType object with 'exact_match' as check-type argument. +>>> my_check = CheckType.init(check_type="exact_match") +>>> my_check +>>> +>>> # Extract the wanted value from pre_dat to later compare with post_data. As we want compare all the body (excluding "interfaceStatistics"), we do not need to define any reference key +>>> pre_value = my_check.get_value(output=pre_data, path=my_jmspath, exclude=exclude_fields) +>>> pre_value +>>> [{'interfaces': {'Management1': {'lastStatusChangeTimestamp': 1626247820.0720868, 'lanes': 0, 'name': 'Management1', 'interfaceStatus': 'connected', 'autoNegotiate': 'success', 'burnedInAddress': '08:00:27:e6:b2:f8', 'loopbackMode': 'loopbackNone'}}}] +>>> post_value = my_check.get_value(output=post_data, path=my_jmspath, exclude=exclude_fields) +>>> post_value +>>> [{'interfaces': {'Management1': {'lastStatusChangeTimestamp': 1626247821.123456, 'lanes': 0, 'name': 'Management1', 'interfaceStatus': 'down', 'autoNegotiate': 'success', 'burnedInAddress': '08:00:27:e6:b2:f8', 'loopbackMode': 'loopbackNone'}}}] +>>> # The pre_value is our intended state for interface Management1, therefore we will use it as reference data. post_value will be our value_to_compare as we want compare the actual state of our interface Management1 (perhaps after a network maintenance) with the its status before the change. +>>> result = my_check.evaluate(value_to_compare=post_value, reference_data=pre_value) +>>> result +>>> ({'interfaces': {'Management1': {'interfaceStatus': {'new_value': 'down', 'old_value': 'connected'}}}}, False) +``` + +As we can see, we return a tuple containing a diff betwee the pre and post data as well as a boolean for the overall test result. In this case a diff has been found so the status of the test is `False`. + +Let's see a better way to run `exact_match` for this specific case. Since we are interested only into `interfaceStatus` we could write our JMSPATH expression as: + +```python + +>>> pre_value = my_check.get_value(output=pre_data, path=my_jmspath) +>>> pre_value +['connected'] +>>> post_value = my_check.get_value(output=post_data, path=my_jmspath) +>>> post_value +['down'] +>>> result = my_check.evaluate(value_to_compare=post_value, reference_data=pre_value) +>>> result +({'Management1': {'new_value': 'down', 'old_value': 'connected'}}, False) ``` -FAIL --------------------- -pre: [{ "A": 1}] -post: [] -``` +Targeting only `interfaceStatus` key, we would need to define a reference key (in this case `$Management1$`) as well as we would not need to define any exclusion list. + +This logic applies to all check-types available in `netcompare` ### parameter_match From 046684393865a073d3de8a1fe6576e74b2eb2928 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Tue, 22 Mar 2022 09:13:58 +0100 Subject: [PATCH 42/80] stash commit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 112e5d0..8ed498a 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ As we can see, we return a tuple containing a diff betwee the pre and post data Let's see a better way to run `exact_match` for this specific case. Since we are interested only into `interfaceStatus` we could write our JMSPATH expression as: ```python - +>>> my_jmspath = "result[*].interfaces.Management1.[$name$,interfaceStatus]" >>> pre_value = my_check.get_value(output=pre_data, path=my_jmspath) >>> pre_value ['connected'] @@ -217,7 +217,7 @@ Let's see a better way to run `exact_match` for this specific case. Since we are >>> result ({'Management1': {'new_value': 'down', 'old_value': 'connected'}}, False) ``` -Targeting only `interfaceStatus` key, we would need to define a reference key (in this case `$Management1$`) as well as we would not need to define any exclusion list. +Targeting only `interfaceStatus` key, we would need to define a reference key (in this case `$name$`) as well as we would not need to define any exclusion list. This logic applies to all check-types available in `netcompare` From cb98aa2d0a0d6065996c900834aea0312ca61c54 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Tue, 22 Mar 2022 09:53:44 +0100 Subject: [PATCH 43/80] propose fix ofr issue 44 --- netcompare/check_types.py | 6 ++++++ tests/mock/raw_novalue_exclude/pre.json | 2 +- tests/test_diff_generator.py | 10 ++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index 9ff0177..0047f8a 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -82,7 +82,13 @@ def get_value(output: Union[Mapping, List], path: str, exclude: List = None) -> paired_key_value = associate_key_of_my_value(jmespath_value_parser(path), values) if re.search(r"\$.*\$", path): # normalize + wanted_reference_keys = jmespath.search(jmespath_refkey_parser(path), output) + try: + if isinstance(wanted_reference_keys[0], list): + wanted_reference_keys = [item for sublist in wanted_reference_keys for item in sublist] + except KeyError: + pass list_of_reference_keys = keys_cleaner(wanted_reference_keys) return keys_values_zipper(list_of_reference_keys, paired_key_value) diff --git a/tests/mock/raw_novalue_exclude/pre.json b/tests/mock/raw_novalue_exclude/pre.json index 419fa0f..fbbdac6 100644 --- a/tests/mock/raw_novalue_exclude/pre.json +++ b/tests/mock/raw_novalue_exclude/pre.json @@ -8,7 +8,7 @@ "lastStatusChangeTimestamp": 1626247820.0720868, "lanes": 0, "name": "Management1", - "interfaceStatus": "connected", + "interfaceStatus": "down", "autoNegotiate": "success", "burnedInAddress": "08:00:27:e6:b2:f8", "loopbackMode": "loopbackNone", diff --git a/tests/test_diff_generator.py b/tests/test_diff_generator.py index 63c5fa8..cd579c7 100644 --- a/tests/test_diff_generator.py +++ b/tests/test_diff_generator.py @@ -57,6 +57,7 @@ "result": { "interfaces": { "Management1": { + "interfaceStatus": {"new_value": "connected", "old_value": "down"}, "lastStatusChangeTimestamp": {"new_value": 1626247821.123456, "old_value": 1626247820.0720868}, "interfaceAddress": { "primaryIp": {"address": {"new_value": "10.2.2.15", "old_value": "10.0.2.15"}} @@ -148,6 +149,14 @@ }, ) +exact_match_test_issue_44 = ( + "raw_novalue_exclude", + "result[*].interfaces.*.[$name$,interfaceStatus]", + [], + {"Management1": {"interfaceStatus": {"new_value": "connected", "old_value": "down"}}}, +) + + eval_tests = [ exact_match_of_global_peers_via_napalm_getter, exact_match_of_bgp_peer_caps_via_api, @@ -161,6 +170,7 @@ exact_match_multi_nested_list, exact_match_textfsm_ospf_int_br_raw, exact_match_textfsm_ospf_int_br_normalized, + exact_match_test_issue_44, ] From fc02e7bd995553ed751781bb1f31f4e5e1c83a46 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Tue, 22 Mar 2022 10:10:10 +0100 Subject: [PATCH 44/80] using flatten list utils --- netcompare/check_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index 0047f8a..b1284df 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -86,7 +86,7 @@ def get_value(output: Union[Mapping, List], path: str, exclude: List = None) -> wanted_reference_keys = jmespath.search(jmespath_refkey_parser(path), output) try: if isinstance(wanted_reference_keys[0], list): - wanted_reference_keys = [item for sublist in wanted_reference_keys for item in sublist] + wanted_reference_keys = flatten_list(wanted_reference_keys)[0] except KeyError: pass list_of_reference_keys = keys_cleaner(wanted_reference_keys) From b724db2073e1a6b586491df80aa38eb44ee30dc2 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Tue, 22 Mar 2022 10:31:19 +0100 Subject: [PATCH 45/80] add test with index in result --- tests/test_diff_generator.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_diff_generator.py b/tests/test_diff_generator.py index cd579c7..03b1a20 100644 --- a/tests/test_diff_generator.py +++ b/tests/test_diff_generator.py @@ -149,13 +149,20 @@ }, ) -exact_match_test_issue_44 = ( +exact_match_test_issue_44_case_1 = ( "raw_novalue_exclude", "result[*].interfaces.*.[$name$,interfaceStatus]", [], {"Management1": {"interfaceStatus": {"new_value": "connected", "old_value": "down"}}}, ) +exact_match_test_issue_44_case_2 = ( + "raw_novalue_exclude", + "result[0].interfaces.*.[$name$,interfaceStatus]", + [], + {"Management1": {"interfaceStatus": {"new_value": "connected", "old_value": "down"}}}, +) + eval_tests = [ exact_match_of_global_peers_via_napalm_getter, @@ -170,7 +177,8 @@ exact_match_multi_nested_list, exact_match_textfsm_ospf_int_br_raw, exact_match_textfsm_ospf_int_br_normalized, - exact_match_test_issue_44, + exact_match_test_issue_44_case_1, + exact_match_test_issue_44_case_2, ] From 3f73a396fb9fc4ec4afada5145adf21b267b9232 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Wed, 23 Mar 2022 11:15:01 +0100 Subject: [PATCH 46/80] work on readme.md --- tests/test_type_checks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_type_checks.py b/tests/test_type_checks.py index a2a30ce..10af483 100644 --- a/tests/test_type_checks.py +++ b/tests/test_type_checks.py @@ -338,3 +338,6 @@ def test_regex_match(filename, check_type_str, evaluate_args, path, expected_res assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( output=actual_results, expected_output=expected_result ) + + + From e09ed3932d5a5db47c2de9c48e425c67a3d6fdc8 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Wed, 23 Mar 2022 11:15:10 +0100 Subject: [PATCH 47/80] work on readme --- README.md | 62 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 8ed498a..bcef467 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ As we can see, we return a tuple containing a diff betwee the pre and post data Let's see a better way to run `exact_match` for this specific case. Since we are interested only into `interfaceStatus` we could write our JMSPATH expression as: ```python ->>> my_jmspath = "result[*].interfaces.Management1.[$name$,interfaceStatus]" +>>> my_jmspath = "result[*].interfaces.*.[$name$,interfaceStatus]" >>> pre_value = my_check.get_value(output=pre_data, path=my_jmspath) >>> pre_value ['connected'] @@ -215,39 +215,57 @@ Let's see a better way to run `exact_match` for this specific case. Since we are ['down'] >>> result = my_check.evaluate(value_to_compare=post_value, reference_data=pre_value) >>> result -({'Management1': {'new_value': 'down', 'old_value': 'connected'}}, False) +({"Management1": {"interfaceStatus": {"new_value": "connected", "old_value": "down"}}}, False) ``` Targeting only `interfaceStatus` key, we would need to define a reference key (in this case `$name$`) as well as we would not need to define any exclusion list. -This logic applies to all check-types available in `netcompare` +The anchor logic for reference key applies to all check-types available in `netcompare` + ### parameter_match parameter_match provides a way to match keys and values in the output with known good values. -The test defines key/value pairs known to be the good value - type `dict()` - to match against the parsed output. The test FAILS if any status has changed based on what is defined in pre/post. If there are new values not contained in the input/test value, that will not count as a failure. +The test defines key/value pairs known to be the good value - type `dict()` - as well as a mode - `match`, `no-match` - to match or not against the parsed output. The test fails if any status has changed based on what is defined in pre/post. If there are new values not contained in the input/test value, that will not count as a failure. Examples: -``` -{"A": 1, "B": 2} - -PASS/PASS -{"A": 1, "B": 2} -{"A": 1, "B": 2} - -PASS/PASS -{"A": 1, "B": 2} -{"A": 1, "B": 2, "C": 3} - -PASS/FAIL -{"A": 1, "B": 2} -{"A": 1, "B": 666} - -FAIL/PASS -{"A": 1} -{"A": 1, "B": 2} +```python +>>> from netcompare import CheckType +>>> post_data = { +... "jsonrpc": "2.0", +... "id": "EapiExplorer-1", +... "result": [ +... { +... "interfaces": { +... "Management1": { +... "lastStatusChangeTimestamp": 1626247821.123456, +... "lanes": 0, +... "name": "Management1", +... "interfaceStatus": "down", +... "autoNegotiate": "success", +... "burnedInAddress": "08:00:27:e6:b2:f8", +... "loopbackMode": "loopbackNone", +... "interfaceStatistics": { +... "inBitsRate": 3403.4362520883615, +... "inPktsRate": 3.7424095978179257, +... "outBitsRate": 16249.69114419833, +... "updateInterval": 300, +... "outPktsRate": 2.1111866059750692 +... } +... } +... } +... } +... ] +>>> my_check = CheckType.init(check_type="parameter_match") +>>> my_jmspath = "result[*].interfaces.*.[$name$,interfaceStatus,autoNegotiate]" +>>> post_value = my_check.get_value(output=pre_data, path=my_jmspath) +>>> my_parameter_match = {"mode": "match", "params": {"interfaceStatus": "connected", "autoNegotiate": "success"}} +>>> my_parameter_match = {"mode": "match", "params": {"interfaceStatus": "connected", "autoNegotiate": "success"}} +>>> actual_results = my_check.evaluate(post_value, **my_parameter_match) +>>> actual_result +({}, True) ``` In network data, this could be a state of bgp neighbors being Established or the connectedness of certain interfaces being up. From 9d57bf48eb91dc3e10423adb196af5869fef9531 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Wed, 23 Mar 2022 12:28:25 +0100 Subject: [PATCH 48/80] readne up to tolerance --- README.md | 114 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 82 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index bcef467..8c44e2e 100644 --- a/README.md +++ b/README.md @@ -260,50 +260,100 @@ Examples: ... ] >>> my_check = CheckType.init(check_type="parameter_match") >>> my_jmspath = "result[*].interfaces.*.[$name$,interfaceStatus,autoNegotiate]" ->>> post_value = my_check.get_value(output=pre_data, path=my_jmspath) ->>> my_parameter_match = {"mode": "match", "params": {"interfaceStatus": "connected", "autoNegotiate": "success"}} +>>> post_value = my_check.get_value(output=post_data, path=my_jmspath) +>>> # mode: match - Match in the ouptut what is defined under 'params' >>> my_parameter_match = {"mode": "match", "params": {"interfaceStatus": "connected", "autoNegotiate": "success"}} >>> actual_results = my_check.evaluate(post_value, **my_parameter_match) ->>> actual_result -({}, True) +>>> actual_results +({'Management1': {'interfaceStatus': 'down'}}, False) +>>> # mode: no-match - Return what does nto match in the ouptut as defined under 'params' +>>> my_parameter_match = {"mode": "no-match", "params": {"interfaceStatus": "connected", "autoNegotiate": "success"}} +>>> actual_results = my_check.evaluate(post_value, **my_parameter_match) +>>> actual_results +({'Management1': {'autoNegotiate': 'success'}}, False ``` In network data, this could be a state of bgp neighbors being Established or the connectedness of certain interfaces being up. ### Tolerance -The `tolerance` test defines a percentage of differing `float()` between the pre and post checks. The threshold is defined as a percentage that can be different either from the value stated in pre and post fields. +The `tolerance` test defines a percentage of differing `float()` between pre and post checks numeric value. The threshold is defined as a percentage that can be different either from the value stated in pre and post fields. The threshold must be `float > 0`, is percentge based, and will be counted as a range centered on the value in pre and post. -``` -Pre: 100 -Post: 110 -Threshold: 10 ------------------ -PASS/PASS -Pre: [100] -Post: [110] - -PASS/PASS -Pre: [100] -Post: [120] - -PASS/PASS -Pre: [100] -Post: [100] - -PASS/FAIL -Pre: [100] -Post: [90] - -PASS/FAIL -Pre: [90] -Post: [20] +Lets have a look to a couple of examples: -FAIL/FAIL -Pre: [80] -Post: [120] +```python +>>> pre_data = { +... "global": { +... "peers": { +... "10.1.0.0": { +... "address_family": { +... "ipv4": { +... "accepted_prefixes": 900, +... "received_prefixes": 999, +... "sent_prefixes": 1011 +... }, +... "ipv6": { +... "accepted_prefixes": 1000, +... "received_prefixes": 1000, +... "sent_prefixes": 1000 +... } +... }, +... "description": "", +... "is_enabled": True, +... "is_up": True, +... "local_as": 4268360780, +... "remote_as": 67890, +... "remote_id": "0.0.0.0", +... "uptime": 1783 +... } +... } +... } +... } +>>> post_data = { +... "global": { +... "peers": { +... "10.1.0.0": { +... "address_family": { +... "ipv4": { +... "accepted_prefixes": 500, +... "received_prefixes": 599, +... "sent_prefixes": 511 +... }, +... "ipv6": { +... "accepted_prefixes": 1000, +... "received_prefixes": 1000, +... "sent_prefixes": 1000 +... } +... }, +... "description": "", +... "is_enabled": True, +... "is_up": True, +... "local_as": 4268360780, +... "remote_as": 67890, +... "remote_id": "0.0.0.0", +... "uptime": 1783 +... } +... } +... } +... } +>>> my_check = CheckType.init(check_type="tolerance") +>>> my_jmspath = "global.$peers$.*.*.ipv4.[accepted_prefixes,received_prefixes,sent_prefixes]" +>>> # Tolerance define as 10% delta between pre and post values +>>> my_tolerance_arguments = {"tolerance": 10} +>>> pre_value = my_check.get_value(pre_data, my_jmspath) +>>> post_value = my_check.get_value(post_data, my_jmspath) +>>> actual_results = my_check.evaluate(post_value, pre_value, **my_tolerance_arguments) +>>> # Netcompare returns the value that are not within the 10% +>>> actual_results +({'10.1.0.0': {'accepted_prefixes': {'new_value': 500, 'old_value': 900}, 'received_prefixes': {'new_value': 599, 'old_value': 999}, 'sent_prefixes': {'new_value': 511, 'old_value': 1011}}}, False) +>>> # Let's difine a higher tolerance +>>> my_tolerance_arguments = {"tolerance": 80} +>>> # In this case, all the values are within the 80% so the check is passed. +>>> actual_results = my_check.evaluate(post_value, pre_value, **my_tolerance_arguments) +>>> actual_results +({}, True) ``` This test can test the tolerance for changing quantities of certain things such as routes, or L2 or L3 neighbors. It could also test actual outputted values such as transmitted light levels for optics. From 88ba82e8f86caa5e504539fa266ffcab0e653dcc Mon Sep 17 00:00:00 2001 From: Network to Code Date: Wed, 23 Mar 2022 13:45:58 +0100 Subject: [PATCH 49/80] up to regex --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/README.md b/README.md index 8c44e2e..a23cfb0 100644 --- a/README.md +++ b/README.md @@ -358,6 +358,59 @@ Lets have a look to a couple of examples: This test can test the tolerance for changing quantities of certain things such as routes, or L2 or L3 neighbors. It could also test actual outputted values such as transmitted light levels for optics. + +### Regex + +The `regex` check type evaluates data against a python regular expression defined as check-tyoe argument. As per `parameter_match` the option `match`, `no-match` is also supported. + +Let's run an example where e want to check that `burnedInAddress` key has a string rappresentig a MAC Address as value + +```python +>>> data = { +... "jsonrpc": "2.0", +... "id": "EapiExplorer-1", +... "result": [ +... { +... "interfaces": { +... "Management1": { +... "lastStatusChangeTimestamp": 1626247821.123456, +... "lanes": 0, +... "name": "Management1", +... "interfaceStatus": "down", +... "autoNegotiate": "success", +... "burnedInAddress": "08:00:27:e6:b2:f8", +... "loopbackMode": "loopbackNone", +... "interfaceStatistics": { +... "inBitsRate": 3403.4362520883615, +... "inPktsRate": 3.7424095978179257, +... "outBitsRate": 16249.69114419833, +... "updateInterval": 300, +... "outPktsRate": 2.1111866059750692 +... } +... } +... } +... } +... ] +... } +>>> # Python regex for matching MAC Address string +>>> regex_args = {"regex": "(?:[0-9a-fA-F]:?){12}", "mode": "match"} +>>> path = "result[*].interfaces.*.[$name$,burnedInAddress]" +>>> check = CheckType.init(check_type="regex") +>>> value = check.get_value(output=data, path=path) +>>> value +[{'Management1': {'burnedInAddress': '08:00:27:e6:b2:f8'}}] +>>> result = check.evaluate(value, **regex_args) +>>> # The test is passed as the burnedInAddress value match our regex +>>> result +({}, True) +>>> # What if we want "no-match"? +>>> regex_args = {"regex": "(?:[0-9a-fA-F]:?){12}", "mode": "no-match"} +>>> result = check.evaluate(value, **regex_args) +>>> # Netcompare return the failing data as the regex match the value +>>> result +({'Management1': {'burnedInAddress': '08:00:27:e6:b2:f8'}}, False) +``` + ## How To Define A Check The check requires at least 2 arguments: `check_type` which can be `exact_match`, `tolerance`, `parameter_match` or `path`. The `path` argument is JMESPath based but uses `$` to anchor the reference key needed to generate the diff - more on this later. From 3a33cbdb34accbe90335ff1bc3a5558ae5d0336b Mon Sep 17 00:00:00 2001 From: Network to Code Date: Wed, 23 Mar 2022 14:27:34 +0100 Subject: [PATCH 50/80] work on operator --- README.md | 240 ++++++-------------------------------- tests/test_type_checks.py | 86 ++++++++++++++ 2 files changed, 119 insertions(+), 207 deletions(-) diff --git a/README.md b/README.md index a23cfb0..875d0b3 100644 --- a/README.md +++ b/README.md @@ -411,231 +411,57 @@ Let's run an example where e want to check that `burnedInAddress` key has a stri ({'Management1': {'burnedInAddress': '08:00:27:e6:b2:f8'}}, False) ``` -## How To Define A Check +### Operator -The check requires at least 2 arguments: `check_type` which can be `exact_match`, `tolerance`, `parameter_match` or `path`. The `path` argument is JMESPath based but uses `$` to anchor the reference key needed to generate the diff - more on this later. +Operator is a check which includes an array o different evaluation logic. Here a summary of the available options: -Example #1: -Run an `exact_match` between 2 files where `peerAddress` is the reference key (note the anchors used - `$` ) for `statebgpPeerCaps`. In this example, key and value are at the same level. +#### `in` operators -Check Definition: -``` -{ - "check_type": "exact_match", - "path": "result[0].vrfs.default.peerList[*].[$peerAddress$,statebgpPeerCaps]", -} -``` -Show Command Output - Pre: -``` -{ - "jsonrpc": "2.0", - "id": "EapiExplorer-1", - "result": [ - { - "vrfs": { - "default": { - "peerList": [ - { - "linkType": "external", - "localAsn": "65130.1100", - "prefixesSent": 52, - "receivedUpdates": 0, - "peerAddress": "7.7.7.7", - "v6PrefixesSent": 0, - "establishedTransitions": 0, - "bgpPeerCaps": 75759616, - "negotiatedVersion": 0, - "sentUpdates": 0, - "v4SrTePrefixesSent": 0, - "lastEvent": "NoEvent", - "configuredKeepaliveTime": 5, - "ttl": 2, - "state": "Idle", - ... -``` -Show Command Output - Post: -``` -{ - "jsonrpc": "2.0", - "id": "EapiExplorer-1", - "result": [ - { - "vrfs": { - "default": { - "peerList": [ - { - "linkType": "external", - "localAsn": "65130.1100", - "prefixesSent": 50, - "receivedUpdates": 0, - "peerAddress": "7.7.7.7", - "v6PrefixesSent": 0, - "establishedTransitions": 0, - "bgpPeerCaps": 75759616, - "negotiatedVersion": 0, - "sentUpdates": 0, - "v4SrTePrefixesSent": 0, - "lastEvent": "NoEvent", - "configuredKeepaliveTime": 5, - "ttl": 2, - "state": "Connected", -``` + 1. is-in: Check if the specified element string value is included in a given list of strings. + - is-in: ["down", "up"] + check if value is in list (down, up) -Result: -``` -{ - "7.7.7.7": { - "state": { - "new_value": "Connected", - "old_value": "Idle" - } - } -} -``` -`result[0].vrfs.default.peerList[*].$peerAddress$` is the reference key (`7.7.7.7`) that we want associated to our value used to generate diff (`state`)...otherwise, how can we understand which `statebgpPeerCaps` is associated to which `peerAddress` ? + 2. not-in: Check if the specified element string value is NOT included in a given list of strings. + - not-in: ["down", "up"] + check if value is not in list (down, up) -Example #2: + 3. in-range: Check if the value of a specified element is in the given numeric range. + - in-range: [ 20, 70 ] + check if value is in range between 20 nad 70 -Similar to Example 1 but with key and value on different level. In this example `peers` will be our reference key, `accepted_prefixes` and `received_prefixes` the values used to generate diff. + 4. not-range: Check if the value of a specified element is outside of a given numeric range. + - not-range: [5 , 40] + checks if value is not in range between 5 and 40 -Check Definition: -``` -{ - "check_type": "exact_match", - "path": "global.$peers$.*.*.ipv4.[accepted_prefixes,received_prefixes]", -} -``` - -Show Command Output - Pre: -``` -{ - "global": { - "peers": { - "10.1.0.0": { - "address_family": { - "ipv4": { - "accepted_prefixes": -9, - "received_prefixes": 0, - "sent_prefixes": 0 - }, - .... -``` +#### `bool` operators -Show Command Output - Post: -``` -{ - "global": { - "peers": { - "10.1.0.0": { - "address_family": { - "ipv4": { - "accepted_prefixes": -1, - "received_prefixes": 0, - "sent_prefixes": 0 - ... -``` + 1. all-same: Check if all content values for the specified element are the same. It can also be used to compare all content values against another specified element. + - all-same: flap-count + checks if all values of node in given path is same or not. -Result: -``` -{ - "10.1.0.0": { - "accepted_prefixes": { - "new_value": -1, - "old_value": -9 - } - }, - ... -``` +#### `str` operators -Example #3: + 1. contains: determines if an element string value contains the provided test-string value. + - contains: "underlay" + checks if "underlay" is present in given data or not. -Similar to Example 1 and 2 but without a reference key defined in `path`, plus some excluded fields to remove verbosity from diff output + 2. not-contains: determines if an element string value does not contain the provided test-string value. + - not-contains: "overlay" + checks if "overlay" is present in given node or not. -Check Definition: -``` -{ - "check_type": "exact_match", - "path": "result[*]", - "exclude": ["interfaceStatistics", "interfaceCounters"], -} -``` +#### `int`, `float` operators -Show Command Output - Pre: -``` -{ - "jsonrpc": "2.0", - "id": "EapiExplorer-1", - "result": [ - { - "interfaces": { - "Management1": { - "lastStatusChangeTimestamp": 1626247820.0720868, - "lanes": 0, - "name": "Management1", - "interfaceStatus": "connected", - "autoNegotiate": "success", - "burnedInAddress": "08:00:27:e6:b2:f8", - "loopbackMode": "loopbackNone", - "interfaceStatistics": { - "inBitsRate": 3582.5323982177174, - "inPktsRate": 3.972702352461616, - "outBitsRate": 17327.65267220522, - "updateInterval": 300, - "outPktsRate": 2.216220664406746 - }, - ... -``` + 1. is-gt: Check if the value of a specified element is greater than a given numeric value. + - is-gt: 2 + checks if value should be greater than 2 -Show Command Output - Post: -``` -{ - "jsonrpc": "2.0", - "id": "EapiExplorer-1", - "result": [ - { - "interfaces": { - "Management1": { - "lastStatusChangeTimestamp": 1626247821.123456, - "lanes": 0, - "name": "Management1", - "interfaceStatus": "connected", - "autoNegotiate": "success", - "burnedInAddress": "08:00:27:e6:b2:f8", - "loopbackMode": "loopbackNone", - "interfaceStatistics": { - "inBitsRate": 3403.4362520883615, - "inPktsRate": 3.7424095978179257, - "outBitsRate": 16249.69114419833, - "updateInterval": 300, - "outPktsRate": 2.1111866059750692 - }, - ... -``` + 2. is-lt: Check if the value of a specified element is lesser than a given numeric value. + - is-lt: 55 + checks if value is lower than 55 or not. -Result: -``` -{ - "interfaces": { - "Management1": { - "lastStatusChangeTimestamp": { - "new_value": 1626247821.123456, - "old_value": 1626247820.0720868 - }, - "interfaceAddress": { - "primaryIp": { - "address": { - "new_value": "10.2.2.15", - "old_value": "10.0.2.15" - } - } - } - } - } -} -``` See [test](./tests) folder for more examples. diff --git a/tests/test_type_checks.py b/tests/test_type_checks.py index 10af483..d5bed20 100644 --- a/tests/test_type_checks.py +++ b/tests/test_type_checks.py @@ -340,4 +340,90 @@ def test_regex_match(filename, check_type_str, evaluate_args, path, expected_res ) +paramere_match_issue_45_case1 = [ + { + "jsonrpc": "2.0", + "id": "EapiExplorer-1", + "result": [ + { + "interfaces": { + "Management1": { + "lastStatusChangeTimestamp": 1626247821.123456, + "lanes": 0, + "name": "Management1", + "interfaceStatus": "down", + "autoNegotiate": "success", + "burnedInAddress": "08:00:27:e6:b2:f8", + "loopbackMode": "loopbackNone", + "interfaceStatistics": { + "inBitsRate": 3403.4362520883615, + "inPktsRate": 3.7424095978179257, + "outBitsRate": 16249.69114419833, + "updateInterval": 300, + "outPktsRate": 2.1111866059750692, + }, + } + } + } + ], + }, + "parameter_match", + {"mode": "match", "params": {"interfaceStatus": "connected", "autoNegotiate": "success"}}, + "result[*].interfaces.*.[$name$,interfaceStatus,autoNegotiate]", + ({"Management1": {"interfaceStatus": "down"}}, False) +] + +@pytest.mark.parametrize("data, check_type_str, evaluate_args, path, expected_result", [paramere_match_issue_45_case1]) +def test_issue_45_case1(data, check_type_str, evaluate_args, path, expected_result): + """Validate regex check type.""" + check = CheckType.init(check_type_str) + # There is not concept of "pre" and "post" in parameter_match. + value = check.get_value(data, path) + actual_results = check.evaluate(value, **evaluate_args) + assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( + output=actual_results, expected_output=expected_result + ) + +paramere_match_issue_45_case2 = [ + { + "jsonrpc": "2.0", + "id": "EapiExplorer-1", + "result": [ + { + "interfaces": { + "Management1": { + "lastStatusChangeTimestamp": 1626247821.123456, + "lanes": 0, + "name": "Management1", + "interfaceStatus": "down", + "autoNegotiate": "success", + "burnedInAddress": "08:00:27:e6:b2:f8", + "loopbackMode": "loopbackNone", + "interfaceStatistics": { + "inBitsRate": 3403.4362520883615, + "inPktsRate": 3.7424095978179257, + "outBitsRate": 16249.69114419833, + "updateInterval": 300, + "outPktsRate": 2.1111866059750692, + }, + } + } + } + ], + }, + "parameter_match", + {"mode": "no-match", "params": {"interfaceStatus": "connected", "autoNegotiate": "success"}}, + "result[*].interfaces.*.[$name$,interfaceStatus,autoNegotiate]", + ({'Management1': {'autoNegotiate': 'success'}}, False) +] +@pytest.mark.parametrize("data, check_type_str, evaluate_args, path, expected_result", [paramere_match_issue_45_case2]) +def test_issue_45_case2(data, check_type_str, evaluate_args, path, expected_result): + """Validate regex check type.""" + check = CheckType.init(check_type_str) + # There is not concept of "pre" and "post" in parameter_match. + value = check.get_value(data, path) + actual_results = check.evaluate(value, **evaluate_args) + assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( + output=actual_results, expected_output=expected_result + ) \ No newline at end of file From 702f0819617354a4e29ffd7ffa5e34fd301b2184 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Wed, 23 Mar 2022 14:29:35 +0100 Subject: [PATCH 51/80] remove test for issue 45 --- tests/test_type_checks.py | 89 --------------------------------------- 1 file changed, 89 deletions(-) diff --git a/tests/test_type_checks.py b/tests/test_type_checks.py index d5bed20..a2a30ce 100644 --- a/tests/test_type_checks.py +++ b/tests/test_type_checks.py @@ -338,92 +338,3 @@ def test_regex_match(filename, check_type_str, evaluate_args, path, expected_res assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( output=actual_results, expected_output=expected_result ) - - -paramere_match_issue_45_case1 = [ - { - "jsonrpc": "2.0", - "id": "EapiExplorer-1", - "result": [ - { - "interfaces": { - "Management1": { - "lastStatusChangeTimestamp": 1626247821.123456, - "lanes": 0, - "name": "Management1", - "interfaceStatus": "down", - "autoNegotiate": "success", - "burnedInAddress": "08:00:27:e6:b2:f8", - "loopbackMode": "loopbackNone", - "interfaceStatistics": { - "inBitsRate": 3403.4362520883615, - "inPktsRate": 3.7424095978179257, - "outBitsRate": 16249.69114419833, - "updateInterval": 300, - "outPktsRate": 2.1111866059750692, - }, - } - } - } - ], - }, - "parameter_match", - {"mode": "match", "params": {"interfaceStatus": "connected", "autoNegotiate": "success"}}, - "result[*].interfaces.*.[$name$,interfaceStatus,autoNegotiate]", - ({"Management1": {"interfaceStatus": "down"}}, False) -] - -@pytest.mark.parametrize("data, check_type_str, evaluate_args, path, expected_result", [paramere_match_issue_45_case1]) -def test_issue_45_case1(data, check_type_str, evaluate_args, path, expected_result): - """Validate regex check type.""" - check = CheckType.init(check_type_str) - # There is not concept of "pre" and "post" in parameter_match. - value = check.get_value(data, path) - actual_results = check.evaluate(value, **evaluate_args) - assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( - output=actual_results, expected_output=expected_result - ) - - -paramere_match_issue_45_case2 = [ - { - "jsonrpc": "2.0", - "id": "EapiExplorer-1", - "result": [ - { - "interfaces": { - "Management1": { - "lastStatusChangeTimestamp": 1626247821.123456, - "lanes": 0, - "name": "Management1", - "interfaceStatus": "down", - "autoNegotiate": "success", - "burnedInAddress": "08:00:27:e6:b2:f8", - "loopbackMode": "loopbackNone", - "interfaceStatistics": { - "inBitsRate": 3403.4362520883615, - "inPktsRate": 3.7424095978179257, - "outBitsRate": 16249.69114419833, - "updateInterval": 300, - "outPktsRate": 2.1111866059750692, - }, - } - } - } - ], - }, - "parameter_match", - {"mode": "no-match", "params": {"interfaceStatus": "connected", "autoNegotiate": "success"}}, - "result[*].interfaces.*.[$name$,interfaceStatus,autoNegotiate]", - ({'Management1': {'autoNegotiate': 'success'}}, False) -] -@pytest.mark.parametrize("data, check_type_str, evaluate_args, path, expected_result", [paramere_match_issue_45_case2]) -def test_issue_45_case2(data, check_type_str, evaluate_args, path, expected_result): - """Validate regex check type.""" - check = CheckType.init(check_type_str) - # There is not concept of "pre" and "post" in parameter_match. - value = check.get_value(data, path) - actual_results = check.evaluate(value, **evaluate_args) - assert actual_results == expected_result, ASSERT_FAIL_MESSAGE.format( - output=actual_results, expected_output=expected_result - ) \ No newline at end of file From b79281f1b7349e3927ba9142d4063c4e6bc1a763 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 24 Mar 2022 10:04:19 +0100 Subject: [PATCH 52/80] complete Readme --- README.md | 111 +++++++++++++++++++++++++++++++++++++- netcompare/check_types.py | 2 +- 2 files changed, 110 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 875d0b3..3a6987c 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,6 @@ The test defines key/value pairs known to be the good value - type `dict()` - as Examples: ```python ->>> from netcompare import CheckType >>> post_data = { ... "jsonrpc": "2.0", ... "id": "EapiExplorer-1", @@ -428,7 +427,7 @@ Operator is a check which includes an array o different evaluation logic. Here a check if value is not in list (down, up) 3. in-range: Check if the value of a specified element is in the given numeric range. - - in-range: [ 20, 70 ] + - in-range: [20, 70] check if value is in range between 20 nad 70 4. not-range: Check if the value of a specified element is outside of a given numeric range. @@ -462,6 +461,114 @@ Operator is a check which includes an array o different evaluation logic. Here a checks if value is lower than 55 or not. +Examples: + +```python +>>> data = { +... "jsonrpc": "2.0", +... "id": "EapiExplorer-1", +... "result": [ +... { +... "vrfs": { +... "default": { +... "peerList": [ +... { +... "linkType": "external", +... "localAsn": "65130.1100", +... "peerAddress": "7.7.7.7", +... "lastEvent": "NoEvent", +... "bgpSoftReconfigInbound": "Default", +... "state": "Connected", +... "asn": "1.2354", +... "routerId": "0.0.0.0", +... "prefixesReceived": 101, +... "maintenance": False, +... "autoLocalAddress": "disabled", +... "lastState": "NoState", +... "establishFailHint": "Peer is not activated in any address-family mode", +... "maxTtlHops": None, +... "vrf": "default", +... "peerGroup": "EVPN-OVERLAY-SPINE", +... "idleReason": "Peer is not activated in any address-family mode", +... }, +... { +... "linkType": "external", +... "localAsn": "65130.1100", +... "peerAddress": "10.1.0.0", +... "lastEvent": "Stop", +... "bgpSoftReconfigInbound": "Default", +... "state": "Idle", +... "asn": "1.2354", +... "routerId": "0.0.0.0", +... "prefixesReceived": 50, +... "maintenance": False, +... "autoLocalAddress": "disabled", +... "lastState": "Active", +... "establishFailHint": "Could not find interface for peer", +... "vrf": "default", +... "peerGroup": "IPv4-UNDERLAY-SPINE", +... "idleReason": "Could not find interface for peer", +... "localRouterId": "1.1.0.1", +... } +... ] +... } +... } +... } +... ] +... } +>>> path = "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup,vrf,state]" +>>> # "operator" checks requires "mode" argument - which specify the operator logic to apply - +>>> # and "operator_data" required for the mode defined. +>>> check_args = {"params": {"mode": "all-same", "operator_data": True}} +>>> check = CheckType.init("operator") +>>> value = check.get_value(data, path) +>>> value +[{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE', 'vrf': 'default', 'state': 'Connected'}}, {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}] +>>> result = check.evaluate(value, check_args) +>>> # We are looking for peers that have the same peerGroup,vrf and state. If not, return those are not. +>>> result +((False, [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE', 'vrf': 'default', 'state': 'Connected'}}, {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}])) +``` + +Let's now look to an example for the `in` operator. Keeping the same `data` and class object as above: + +```python +>>> check_args = {"params": {"mode": "is-in", "operator_data": [20, 40, 50]}} +>>> path = "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesReceived]" +>>> value = check.get_value(data, path) +>>> value +[{'7.7.7.7': {'prefixesReceived': 101}}, {'10.1.0.0': {'prefixesReceived': 50}}] +>>> # We are looking for prefixesReceived value in operator_data list. +>>> result = check.evaluate(value, check_args) +>>> result +((True, [{'10.1.0.0': {'prefixesReceived': 50}}])) +``` + +What about `str` operator? + +```python +>>> path = "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]" +>>> check_args = {"params": {"mode": "contains", "operator_data": "EVPN"}} +>>> value = check.get_value(data, path) +>>> value +[{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE'}}, {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE'}}] +>>> result = check.evaluate(value, check_args) +>>> result +((True, [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE'}}])) +``` + +Can you guess what would ne the outcome for an `int`, `float` operator? + +```python +>>> path = "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesReceived]" +>>> check_args = {"params": {"mode": "is-gt", "operator_data": 20}} +>>> value = check.get_value(data, path) +>>> value +[{'7.7.7.7': {'prefixesReceived': 101}}, {'10.1.0.0': {'prefixesReceived': 50}}] +>>> result = check.evaluate(value, check_args) +>>> result +((True, [{'7.7.7.7': {'prefixesReceived': 101}}, {'10.1.0.0': {'prefixesReceived': 50}}])) +``` See [test](./tests) folder for more examples. diff --git a/netcompare/check_types.py b/netcompare/check_types.py index 7a91672..54dffa1 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -320,4 +320,4 @@ def evaluate(self, value_to_compare: Any, params: Any) -> Tuple[Mapping, bool]: # For naming consistency reference_data = params evaluation_result = operator_evaluator(reference_data["params"], value_to_compare) - return evaluation_result, not evaluation_result + return evaluation_result From d2052e2f8cfe6a6adcfba2d6409d469c04a8264e Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 24 Mar 2022 10:14:17 +0100 Subject: [PATCH 53/80] restor inner bool return for operator --- README.md | 8 ++++---- netcompare/check_types.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3a6987c..5530924 100644 --- a/README.md +++ b/README.md @@ -527,7 +527,7 @@ Examples: >>> result = check.evaluate(value, check_args) >>> # We are looking for peers that have the same peerGroup,vrf and state. If not, return those are not. >>> result -((False, [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE', 'vrf': 'default', 'state': 'Connected'}}, {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}])) +((False, [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE', 'vrf': 'default', 'state': 'Connected'}}, {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE', 'vrf': 'default', 'state': 'Idle'}}]), False) ``` Let's now look to an example for the `in` operator. Keeping the same `data` and class object as above: @@ -541,7 +541,7 @@ Let's now look to an example for the `in` operator. Keeping the same `data` and >>> # We are looking for prefixesReceived value in operator_data list. >>> result = check.evaluate(value, check_args) >>> result -((True, [{'10.1.0.0': {'prefixesReceived': 50}}])) +((True, [{'10.1.0.0': {'prefixesReceived': 50}}]), False) ``` What about `str` operator? @@ -554,7 +554,7 @@ What about `str` operator? [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE'}}, {'10.1.0.0': {'peerGroup': 'IPv4-UNDERLAY-SPINE'}}] >>> result = check.evaluate(value, check_args) >>> result -((True, [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE'}}])) +((True, [{'7.7.7.7': {'peerGroup': 'EVPN-OVERLAY-SPINE'}}]), False) ``` Can you guess what would ne the outcome for an `int`, `float` operator? @@ -567,7 +567,7 @@ Can you guess what would ne the outcome for an `int`, `float` operator? [{'7.7.7.7': {'prefixesReceived': 101}}, {'10.1.0.0': {'prefixesReceived': 50}}] >>> result = check.evaluate(value, check_args) >>> result -((True, [{'7.7.7.7': {'prefixesReceived': 101}}, {'10.1.0.0': {'prefixesReceived': 50}}])) +((True, [{'7.7.7.7': {'prefixesReceived': 101}}, {'10.1.0.0': {'prefixesReceived': 50}}]), False) ``` See [test](./tests) folder for more examples. diff --git a/netcompare/check_types.py b/netcompare/check_types.py index 54dffa1..7a91672 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -320,4 +320,4 @@ def evaluate(self, value_to_compare: Any, params: Any) -> Tuple[Mapping, bool]: # For naming consistency reference_data = params evaluation_result = operator_evaluator(reference_data["params"], value_to_compare) - return evaluation_result + return evaluation_result, not evaluation_result From 0e4f80759bcdb4e9ca19316a67846d578b129487 Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:22:03 +0100 Subject: [PATCH 54/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5530924..59e88a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # netcompare -This library is meant to be a light-weight way to compare structured output from network device `show` commands. `netcompare` is a python library targeted at intelligently deep diffing structured data objects of json type. In addition, `netcompare` provides some basic tests of keys and values within the data structure. +This library is meant to be a light-weight way to compare structured output from network devices `show` commands. `netcompare` is a python library targeted at intelligently deep diffing structured data objects of json type. In addition, `netcompare` can also provide some basic testing of key/values within a data structure. The libraly heavely rely on [jmspath](https://jmespath.org/) for traversing the json object and find the wanted value to be evaluated. More on that later. From 6d695056bfe8dda60d8ec3b4c447231199639f6a Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:23:17 +0100 Subject: [PATCH 55/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 59e88a6..62de959 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This library is meant to be a light-weight way to compare structured output from network devices `show` commands. `netcompare` is a python library targeted at intelligently deep diffing structured data objects of json type. In addition, `netcompare` can also provide some basic testing of key/values within a data structure. -The libraly heavely rely on [jmspath](https://jmespath.org/) for traversing the json object and find the wanted value to be evaluated. More on that later. +The library heavily relies on [jmespath](https://jmespath.org/) for traversing the json object and finding the value(s) to be evaluated. More on that later. ## Use Case From 53843a28c560d165302c9d56df8be15a6c77a765 Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:23:35 +0100 Subject: [PATCH 56/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 62de959..19d2300 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The library heavily relies on [jmespath](https://jmespath.org/) for traversing t ![netcompare HLD](./docs/images/hld.png) -As first thing, an instance of `CheckType` object must be created passing one of the below check types as argument: +An instance of `CheckType` object must be created first before passing one of the below check types as an argument: - `exact_match` - `tolerance` From bbabb62a9750825fab97d1ffbdf4f1d9911c5e39 Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:24:02 +0100 Subject: [PATCH 57/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 19d2300..4261825 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ my_check = "exact_match" check = CheckType.init(my_check) ``` -We would then need to define our json object used as reference data, as well as a JMSPATH expression to extract the value wanted and pass them to `get_value` method. Be aware! `netcompare` works with a customized version of JMSPATH. More on that later. +Next, define a json object as reference data, as well as a JMESPATH expression to extract the value wanted and pass them to `get_value` method. Be aware! `netcompare` works with a customized version of JMESPATH. More on that later. ```python bgp_pre_change = "./pre/bgp.json" From 24cb603bab3684c9998e9d1b4d0942c727090ddc Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:24:18 +0100 Subject: [PATCH 58/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4261825..a63e3db 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ bgp_jmspath_exp = "result[0].vrfs.default.peerList[*].[$peerAddress$,establishe pre_value = check.get_value(bgp_pre_change, bgp_jmspath_exp) ``` -Once extracted our pre-change value, we would need to evaluate it against our post-change value. In case of chec-type `exact_match` our post-value would be another json object: +Once the pre-change values are extracted, we would need to evaluate it against our post-change value. In case of check-type `exact_match` our post-value would be another json object: ```python bgp_post_change = "./post/bgp.json" From 88972dec9eb2c4c5538475d9f2bb1d7746b5e2a9 Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:25:00 +0100 Subject: [PATCH 59/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a63e3db..4463cf2 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ bgp_post_change = "./post/bgp.json" post_value = check.get_value(bgp_post_change, bgp_jmspath_exp) ``` -Every check type expect different type of arguments. For example, in case of check type `tolerance` we would also need to pass `tolerance` argument; `parameters` expect only a dictionary... +Every check type expects different types of arguments. For example; check type, `tolerance` needs a `tolerance` argument; `parameters` expect only a dictionary. Now that we have pre and post data, we just need to compare them with `evaluate` method which will return our evaluation result. From 5e701770eb92ef2d59248557fb2be6f6e1bf1d27 Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:25:12 +0100 Subject: [PATCH 60/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4463cf2..345b394 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ post_value = check.get_value(bgp_post_change, bgp_jmspath_exp) Every check type expects different types of arguments. For example; check type, `tolerance` needs a `tolerance` argument; `parameters` expect only a dictionary. -Now that we have pre and post data, we just need to compare them with `evaluate` method which will return our evaluation result. +Now that we have pre and post data, we just need to compare them with the `evaluate` method which will return our evaluation result. ```python results = check.evaluate(post_value, pre_value, **evaluate_args) From 1b3fab233573a538fccc6bf6a093bd32326239be Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:25:21 +0100 Subject: [PATCH 61/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 345b394..f80213e 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Now that we have pre and post data, we just need to compare them with the `evalu results = check.evaluate(post_value, pre_value, **evaluate_args) ``` -## Customized JMSPATH +## Customized JMESPATH Since `netcompare` work with json object as data inputs, JMSPATH was the obvous choise for traversing the data and extract the value wanted from it. From ffef1e442c55b817e243baaf42a0d0c13372e6c6 Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:25:31 +0100 Subject: [PATCH 62/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f80213e..5f65f17 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ results = check.evaluate(post_value, pre_value, **evaluate_args) ## Customized JMESPATH -Since `netcompare` work with json object as data inputs, JMSPATH was the obvous choise for traversing the data and extract the value wanted from it. +Since `netcompare` works with json objects as data inputs, JMESPATH was the obvious choice for traversing the data and extracting the value(s) to compare. However, JMSPATH comes with a limitation where is not possible to define a `key` to which the `value` belongs to. From 68e5b9f64179c72ce3ea63bfdb4ab82dc931e87d Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:26:12 +0100 Subject: [PATCH 63/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f65f17..a93082e 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ results = check.evaluate(post_value, pre_value, **evaluate_args) Since `netcompare` works with json objects as data inputs, JMESPATH was the obvious choice for traversing the data and extracting the value(s) to compare. -However, JMSPATH comes with a limitation where is not possible to define a `key` to which the `value` belongs to. +However, JMESPATH comes with a limitation where is not possible to define a `key` to which the `value` belongs to. Let's have a look to the below `show bgp` output example. From 90d12752c706138c9d6e6003ce8b4f98b1790e07 Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:26:29 +0100 Subject: [PATCH 64/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a93082e..72d5bcb 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Since `netcompare` works with json objects as data inputs, JMESPATH was the obvi However, JMESPATH comes with a limitation where is not possible to define a `key` to which the `value` belongs to. -Let's have a look to the below `show bgp` output example. +Below is the output of `show bgp`. ```json { From 9bc93fe65e647a13e65f1e06ca59b43bb54edde4 Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:27:04 +0100 Subject: [PATCH 65/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 72d5bcb..5d253cf 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ If we would define a JMSPATH expression to extract `state` we would have somethi ``` How can we understand that `Idle` is relative to peer 7.7.7.7 and `Connected` to peer `10.1.0.0` ? -We could index the output but that would require some post-processing data. For that reason, `netcompare` use a customized version of JMSPATH where is possible to define a reference key for the value(s) wanted. The reference key must be within `$` sign anchors and defined in a list, together with the value(s): +We could index the output but that would require some post-processing of the data. For that reason, `netcompare` use a customized version of JMESPATH where it is possible to define a reference key for the value(s) wanted. The reference key must be within `$` sign anchors and defined in a list, together with the value(s): ```python "result[0].vrfs.default.peerList[*].[$peerAddress$,state] From c66d28b602f51f43865f3034f93c6411990c1262 Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:27:15 +0100 Subject: [PATCH 66/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d253cf..953f7ce 100644 --- a/README.md +++ b/README.md @@ -412,7 +412,7 @@ Let's run an example where e want to check that `burnedInAddress` key has a stri ### Operator -Operator is a check which includes an array o different evaluation logic. Here a summary of the available options: +Operator is a check which includes an array of different evaluation logic. Here a summary of the available options: #### `in` operators From 1c458e93da256511a6a200a5ab6aba92f9fb61ee Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:28:26 +0100 Subject: [PATCH 67/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 953f7ce..56b1633 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ Below is the output of `show bgp`. ] } ``` -If we would define a JMSPATH expression to extract `state` we would have something like... +A JMESPATH expression to extract `state` is shown below. ```python "result[0].vrfs.default.peerList[*].state From 0a51089491afb05a0b9b341e829577c81d52b384 Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:28:31 +0100 Subject: [PATCH 68/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 56b1633..f53b4c6 100644 --- a/README.md +++ b/README.md @@ -360,7 +360,7 @@ This test can test the tolerance for changing quantities of certain things such ### Regex -The `regex` check type evaluates data against a python regular expression defined as check-tyoe argument. As per `parameter_match` the option `match`, `no-match` is also supported. +The `regex` check type evaluates data against a python regular expression defined as check-type argument. As per `parameter_match` the option `match`, `no-match` is also supported. Let's run an example where e want to check that `burnedInAddress` key has a string rappresentig a MAC Address as value From a0bc045275148b5a7156fe1767a72900aaccd31d Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:28:37 +0100 Subject: [PATCH 69/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f53b4c6..81fc6d9 100644 --- a/README.md +++ b/README.md @@ -362,7 +362,7 @@ This test can test the tolerance for changing quantities of certain things such The `regex` check type evaluates data against a python regular expression defined as check-type argument. As per `parameter_match` the option `match`, `no-match` is also supported. -Let's run an example where e want to check that `burnedInAddress` key has a string rappresentig a MAC Address as value +Let's run an example where we want to check the `burnedInAddress` key has a string representing a MAC Address as value ```python >>> data = { From ec7a32b2ab910dbb4f2d8f42cef6b8bce11a3962 Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:28:59 +0100 Subject: [PATCH 70/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 81fc6d9..2d6ab3c 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ That would give us... ``` -## Check types explained +## Check Types Explained ### exact_match From 59b3be78c1add6fcf36688c8579a5d3570760a8b Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:29:08 +0100 Subject: [PATCH 71/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d6ab3c..2cab794 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ Let's see a better way to run `exact_match` for this specific case. Since we are ``` Targeting only `interfaceStatus` key, we would need to define a reference key (in this case `$name$`) as well as we would not need to define any exclusion list. -The anchor logic for reference key applies to all check-types available in `netcompare` +The anchor logic for the reference key applies to all check-types available in `netcompare` ### parameter_match From 7c90ab542dad8bb9c408b43604fbc7243174745f Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:29:14 +0100 Subject: [PATCH 72/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2cab794..c62f80f 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ Let's see a better way to run `exact_match` for this specific case. Since we are >>> result ({"Management1": {"interfaceStatus": {"new_value": "connected", "old_value": "down"}}}, False) ``` -Targeting only `interfaceStatus` key, we would need to define a reference key (in this case `$name$`) as well as we would not need to define any exclusion list. +Targeting only the `interfaceStatus` key, we would need to define a reference key (in this case `$name$`), we would not define any exclusion list. The anchor logic for the reference key applies to all check-types available in `netcompare` From 332b47e234b349a692f79d5513f6e7caa1de104d Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:29:21 +0100 Subject: [PATCH 73/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c62f80f..22aadc4 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ That would give us... ### exact_match -Check type `exact_match` is concerned about the value of the elements within the data structure. The keys and values should match between the pre and post values. A diff is generated between the two data sets. +Check type `exact_match` is concerned about the value of the elements within the data structure. The key/values should match between the pre and post values. A diff is generated between the two data sets. As some outputs might be too verbose or includes fields that constantly change (i.e. interface counter), is possible to exclude a portion of data traversed by JMSPATH, defining a list of keys that we want to exclude. Examples: From 1ea17df64ea65d0fa281060e346dd98e1d2373cb Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:29:47 +0100 Subject: [PATCH 74/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22aadc4..f6b345a 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ That would give us... ### exact_match Check type `exact_match` is concerned about the value of the elements within the data structure. The key/values should match between the pre and post values. A diff is generated between the two data sets. -As some outputs might be too verbose or includes fields that constantly change (i.e. interface counter), is possible to exclude a portion of data traversed by JMSPATH, defining a list of keys that we want to exclude. +As some outputs might be too verbose or includes fields that constantly change (i.e. interface counter), it is possible to exclude a portion of data traversed by JMESPATH, defining a list of keys that we want to exclude. Examples: From 9c8320c200e29c02346f5537022921cee353cfcd Mon Sep 17 00:00:00 2001 From: Federico87 <15066806+lvrfrc87@users.noreply.github.com> Date: Thu, 24 Mar 2022 15:30:48 +0100 Subject: [PATCH 75/80] Update README.md Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f6b345a..150c9e1 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ Examples: As we can see, we return a tuple containing a diff betwee the pre and post data as well as a boolean for the overall test result. In this case a diff has been found so the status of the test is `False`. -Let's see a better way to run `exact_match` for this specific case. Since we are interested only into `interfaceStatus` we could write our JMSPATH expression as: +Let's see a better way to run `exact_match` for this specific case. Since we are interested only in. `interfaceStatus` we could write our JMESPATH expression as: ```python >>> my_jmspath = "result[*].interfaces.*.[$name$,interfaceStatus]" From 985691504c707467ff23ebbdb45376177ddfe4a7 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Fri, 25 Mar 2022 14:17:36 +0100 Subject: [PATCH 76/80] improve reference key validation --- netcompare/check_types.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index 7a91672..ffa515b 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -84,15 +84,24 @@ def get_value(output: Union[Mapping, List], path: str, exclude: List = None) -> paired_key_value = associate_key_of_my_value(jmespath_value_parser(path), values) - if re.search(r"\$.*\$", path): # normalize - + # We need to get a list of reference keys - list of strings. + # Based on the expression or output type we might have differen data types + # therefore we need to normalize. + if re.search(r"\$.*\$", path): wanted_reference_keys = jmespath.search(jmespath_refkey_parser(path), output) - try: - if isinstance(wanted_reference_keys[0], list): - wanted_reference_keys = flatten_list(wanted_reference_keys)[0] - except KeyError: - pass - list_of_reference_keys = keys_cleaner(wanted_reference_keys) + + # if dict() + if isinstance(wanted_reference_keys, dict): + list_of_reference_keys = keys_cleaner(wanted_reference_keys) + # if list(list()) + elif any(isinstance(element, list) for element in wanted_reference_keys): + list_of_reference_keys = flatten_list(wanted_reference_keys)[0] + # if list() + elif isinstance(wanted_reference_keys, list): + list_of_reference_keys = wanted_reference_keys + else: + raise ValueError("Reference Key normalization failure. Please verify datat type returned.") + return keys_values_zipper(list_of_reference_keys, paired_key_value) return values From 3df96002399f8b380f5041c40e75c77dd1aa07ec Mon Sep 17 00:00:00 2001 From: Network to Code Date: Wed, 30 Mar 2022 11:40:06 +0200 Subject: [PATCH 77/80] pointing CI to PyPi dev --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 663bee3..6d10547 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -233,5 +233,7 @@ jobs: with: user: "__token__" password: "${{ secrets.PYPI_API_TOKEN }}" + # Using PyPi dev. + repository_url: "https://test.pypi.org/legacy/" needs: - "pytest" From f78849a5898808b411d5fd71aa517ba0a6750665 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Wed, 20 Apr 2022 10:12:57 +0200 Subject: [PATCH 78/80] remove inner boolean from operator --- netcompare/check_types.py | 4 ++-- tests/test_operators.py | 29 +++-------------------------- 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index e212715..af2ab68 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -324,7 +324,7 @@ def validate(**kwargs) -> None: def evaluate(self, value_to_compare: Any, params: Any) -> Tuple[Mapping, bool]: """Operator evaluator implementation.""" self.validate(**params) - # For naming consistency + # For name consistency. reference_data = params evaluation_result = operator_evaluator(reference_data["params"], value_to_compare) - return evaluation_result, not evaluation_result + return evaluation_result[1], not evaluation_result[0] \ No newline at end of file diff --git a/tests/test_operators.py b/tests/test_operators.py index 9d3b5ab..64ba6e3 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -9,8 +9,6 @@ {"params": {"mode": "all-same", "operator_data": True}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup,vrf,state]", ( - ( - False, [ { "7.7.7.7": { @@ -41,8 +39,7 @@ } }, ], - ), - False, + True, ), ) operator_contains = ( @@ -50,7 +47,7 @@ "operator", {"params": {"mode": "contains", "operator_data": "EVPN"}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", - ((True, [{"7.7.7.7": {"peerGroup": "EVPN-OVERLAY-SPINE"}}]), False), + ([{"7.7.7.7": {"peerGroup": "EVPN-OVERLAY-SPINE"}}], False), ) operator_not_contains = ( "pre.json", @@ -58,14 +55,12 @@ {"params": {"mode": "not-contains", "operator_data": "EVPN"}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", ( - ( - True, + [ {"10.1.0.0": {"peerGroup": "IPv4-UNDERLAY-SPINE"}}, {"10.2.0.0": {"peerGroup": "IPv4-UNDERLAY-SPINE"}}, {"10.64.207.255": {"peerGroup": "IPv4-UNDERLAY-MLAG-PEER"}}, ], - ), False, ), ) @@ -75,15 +70,12 @@ {"params": {"mode": "is-gt", "operator_data": 20}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( - ( - True, [ {"7.7.7.7": {"prefixesSent": 50}}, {"10.1.0.0": {"prefixesSent": 50}}, {"10.2.0.0": {"prefixesSent": 50}}, {"10.64.207.255": {"prefixesSent": 50}}, ], - ), False, ), ) @@ -93,15 +85,12 @@ {"params": {"mode": "is-lt", "operator_data": 60}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( - ( - True, [ {"7.7.7.7": {"prefixesSent": 50}}, {"10.1.0.0": {"prefixesSent": 50}}, {"10.2.0.0": {"prefixesSent": 50}}, {"10.64.207.255": {"prefixesSent": 50}}, ], - ), False, ), ) @@ -111,15 +100,12 @@ {"params": {"mode": "is-in", "operator_data": [20, 40, 50]}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( - ( - True, [ {"7.7.7.7": {"prefixesSent": 50}}, {"10.1.0.0": {"prefixesSent": 50}}, {"10.2.0.0": {"prefixesSent": 50}}, {"10.64.207.255": {"prefixesSent": 50}}, ], - ), False, ), ) @@ -129,15 +115,12 @@ {"params": {"mode": "not-in", "operator_data": [20, 40, 60]}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( - ( - True, [ {"7.7.7.7": {"prefixesSent": 50}}, {"10.1.0.0": {"prefixesSent": 50}}, {"10.2.0.0": {"prefixesSent": 50}}, {"10.64.207.255": {"prefixesSent": 50}}, ], - ), False, ), ) @@ -147,15 +130,12 @@ {"params": {"mode": "in-range", "operator_data": (20, 60)}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( - ( - True, [ {"7.7.7.7": {"prefixesSent": 50}}, {"10.1.0.0": {"prefixesSent": 50}}, {"10.2.0.0": {"prefixesSent": 50}}, {"10.64.207.255": {"prefixesSent": 50}}, ], - ), False, ), ) @@ -165,15 +145,12 @@ {"params": {"mode": "not-range", "operator_data": (20, 40)}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( - ( - True, [ {"7.7.7.7": {"prefixesSent": 50}}, {"10.1.0.0": {"prefixesSent": 50}}, {"10.2.0.0": {"prefixesSent": 50}}, {"10.64.207.255": {"prefixesSent": 50}}, ], - ), False, ), ) From f454d5bdf6b64b697eecf2dd584265156ec2d3a3 Mon Sep 17 00:00:00 2001 From: Network to Code Date: Wed, 20 Apr 2022 10:15:17 +0200 Subject: [PATCH 79/80] fix tests --- netcompare/check_types.py | 2 +- tests/test_operators.py | 143 +++++++++++++++++++------------------- 2 files changed, 72 insertions(+), 73 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index af2ab68..6221623 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -327,4 +327,4 @@ def evaluate(self, value_to_compare: Any, params: Any) -> Tuple[Mapping, bool]: # For name consistency. reference_data = params evaluation_result = operator_evaluator(reference_data["params"], value_to_compare) - return evaluation_result[1], not evaluation_result[0] \ No newline at end of file + return evaluation_result[1], not evaluation_result[0] diff --git a/tests/test_operators.py b/tests/test_operators.py index 64ba6e3..bbb486d 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -9,36 +9,36 @@ {"params": {"mode": "all-same", "operator_data": True}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup,vrf,state]", ( - [ - { - "7.7.7.7": { - "peerGroup": "EVPN-OVERLAY-SPINE", - "state": "Idle", - "vrf": "default", - } - }, - { - "10.1.0.0": { - "peerGroup": "IPv4-UNDERLAY-SPINE", - "state": "Idle", - "vrf": "default", - } - }, - { - "10.2.0.0": { - "peerGroup": "IPv4-UNDERLAY-SPINE", - "state": "Idle", - "vrf": "default", - } - }, - { - "10.64.207.255": { - "peerGroup": "IPv4-UNDERLAY-MLAG-PEER", - "state": "Idle", - "vrf": "default", - } - }, - ], + [ + { + "7.7.7.7": { + "peerGroup": "EVPN-OVERLAY-SPINE", + "state": "Idle", + "vrf": "default", + } + }, + { + "10.1.0.0": { + "peerGroup": "IPv4-UNDERLAY-SPINE", + "state": "Idle", + "vrf": "default", + } + }, + { + "10.2.0.0": { + "peerGroup": "IPv4-UNDERLAY-SPINE", + "state": "Idle", + "vrf": "default", + } + }, + { + "10.64.207.255": { + "peerGroup": "IPv4-UNDERLAY-MLAG-PEER", + "state": "Idle", + "vrf": "default", + } + }, + ], True, ), ) @@ -55,12 +55,11 @@ {"params": {"mode": "not-contains", "operator_data": "EVPN"}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", ( - - [ - {"10.1.0.0": {"peerGroup": "IPv4-UNDERLAY-SPINE"}}, - {"10.2.0.0": {"peerGroup": "IPv4-UNDERLAY-SPINE"}}, - {"10.64.207.255": {"peerGroup": "IPv4-UNDERLAY-MLAG-PEER"}}, - ], + [ + {"10.1.0.0": {"peerGroup": "IPv4-UNDERLAY-SPINE"}}, + {"10.2.0.0": {"peerGroup": "IPv4-UNDERLAY-SPINE"}}, + {"10.64.207.255": {"peerGroup": "IPv4-UNDERLAY-MLAG-PEER"}}, + ], False, ), ) @@ -70,12 +69,12 @@ {"params": {"mode": "is-gt", "operator_data": 20}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( - [ - {"7.7.7.7": {"prefixesSent": 50}}, - {"10.1.0.0": {"prefixesSent": 50}}, - {"10.2.0.0": {"prefixesSent": 50}}, - {"10.64.207.255": {"prefixesSent": 50}}, - ], + [ + {"7.7.7.7": {"prefixesSent": 50}}, + {"10.1.0.0": {"prefixesSent": 50}}, + {"10.2.0.0": {"prefixesSent": 50}}, + {"10.64.207.255": {"prefixesSent": 50}}, + ], False, ), ) @@ -85,12 +84,12 @@ {"params": {"mode": "is-lt", "operator_data": 60}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( - [ - {"7.7.7.7": {"prefixesSent": 50}}, - {"10.1.0.0": {"prefixesSent": 50}}, - {"10.2.0.0": {"prefixesSent": 50}}, - {"10.64.207.255": {"prefixesSent": 50}}, - ], + [ + {"7.7.7.7": {"prefixesSent": 50}}, + {"10.1.0.0": {"prefixesSent": 50}}, + {"10.2.0.0": {"prefixesSent": 50}}, + {"10.64.207.255": {"prefixesSent": 50}}, + ], False, ), ) @@ -100,12 +99,12 @@ {"params": {"mode": "is-in", "operator_data": [20, 40, 50]}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( - [ - {"7.7.7.7": {"prefixesSent": 50}}, - {"10.1.0.0": {"prefixesSent": 50}}, - {"10.2.0.0": {"prefixesSent": 50}}, - {"10.64.207.255": {"prefixesSent": 50}}, - ], + [ + {"7.7.7.7": {"prefixesSent": 50}}, + {"10.1.0.0": {"prefixesSent": 50}}, + {"10.2.0.0": {"prefixesSent": 50}}, + {"10.64.207.255": {"prefixesSent": 50}}, + ], False, ), ) @@ -115,12 +114,12 @@ {"params": {"mode": "not-in", "operator_data": [20, 40, 60]}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( - [ - {"7.7.7.7": {"prefixesSent": 50}}, - {"10.1.0.0": {"prefixesSent": 50}}, - {"10.2.0.0": {"prefixesSent": 50}}, - {"10.64.207.255": {"prefixesSent": 50}}, - ], + [ + {"7.7.7.7": {"prefixesSent": 50}}, + {"10.1.0.0": {"prefixesSent": 50}}, + {"10.2.0.0": {"prefixesSent": 50}}, + {"10.64.207.255": {"prefixesSent": 50}}, + ], False, ), ) @@ -130,12 +129,12 @@ {"params": {"mode": "in-range", "operator_data": (20, 60)}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( - [ - {"7.7.7.7": {"prefixesSent": 50}}, - {"10.1.0.0": {"prefixesSent": 50}}, - {"10.2.0.0": {"prefixesSent": 50}}, - {"10.64.207.255": {"prefixesSent": 50}}, - ], + [ + {"7.7.7.7": {"prefixesSent": 50}}, + {"10.1.0.0": {"prefixesSent": 50}}, + {"10.2.0.0": {"prefixesSent": 50}}, + {"10.64.207.255": {"prefixesSent": 50}}, + ], False, ), ) @@ -145,12 +144,12 @@ {"params": {"mode": "not-range", "operator_data": (20, 40)}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,prefixesSent]", ( - [ - {"7.7.7.7": {"prefixesSent": 50}}, - {"10.1.0.0": {"prefixesSent": 50}}, - {"10.2.0.0": {"prefixesSent": 50}}, - {"10.64.207.255": {"prefixesSent": 50}}, - ], + [ + {"7.7.7.7": {"prefixesSent": 50}}, + {"10.1.0.0": {"prefixesSent": 50}}, + {"10.2.0.0": {"prefixesSent": 50}}, + {"10.64.207.255": {"prefixesSent": 50}}, + ], False, ), ) From 19334ccbe0a789f91430f364e86bfc0d251f768f Mon Sep 17 00:00:00 2001 From: Network to Code Date: Thu, 21 Apr 2022 10:11:06 +0200 Subject: [PATCH 80/80] implementation fixes as per comments --- netcompare/check_types.py | 3 +-- netcompare/operator.py | 12 ++++++------ tests/test_operators.py | 18 +++++++++--------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/netcompare/check_types.py b/netcompare/check_types.py index 6221623..430dacb 100644 --- a/netcompare/check_types.py +++ b/netcompare/check_types.py @@ -326,5 +326,4 @@ def evaluate(self, value_to_compare: Any, params: Any) -> Tuple[Mapping, bool]: self.validate(**params) # For name consistency. reference_data = params - evaluation_result = operator_evaluator(reference_data["params"], value_to_compare) - return evaluation_result[1], not evaluation_result[0] + return operator_evaluator(reference_data["params"], value_to_compare) diff --git a/netcompare/operator.py b/netcompare/operator.py index ba4effd..e57f4c5 100644 --- a/netcompare/operator.py +++ b/netcompare/operator.py @@ -50,8 +50,8 @@ def _loop_through_wrapper(self, call_ops: str) -> Tuple[bool, List]: elif ops[call_ops](evaluated_value, self.referance_data): result.append(item) if result: - return (True, result) - return (False, result) + return (result, True) + return (result, False) def all_same(self) -> Tuple[bool, Any]: """All same operator implementation.""" @@ -70,12 +70,12 @@ def all_same(self) -> Tuple[bool, Any]: result.append(True) if self.referance_data and not all(result): - return (False, self.value_to_compare) + return (self.value_to_compare, False) if self.referance_data: - return (True, self.value_to_compare) + return (self.value_to_compare, True) if not all(result): - return (True, self.value_to_compare) - return (False, self.value_to_compare) + return (self.value_to_compare, True) + return (self.value_to_compare, False) def contains(self) -> Tuple[bool, List]: """Contains operator implementation.""" diff --git a/tests/test_operators.py b/tests/test_operators.py index bbb486d..0d67431 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -39,7 +39,7 @@ } }, ], - True, + False, ), ) operator_contains = ( @@ -47,7 +47,7 @@ "operator", {"params": {"mode": "contains", "operator_data": "EVPN"}}, "result[0].vrfs.default.peerList[*].[$peerAddress$,peerGroup]", - ([{"7.7.7.7": {"peerGroup": "EVPN-OVERLAY-SPINE"}}], False), + ([{"7.7.7.7": {"peerGroup": "EVPN-OVERLAY-SPINE"}}], True), ) operator_not_contains = ( "pre.json", @@ -60,7 +60,7 @@ {"10.2.0.0": {"peerGroup": "IPv4-UNDERLAY-SPINE"}}, {"10.64.207.255": {"peerGroup": "IPv4-UNDERLAY-MLAG-PEER"}}, ], - False, + True, ), ) operator_is_gt = ( @@ -75,7 +75,7 @@ {"10.2.0.0": {"prefixesSent": 50}}, {"10.64.207.255": {"prefixesSent": 50}}, ], - False, + True, ), ) operator_is_lt = ( @@ -90,7 +90,7 @@ {"10.2.0.0": {"prefixesSent": 50}}, {"10.64.207.255": {"prefixesSent": 50}}, ], - False, + True, ), ) operator_is_in = ( @@ -105,7 +105,7 @@ {"10.2.0.0": {"prefixesSent": 50}}, {"10.64.207.255": {"prefixesSent": 50}}, ], - False, + True, ), ) operator_not_in = ( @@ -120,7 +120,7 @@ {"10.2.0.0": {"prefixesSent": 50}}, {"10.64.207.255": {"prefixesSent": 50}}, ], - False, + True, ), ) operator_in_range = ( @@ -135,7 +135,7 @@ {"10.2.0.0": {"prefixesSent": 50}}, {"10.64.207.255": {"prefixesSent": 50}}, ], - False, + True, ), ) operator_not_in_range = ( @@ -150,7 +150,7 @@ {"10.2.0.0": {"prefixesSent": 50}}, {"10.64.207.255": {"prefixesSent": 50}}, ], - False, + True, ), )