diff --git a/scripts/route_check.py b/scripts/route_check.py index 3bb47a2643..3970964ab0 100755 --- a/scripts/route_check.py +++ b/scripts/route_check.py @@ -1,16 +1,59 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +""" +What it is: + The routes flow from APPL-DB to ASIC-DB, via orchagent. + This tool's job is to verify that all routes added to APPL-DB do + get into ASIC-DB. + + +How: + NOTE: The flow from APPL-DB to ASIC-DB takes non zero milliseconds. + 1) Initiate subscribe for ASIC-DB updates. + 2) Read APPL-DB & ASIC-DB + 3) Get the diff. + 4) If any diff, + 4.1) Collect subscribe messages for a second + 4.2) check diff against the subscribe messages + 5) Rule out local interfaces & default routes + 6) If still outstanding diffs, report failure. + +To verify: + Run this tool in SONiC switch and watch the result. In case of failure + checkout the result to validate the failure. + To simulate failure: + Stop Orchagent. + Run this tool, and likely you would see some failures. + You could potentially remove / add routes in APPL / ASIC DBs with orchagent + down to ensure failure. + Analyze the reported failures to match expected. + You may use the exit code to verify the result as success or not. + + + +""" + +import argparse +from enum import Enum +import ipaddress +import json import os import re import sys -import argparse -import ipaddress import syslog -import json import time -from enum import Enum -from swsssdk import ConfigDBConnector + +from swsscommon import swsscommon + +APPL_DB_NAME = 'APPL_DB' +ASIC_DB_NAME = 'ASIC_DB' +ASIC_TABLE_NAME = 'ASIC_STATE' +ASIC_KEY_PREFIX = 'SAI_OBJECT_TYPE_ROUTE_ENTRY:' + +SUBSCRIBE_WAIT_SECS = 1 + +UNIT_TESTING = 0 os.environ['PYTHONUNBUFFERED']='True' @@ -33,6 +76,12 @@ def __str__(self): write_to_syslog = False def set_level(lvl, log_to_syslog): + """ + Sets the log level + :param lvl: Log level as ERR/INFO/DEBUG; default: syslog.LOG_ERR + :param log_to_syslog; True - write into syslog. False: skip + :return None + """ global report_level global write_to_syslog @@ -45,6 +94,12 @@ def set_level(lvl, log_to_syslog): def print_message(lvl, *args): + """ + print and log the message for given level. + :param lvl: Log level for this message as ERR/INFO/DEBUG + :param args: message as list of strings or convertible to string + :return None + """ if (lvl <= report_level): msg = "" for arg in args: @@ -55,6 +110,11 @@ def print_message(lvl, *args): def add_prefix(ip): + """ + helper add static prefix based on IP type + :param ip: IP to add prefix as string. + :return ip + "/32 or /128" + """ if ip.find(IPV6_SEPARATOR) == -1: ip = ip + PREFIX_SEPARATOR + "32" else: @@ -63,20 +123,41 @@ def add_prefix(ip): def add_prefix_ifnot(ip): + """ + helper add static prefix if absent + :param ip: IP to add prefix as string. + :return ip with prefix + """ return ip if ip.find(PREFIX_SEPARATOR) != -1 else add_prefix(ip) def is_local(ip): + """ + helper to check if this IP qualify as link local + :param ip: IP to check as string + :return True if link local, else False + """ t = ipaddress.ip_address(ip.split("/")[0]) return t.is_link_local def is_default_route(ip): + """ + helper to check if this IP is default route + :param ip: IP to check as string + :return True if default, else False + """ t = ipaddress.ip_address(ip.split("/")[0]) return t.is_unspecified and ip.split("/")[1] == "0" def cmps(s1, s2): + """ + helper to compare two strings + :param s1: left string + :param s2: right string + :return comparison result as -1/0/1 + """ if (s1 == s2): return 0 if (s1 < s2): @@ -84,7 +165,13 @@ def cmps(s1, s2): return 1 -def do_diff(t1, t2): +def diff_sorted_lists(t1, t2): + """ + helper to compare two sorted lists. + :param t1: list 1 + :param t2: list 2 + :return (, ) + """ t1_x = t2_x = 0 t1_miss = [] t2_miss = [] @@ -112,11 +199,59 @@ def do_diff(t1, t2): return t1_miss, t2_miss +def checkout_rt_entry(k): + """ + helper to filter out correct keys and strip out IP alone. + :param ip: key to check as string + :return (True, ip) or (False, None) + """ + if k.startswith(ASIC_KEY_PREFIX): + e = k.lower().split("\"", -1)[3] + if not is_local(e): + return True, e + return False, None + + +def get_subscribe_updates(selector, subs): + """ + helper to collect subscribe messages for a period + :param selector: Selector object to wait + :param subs: Subscription object to pop messages + :return (add, del) messages as sorted + """ + adds = [] + deletes = [] + t_end = time.time() + SUBSCRIBE_WAIT_SECS + t_wait = SUBSCRIBE_WAIT_SECS + + while t_wait > 0: + selector.select(t_wait) + t_wait = int(t_end - time.time()) + while True: + key, op, val = subs.pop() + if not key: + break + res, e = checkout_rt_entry(key) + if res: + if op == "SET": + adds.append(e) + elif op == "DEL": + deletes.append(e) + + print_message(syslog.LOG_DEBUG, "adds={}".format(adds)) + print_message(syslog.LOG_DEBUG, "dels={}".format(deletes)) + return (sorted(adds), sorted(deletes)) + + def get_routes(): - db = ConfigDBConnector() - db.db_connect('APPL_DB') + """ + helper to read route table from APPL-DB. + :return list of sorted routes with prefix ensured + """ + db = swsscommon.DBConnector(APPL_DB_NAME, 0) print_message(syslog.LOG_DEBUG, "APPL DB connected for routes") - keys = db.get_keys('ROUTE_TABLE') + tbl = swsscommon.Table(db, 'ROUTE_TABLE') + keys = tbl.getKeys() valid_rt = [] for k in keys: @@ -128,28 +263,42 @@ def get_routes(): def get_route_entries(): - db = ConfigDBConnector() - db.db_connect('ASIC_DB') + """ + helper to read present route entries from ASIC-DB and + as well initiate selector for ASIC-DB:ASIC-state updates. + :return (selector, subscriber, ) + """ + db = swsscommon.DBConnector(ASIC_DB_NAME, 0) + subs = swsscommon.SubscriberStateTable(db, ASIC_TABLE_NAME) print_message(syslog.LOG_DEBUG, "ASIC DB connected") - keys = db.get_keys('ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY', False) rt = [] - for k in keys: - e = k.lower().split("\"", -1)[3] - if not is_local(e): + while True: + k, _, _ = subs.pop() + if not k: + break + res, e = checkout_rt_entry(k) + if res: rt.append(e) + print_message(syslog.LOG_DEBUG, json.dumps({"ASIC_ROUTE_ENTRY": sorted(rt)}, indent=4)) - return sorted(rt) + + selector = swsscommon.Select() + selector.addSelectable(subs) + return (selector, subs, sorted(rt)) def get_interfaces(): - db = ConfigDBConnector() - db.db_connect('APPL_DB') + """ + helper to read interface table from APPL-DB. + :return sorted list of IP addresses with added prefix + """ + db = swsscommon.DBConnector(APPL_DB_NAME, 0) print_message(syslog.LOG_DEBUG, "APPL DB connected for interfaces") + tbl = swsscommon.Table(db, 'INTF_TABLE') + keys = tbl.getKeys() intf = [] - keys = db.get_keys('INTF_TABLE') - for k in keys: lst = re.split(':', k.lower(), maxsplit=1) if len(lst) == 1: @@ -165,17 +314,22 @@ def get_interfaces(): def filter_out_local_interfaces(keys): + """ + helper to filter out local interfaces + :param keys: APPL-DB:ROUTE_TABLE Routes to check. + :return keys filtered out of local + """ rt = [] local_if_re = ['eth0', 'lo', 'docker0', 'Loopback\d+'] - db = ConfigDBConnector() - db.db_connect('APPL_DB') + db = swsscommon.DBConnector(APPL_DB_NAME, 0) + tbl = swsscommon.Table(db, 'ROUTE_TABLE') for k in keys: - e = db.get_entry('ROUTE_TABLE', k) + e = dict(tbl.get(k)[1]) if not e: # Prefix might have been added. So try w/o it. - e = db.get_entry('ROUTE_TABLE', k.split("/")[0]) + e = dict(tbl.get(k.split("/"))[1]) if not e or all([not re.match(x, e['ifname']) for x in local_if_re]): rt.append(k) @@ -183,6 +337,11 @@ def filter_out_local_interfaces(keys): def filter_out_default_routes(lst): + """ + helper to filter out default routes + :param lst: list to filter + :return filtered list. + """ upd = [] for rt in lst: @@ -193,52 +352,82 @@ def filter_out_default_routes(lst): def check_routes(): + """ + The heart of this script which runs the checks. + Read APPL-DB & ASIC-DB, the relevant tables for route checking. + Checkout routes in ASIC-DB to match APPL-DB, discounting local & + default routes. In case of missed / unexpected entries in ASIC, + it might be due to update latency between APPL & ASIC DBs. So collect + ASIC-DB subscribe updates for a second, and checkout if you see SET + command for missing ones & DEL command for unexpectes ones in ASIC. + + If there are still some unjustifiable diffs, between APPL & ASIC DB, + related to routes report failure, else all good. + + :return (0, None) on sucess, else (-1, results) where results holds + the unjustifiable entries. + """ intf_appl_miss = [] rt_appl_miss = [] rt_asic_miss = [] results = {} - err_present = False + + selector, subs, rt_asic = get_route_entries() rt_appl = get_routes() - rt_asic = get_route_entries() intf_appl = get_interfaces() # Diff APPL-DB routes & ASIC-DB routes - rt_appl_miss, rt_asic_miss = do_diff(rt_appl, rt_asic) + rt_appl_miss, rt_asic_miss = diff_sorted_lists(rt_appl, rt_asic) # Check missed ASIC routes against APPL-DB INTF_TABLE - _, rt_asic_miss = do_diff(intf_appl, rt_asic_miss) + _, rt_asic_miss = diff_sorted_lists(intf_appl, rt_asic_miss) rt_asic_miss = filter_out_default_routes(rt_asic_miss) # Check APPL-DB INTF_TABLE with ASIC table route entries - intf_appl_miss, _ = do_diff(intf_appl, rt_asic) + intf_appl_miss, _ = diff_sorted_lists(intf_appl, rt_asic) - if (len(rt_appl_miss) != 0): + if rt_appl_miss: rt_appl_miss = filter_out_local_interfaces(rt_appl_miss) - if (len(rt_appl_miss) != 0): + if rt_appl_miss or rt_asic_miss: + # Look for subscribe updates for a second + adds, deletes = get_subscribe_updates(selector, subs) + + # Drop all those for which SET received + rt_appl_miss, _ = diff_sorted_lists(rt_appl_miss, adds) + + # Drop all those for which DEL received + rt_asic_miss, _ = diff_sorted_lists(rt_asic_miss, deletes) + + if rt_appl_miss: results["missed_ROUTE_TABLE_routes"] = rt_appl_miss - err_present = True - if (len(intf_appl_miss) != 0): + if intf_appl_miss: results["missed_INTF_TABLE_entries"] = intf_appl_miss - err_present = True - if (len(rt_asic_miss) != 0): + if rt_asic_miss: results["Unaccounted_ROUTE_ENTRY_TABLE_entries"] = rt_asic_miss - err_present = True - if err_present: - print_message(syslog.LOG_ERR, "results: {", json.dumps(results, indent=4), "}") + if results: + print_message(syslog.LOG_ERR, "Failure results: {", json.dumps(results, indent=4), "}") print_message(syslog.LOG_ERR, "Failed. Look at reported mismatches above") - return -1 + print_message(syslog.LOG_ERR, "add: {", json.dumps(adds, indent=4), "}") + print_message(syslog.LOG_ERR, "del: {", json.dumps(deletes, indent=4), "}") + return -1, results else: print_message(syslog.LOG_INFO, "All good!") - return 0 + return 0, None -def main(argv): +def main(): + """ + main entry point, which mainly parses the args and call check_routes + In case of single run, it returns on one call or stays in forever loop + with given interval in-between calls to check_route + :return Same return value as returned by check_route. + """ interval = 0 parser=argparse.ArgumentParser(description="Verify routes between APPL-DB & ASIC-DB are in sync") parser.add_argument('-m', "--mode", type=Level, choices=list(Level), default='ERR') @@ -255,15 +444,20 @@ def main(argv): interval = MAX_SCAN_INTERVAL else: interval = args.interval + if UNIT_TESTING: + interval = 1 while True: - ret = check_routes() + ret, res= check_routes() if interval: time.sleep(interval) + if UNIT_TESTING: + return ret, res else: - sys.exit(ret) + return ret, res + if __name__ == "__main__": - main(sys.argv[1:]) + sys.exit(main()[0]) diff --git a/tests/route_check_test.py b/tests/route_check_test.py new file mode 100644 index 0000000000..c7c0d47b88 --- /dev/null +++ b/tests/route_check_test.py @@ -0,0 +1,431 @@ +import copy +import json +import os +import sys +from unittest.mock import MagicMock, patch + +import pytest + +sys.path.append("scripts") +import route_check + +DESCR = "Description" +ARGS = "args" +RET = "return" +APPL_DB = 0 +ASIC_DB = 1 +PRE = "pre-value" +UPD = "update" +RESULT = "res" + +OP_SET = "SET" +OP_DEL = "DEL" + +ROUTE_TABLE = 'ROUTE_TABLE' +INTF_TABLE = 'INTF_TABLE' +RT_ENTRY_TABLE = 'ASIC_STATE' +SEPARATOR = ":" + +RT_ENTRY_KEY_PREFIX = 'SAI_OBJECT_TYPE_ROUTE_ENTRY:{\"dest":\"' +RT_ENTRY_KEY_SUFFIX = '\",\"switch_id\":\"oid:0x21000000000000\",\"vr\":\"oid:0x3000000000023\"}' + +current_test_name = None +current_test_no = None +current_test_data = None + +tables_returned = {} + +selector_returned = None +subscribers_returned = {} + +test_data = { + "0": { + DESCR: "basic good one", + ARGS: "route_check -m INFO -i 1000", + PRE: { + APPL_DB: { + ROUTE_TABLE: { + "0.0.0.0/0" : { "ifname": "portchannel0" }, + "10.10.196.12/31" : { "ifname": "portchannel0" }, + "10.10.196.20/31" : { "ifname": "portchannel0" }, + "10.10.196.30/31" : { "ifname": "lo" } + }, + INTF_TABLE: { + "PortChannel1013:10.10.196.24/31": {}, + "PortChannel1023:2603:10b0:503:df4::5d/126": {}, + "PortChannel1024": {} + } + }, + ASIC_DB: { + RT_ENTRY_TABLE: { + RT_ENTRY_KEY_PREFIX + "10.10.196.12/31" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "10.10.196.20/31" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "10.10.196.24/32" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "2603:10b0:503:df4::5d/128" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "0.0.0.0/0" + RT_ENTRY_KEY_SUFFIX: {} + } + } + } + }, + "1": { + DESCR: "With updates", + ARGS: "route_check -m DEBUG -i 1", + PRE: { + APPL_DB: { + ROUTE_TABLE: { + "0.0.0.0/0" : { "ifname": "portchannel0" }, + "10.10.196.12/31" : { "ifname": "portchannel0" }, + "10.10.196.20/31" : { "ifname": "portchannel0" }, + "10.10.196.30/31" : { "ifname": "lo" } + }, + INTF_TABLE: { + "PortChannel1013:10.10.196.24/31": {}, + "PortChannel1023:2603:10b0:503:df4::5d/126": {} + } + }, + ASIC_DB: { + RT_ENTRY_TABLE: { + RT_ENTRY_KEY_PREFIX + "10.10.196.20/31" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "10.10.196.24/32" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "2603:10b0:503:df4::5d/128" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "0.0.0.0/0" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "10.10.10.10/32" + RT_ENTRY_KEY_SUFFIX: {} + } + } + }, + UPD: { + ASIC_DB: { + RT_ENTRY_TABLE: { + OP_SET: { + RT_ENTRY_KEY_PREFIX + "10.10.196.12/31" + RT_ENTRY_KEY_SUFFIX: {}, + }, + OP_DEL: { + RT_ENTRY_KEY_PREFIX + "10.10.10.10/32" + RT_ENTRY_KEY_SUFFIX: {} + } + } + } + } + }, + "2": { + DESCR: "basic failure one", + ARGS: "route_check -i 15", + RET: -1, + PRE: { + APPL_DB: { + ROUTE_TABLE: { + "0.0.0.0/0" : { "ifname": "portchannel0" }, + "10.10.196.12/31" : { "ifname": "portchannel0" }, + "10.10.196.20/31" : { "ifname": "portchannel0" }, + "10.10.196.30/31" : { "ifname": "lo" } + }, + INTF_TABLE: { + "PortChannel1013:90.10.196.24/31": {}, + "PortChannel1023:9603:10b0:503:df4::5d/126": {}, + "PortChannel1024": {} + } + }, + ASIC_DB: { + RT_ENTRY_TABLE: { + RT_ENTRY_KEY_PREFIX + "20.10.196.12/31" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "20.10.196.20/31" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "20.10.196.24/32" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "3603:10b0:503:df4::5d/128" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "0.0.0.0/0" + RT_ENTRY_KEY_SUFFIX: {} + } + } + }, + RESULT: { + "missed_ROUTE_TABLE_routes": [ + "10.10.196.12/31", + "10.10.196.20/31" + ], + "missed_INTF_TABLE_entries": [ + "90.10.196.24/32", + "9603:10b0:503:df4::5d/128" + ], + "Unaccounted_ROUTE_ENTRY_TABLE_entries": [ + "20.10.196.12/31", + "20.10.196.20/31", + "20.10.196.24/32", + "3603:10b0:503:df4::5d/128" + ] + } + }, + "3": { + DESCR: "basic good one with no args", + ARGS: "route_check", + PRE: { + APPL_DB: { + ROUTE_TABLE: { + "0.0.0.0/0" : { "ifname": "portchannel0" }, + "10.10.196.12/31" : { "ifname": "portchannel0" }, + "10.10.196.20/31" : { "ifname": "portchannel0" }, + "10.10.196.30/31" : { "ifname": "lo" } + }, + INTF_TABLE: { + "PortChannel1013:10.10.196.24/31": {}, + "PortChannel1023:2603:10b0:503:df4::5d/126": {}, + "PortChannel1024": {} + } + }, + ASIC_DB: { + RT_ENTRY_TABLE: { + RT_ENTRY_KEY_PREFIX + "10.10.196.12/31" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "10.10.196.20/31" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "10.10.196.24/32" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "2603:10b0:503:df4::5d/128" + RT_ENTRY_KEY_SUFFIX: {}, + RT_ENTRY_KEY_PREFIX + "0.0.0.0/0" + RT_ENTRY_KEY_SUFFIX: {} + } + } + } + } +} + +def do_start_test(tname, tno, ctdata): + global current_test_name, current_test_no, current_test_data + global tables_returned, selector_returned, subscribers_returned + + current_test_name = tname + current_test_no = tno + current_test_data = ctdata + tables_returned = {} + + selector_returned = None + subscribers_returned = {} + + print("Starting test case {} number={}".format(tname, tno)) + + +def check_subset(d_sub, d_all): + if type(d_sub) != type(d_all): + return -1 + if not type(d_sub) is dict: + ret = 0 if d_sub == d_all else -2 + return ret + + for (k, v) in d_sub.items(): + if not k in d_all: + return -3 + ret = check_subset(v, d_all[k]) + if ret != 0: + return ret + return 0 + + +def recursive_update(d, t): + assert (type(t) is dict) + for k in t.keys(): + if type(t[k]) is not dict: + d.update(t) + return + + if k not in d: + d[k] = {} + recursive_update(d[k], t[k]) + + +class Table: + + def __init__(self, db, tbl): + self.db = db + self.tbl = tbl + self.data = copy.deepcopy(self.get_val(current_test_data[PRE], [db, tbl])) + # print("Table:init: db={} tbl={} data={}".format(db, tbl, json.dumps(self.data, indent=4))) + + + def update(self): + t = copy.deepcopy(self.get_val(current_test_data.get(UPD, {}), + [self.db, self.tbl, OP_SET])) + drop = copy.deepcopy(self.get_val(current_test_data.get(UPD, {}), + [self.db, self.tbl, OP_DEL])) + if t: + recursive_update(self.data, t) + + for k in drop: + self.data.pop(k, None) + return (list(t.keys()), list(drop.keys())) + + + def get_val(self, d, keys): + for k in keys: + d = d[k] if k in d else {} + return d + + + def getKeys(self): + return list(self.data.keys()) + + + def get(self, key): + ret = copy.deepcopy(self.data.get(key, {})) + return (True, ret) + + +db_conns = {"APPL_DB": APPL_DB, "ASIC_DB": ASIC_DB} +def conn_side_effect(arg, _): + return db_conns[arg] + + +def table_side_effect(db, tbl): + if not db in tables_returned: + tables_returned[db] = {} + if not tbl in tables_returned[db]: + tables_returned[db][tbl] = Table(db, tbl) + return tables_returned[db][tbl] + + +class mock_selector: + TIMEOUT = 1 + + def __init__(self): + self.select_state = 0 + self.select_cnt = 0 + self.subs = None + # print("Mock Selector constructed") + + + def addSelectable(self, subs): + self.subs = subs + return 0 + + + def select(self, timeout): + # Toggle between good & timeout + # + state = self.select_state + self.subs.update() + + if self.select_state == 0: + self.select_state = self.TIMEOUT + else: + time.sleep(timeout) + + return (state, None) + + +class mock_db_conn: + def __init__(self, db): + self.db_name = None + for (k, v) in db_conns.items(): + if v == db: + self.db_name = k + assert self.db_name != None + + def getDbName(self): + return self.db_name + + +class mock_subscriber: + def __init__(self, db, tbl): + self.state = PRE + self.db = db + self.tbl = tbl + self.dbconn = mock_db_conn(db) + self.mock_tbl = table_side_effect(self.db, self.tbl) + self.set_keys = list(self.mock_tbl.data.keys()) + self.del_keys = [] + + + def update(self): + if self.state == PRE: + s_keys, d_keys = self.mock_tbl.update() + self.set_keys += s_keys + self.del_keys += d_keys + self.state = UPD + + + def pop(self): + v = None + if self.set_keys: + op = OP_SET + k = self.set_keys.pop(0) + v = self.mock_tbl.get(k)[1] + elif self.del_keys: + op = OP_DEL + k = self.del_keys.pop(0) + else: + k = "" + op = "" + + print("state={} k={} op={} v={}".format(self.state, k, op, str(v))) + return (k, op, v) + + + def getDbConnector(self): + return self.dbconn + + + def getTableName(self): + return self.tbl + + +def subscriber_side_effect(db, tbl): + global subscribers_returned + + key = "db_{}_tbl_{}".format(db, tbl) + if not key in subscribers_returned: + subscribers_returned[key] = mock_subscriber(db, tbl) + return subscribers_returned[key] + + +def select_side_effect(): + global selector_returned + + if not selector_returned: + selector_returned = mock_selector() + return selector_returned + + +def table_side_effect(db, tbl): + if not db in tables_returned: + tables_returned[db] = {} + if not tbl in tables_returned[db]: + tables_returned[db][tbl] = Table(db, tbl) + return tables_returned[db][tbl] + + +def set_mock(mock_table, mock_conn, mock_sel, mock_subs): + mock_conn.side_effect = conn_side_effect + mock_table.side_effect = table_side_effect + mock_sel.side_effect = select_side_effect + mock_subs.side_effect = subscriber_side_effect + + +class TestRouteCheck(object): + def setup(self): + pass + + def init(self): + route_check.UNIT_TESTING = 1 + + + @patch("route_check.swsscommon.DBConnector") + @patch("route_check.swsscommon.Table") + @patch("route_check.swsscommon.Select") + @patch("route_check.swsscommon.SubscriberStateTable") + def test_server(self, mock_subs, mock_sel, mock_table, mock_conn): + self.init() + ret = 0 + + set_mock(mock_table, mock_conn, mock_sel, mock_subs) + for (i, ct_data) in test_data.items(): + do_start_test("route_test", i, ct_data) + + with patch('sys.argv', ct_data[ARGS].split()): + ret, res = route_check.main() + expect_ret = ct_data[RET] if RET in ct_data else 0 + expect_res = ct_data[RESULT] if RESULT in ct_data else None + if res: + print("res={}".format(json.dumps(res, indent=4))) + if expect_res: + print("expect_res={}".format(json.dumps(expect_res, indent=4))) + assert ret == expect_ret + assert res == expect_res + + + + + + + +