From f67bc13e3ecb66028de7c2b4bc182d5a7f5740f9 Mon Sep 17 00:00:00 2001 From: Narayana-CT Date: Thu, 23 Jul 2026 13:22:29 +0530 Subject: [PATCH 1/5] lf_interop_port_reset_test.py: Add retry wrapper for json_get on transient LANforge API failures json_get() returns None on any failed request (bad status, connection error, malformed JSON) with no retry, so a momentary LANforge hiccup during wifi-msgs/port-state lookups could crash the reset loop with an unhandled NoneType error. Add json_get_with_retry(), which retries every 5s for up to 40s before giving up, and route all 9 json_get call sites in InteropPortReset through it. Signed-off-by: Narayana-CT --- py-scripts/lf_interop_port_reset_test.py | 38 ++++++++++++++++++------ 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/py-scripts/lf_interop_port_reset_test.py b/py-scripts/lf_interop_port_reset_test.py index e5bd2b6ad..77e410865 100755 --- a/py-scripts/lf_interop_port_reset_test.py +++ b/py-scripts/lf_interop_port_reset_test.py @@ -170,6 +170,26 @@ def __init__(self, host, # logging.basicConfig(filename='port_reset.log', filemode='w', format='%(asctime)s - %(message)s', # level=logging.INFO, force=True) + def json_get_with_retry(self, url, wait_time=40, poll_interval=5, debug_=False): + """ + Calls self.json_get(url), retrying every poll_interval seconds for up + to wait_time seconds if LANforge returns no response. + """ + start_time = time.time() + response = self.json_get(url, debug_=debug_) + while response is None and (time.time() - start_time) < wait_time: + logging.warning(f"GET {url} returned no response from LANforge; retrying...") + time.sleep(poll_interval) + response = self.json_get(url, debug_=debug_) + + if response is None: + logging.error( + f"GET {url} returned no response from LANforge after waiting " + f"{wait_time} seconds." + ) + + return response + def selecting_devices_from_available(self): # If device list is not provided by user, then it shows the available devices to choose from if self.device_list is None: @@ -236,7 +256,7 @@ def remove_files_with_duplicate_names(self, folder_path): file_names[file_name] = file_path def get_last_wifi_msg(self): - a = self.json_get("/wifi-msgs/last/1", debug_=True) + a = self.json_get_with_retry("/wifi-msgs/last/1", debug_=True) last = a['wifi-messages']['time-stamp'] logging.info(f"Last WiFi Message Time Stamp: {last}") return last @@ -305,7 +325,7 @@ def get_count(self, value=None, keys_list=None, device=None, filter=None): def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, file_name="dummy.json", reset_cnt=None): # print("Waiting for 20 sec to fetch the logs...") # time.sleep(20) - a = self.json_get("/wifi-msgs/since=time/" + str(timee), debug_=True) + a = self.json_get_with_retry("/wifi-msgs/since=time/" + str(timee), debug_=True) values = a['wifi-messages'] # print("Wifi msgs Response : ", values) logging.info( @@ -323,7 +343,7 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi # print("Key list", keys_list) # android (flag) check for clustered lanforge cases android = False - for device_data in self.json_get('/adb/')['devices']: + for device_data in self.json_get_with_retry('/adb/')['devices']: device_name, _ = list(device_data.keys())[0], list(device_data.values())[0] if phn_name in device_name: android = True @@ -383,10 +403,10 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi local_dict[str(phn_name)]["Association Rejection"] = adb_association_rejection if adb_connected_count > 0: _, shelf, serial = phn_name.split('.') - resource_id = self.json_get('/adb/1/{}/{}?fields=resource-id'.format(shelf, serial)) + resource_id = self.json_get_with_retry('/adb/1/{}/{}?fields=resource-id'.format(shelf, serial)) resource_id = resource_id['devices']['resource-id'] - port_ssid_query = self.json_get('port/1/{}/wlan0?fields=cx time (us)'.format(resource_id.split('.')[1])) + port_ssid_query = self.json_get_with_retry('port/1/{}/wlan0?fields=cx time (us)'.format(resource_id.split('.')[1])) uptime = port_ssid_query['interface']['cx time (us)'] local_dict[str(phn_name)]['cx time (us)'] = uptime else: @@ -430,7 +450,7 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi # Double-checking if win_connected_count > 1 or win_connected_count == 0: port_name = phn_name.split(".") - port_ssid_query = self.json_get(f"port/{port_name[0]}/{port_name[1]}/{port_name[2]}?fields=ssid,ip") + port_ssid_query = self.json_get_with_retry(f"port/{port_name[0]}/{port_name[1]}/{port_name[2]}?fields=ssid,ip") if port_ssid_query['interface']['ssid'] == self.ssid and port_ssid_query['interface']['ip'] != "0.0.0.0": win_connected_count = 1 else: @@ -450,7 +470,7 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi local_dict[str(phn_name)]["Remarks"] = remarks if win_connected_count > 0: port_name = phn_name.split(".") - port_ssid_query = self.json_get(f"port/{port_name[0]}/{port_name[1]}/{port_name[2]}?fields=cx time (us)") + port_ssid_query = self.json_get_with_retry(f"port/{port_name[0]}/{port_name[1]}/{port_name[2]}?fields=cx time (us)") uptime = port_ssid_query['interface']['cx time (us)'] local_dict[str(phn_name)]['cx time (us)'] = uptime else: @@ -494,7 +514,7 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi # Double-checking & adding remarks if any if other_connected_count > 1 or other_connected_count == 0: port_name = phn_name.split(".") - port_ssid_query = self.json_get(f"port/{port_name[0]}/{port_name[1]}/{port_name[2]}?fields=ssid,ip") + port_ssid_query = self.json_get_with_retry(f"port/{port_name[0]}/{port_name[1]}/{port_name[2]}?fields=ssid,ip") if port_ssid_query['interface']['ssid'] == self.ssid and port_ssid_query['interface']['ip'] != "0.0.0.0": other_connected_count = 1 else: @@ -514,7 +534,7 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi local_dict[str(phn_name)]["Remarks"] = remarks if other_connected_count > 0: port_name = phn_name.split(".") - port_ssid_query = self.json_get(f"port/{port_name[0]}/{port_name[1]}/{port_name[2]}?fields=cx time (us)") + port_ssid_query = self.json_get_with_retry(f"port/{port_name[0]}/{port_name[1]}/{port_name[2]}?fields=cx time (us)") uptime = port_ssid_query['interface']['cx time (us)'] local_dict[str(phn_name)]['cx time (us)'] = uptime else: From fa6507191c1df71763f5ae5e68ccb48f6721c72f Mon Sep 17 00:00:00 2001 From: Narayana-CT Date: Thu, 23 Jul 2026 15:18:35 +0530 Subject: [PATCH 2/5] lf_interop_port_reset_test.py: Add retry wrapper for json_get and make mgr_ip resolution fail loudly json_get() returns None on any failed request (bad status, connection error, malformed JSON) with no retry, so a momentary LANforge hiccup during wifi-msgs/port-state lookups could crash the reset loop with an unhandled NoneType error. Add json_get_with_retry(), which retries every 5s for up to 40s before giving up, and route all 9 json_get call sites in InteropPortReset through it. Also fold change_port_to_ip() into InteropPortReset as a method (reusing self.name_to_eid/self.json_get_with_retry instead of a throwaway Realm object), and have it abort the test if the manager IP can't be resolved rather than silently continuing with an unresolved port name. Since this now needs an instance to call into, mgr_ip resolution moves to run right after InteropPortReset is constructed instead of before, patching base_interop_profile.server_ip afterward since it captured the unresolved value at construction time. Signed-off-by: Narayana-CT --- py-scripts/lf_interop_port_reset_test.py | 52 +++++++++++++++--------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/py-scripts/lf_interop_port_reset_test.py b/py-scripts/lf_interop_port_reset_test.py index 77e410865..23a8a8124 100755 --- a/py-scripts/lf_interop_port_reset_test.py +++ b/py-scripts/lf_interop_port_reset_test.py @@ -75,7 +75,6 @@ Realm = realm.Realm lf_report_pdf = importlib.import_module("py-scripts.lf_report") lf_graph = importlib.import_module("py-scripts.lf_graph") -LFUtils = importlib.import_module("py-json.LANforge.LFUtils") logger = logging.getLogger(__name__) lf_logger_config = importlib.import_module("py-scripts.lf_logger_config") @@ -190,6 +189,29 @@ def json_get_with_retry(self, url, wait_time=40, poll_interval=5, debug_=False): return response + def change_port_to_ip(self, upstream_port): + # Resolves an ethernet port name (e.g. "eth1") to its IP via LANforge, used + # for --mgr_ip. This is only called once, at test start, and we cannot + # proceed without a resolved manager IP, so retry for a while and then + # abort instead of silently continuing with an unresolved port name. + if upstream_port.count('.') != 3: + target_port_list = self.name_to_eid(upstream_port) + shelf, resource, port, _ = target_port_list + response = self.json_get_with_retry(f'/port/{shelf}/{resource}/{port}?fields=ip') + try: + upstream_port = response['interface']['ip'] + except (TypeError, KeyError) as e: + logging.error( + f"change_port_to_ip: could not resolve upstream port '{upstream_port}' to an IP; LANforge " + f"response is not in expected format ({e}). Data received: {response}") + logging.critical("Aborting the test: a valid manager IP is required to proceed.") + exit(1) + logging.info(f"Upstream port IP {upstream_port}") + else: + logging.info(f"Upstream port IP {upstream_port}") + + return upstream_port + def selecting_devices_from_available(self): # If device list is not provided by user, then it shows the available devices to choose from if self.device_list is None: @@ -1607,23 +1629,6 @@ def create_dict_csv(self, port_reset_dict): print(df_summary) -def change_port_to_ip(upstream_port, lfclient_host, lfclient_port): - if upstream_port.count('.') != 3: - target_port_list = LFUtils.name_to_eid(upstream_port) - shelf, resource, port, _ = target_port_list - try: - realm_obj = Realm(lfclient_host=lfclient_host, lfclient_port=lfclient_port) - target_port_ip = realm_obj.json_get(f'/port/{shelf}/{resource}/{port}?fields=ip')['interface']['ip'] - upstream_port = target_port_ip - except Exception: - logging.warning(f'The upstream port is not an ethernet port. Proceeding with the given upstream_port {upstream_port}.') - logging.info(f"Upstream port IP {upstream_port}") - else: - logging.info(f"Upstream port IP {upstream_port}") - - return upstream_port - - def main(): help_summary = '''\ The LANforge interop port reset test enables users to use real Wi-Fi stations and connect them to the Access Point @@ -1757,8 +1762,6 @@ def main(): if args.log_level: logger_config.set_level(level=args.log_level) - args.mgr_ip = change_port_to_ip(args.mgr_ip, args.host, args.port) - print(args.mgr_ip) if args.lf_logger_config_json: # logger_config.lf_logger_config_json = "lf_logger_config.json" logger_config.lf_logger_config_json = args.lf_logger_config_json @@ -1789,6 +1792,15 @@ def main(): get_live_view=args.get_live_view, total_floors=args.total_floors ) + + # mgr_ip may have been given as a port name (e.g. "eth1") rather than an IP; + # resolve it now that we have an instance to reuse for the LANforge lookup. + # base_interop_profile.server_ip was already captured from the unresolved + # mgr_ip at construction time above, so it must be patched too. + obj.mgr_ip = obj.change_port_to_ip(obj.mgr_ip) + obj.base_interop_profile.server_ip = obj.mgr_ip + print(obj.mgr_ip) + obj.selecting_devices_from_available() if obj.robot_test: obj.run() From c647d2996fb2e3a8901990552a81d4b279a6bfae Mon Sep 17 00:00:00 2001 From: Narayana-CT Date: Thu, 23 Jul 2026 17:46:06 +0530 Subject: [PATCH 3/5] lf_interop_port_reset_test.py: handle malformed LANforge responses in wifi-msgs/adb lookups get_last_wifi_msg, get_time_from_wifi_msgs, and its /adb/ device-list check all indexed straight into the json_get_with_retry() response (a['wifi-messages']['time-stamp'], etc.) with no guard, so a bad or None response would raise TypeError/KeyError and crash whichever reset iteration was in flight. Wrap each in try/except, log the error with the raw response received, and degrade instead of crashing: - get_last_wifi_msg falls back to a 'NA' baseline timestamp. That 'NA' harmlessly propagates into get_time_from_wifi_msgs's own fallback below rather than needing special-case handling. - get_time_from_wifi_msgs's wifi-msgs fetch falls back to marking just that device's row 'NA' for the reset and returns early, so one bad response doesn't take down every other device's stats. - The /adb/ device-list lookup (a secondary android-detection signal used only for clustered LANforge setups) falls back to an empty list, so classification relies on the '1.1.' prefix heuristic alone. This is logged as an error since it can misclassify an android device outside '1.1.' in a clustered setup. Signed-off-by: Narayana-CT Signed-off-by: Narayana-CT --- py-scripts/lf_interop_port_reset_test.py | 32 +++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/py-scripts/lf_interop_port_reset_test.py b/py-scripts/lf_interop_port_reset_test.py index 23a8a8124..8976e8159 100755 --- a/py-scripts/lf_interop_port_reset_test.py +++ b/py-scripts/lf_interop_port_reset_test.py @@ -279,7 +279,13 @@ def remove_files_with_duplicate_names(self, folder_path): def get_last_wifi_msg(self): a = self.json_get_with_retry("/wifi-msgs/last/1", debug_=True) - last = a['wifi-messages']['time-stamp'] + try: + last = a['wifi-messages']['time-stamp'] + except (TypeError, KeyError) as e: + logging.error( + f"get_last_wifi_msg: LANforge response is not in expected format ({e}). Data received: {a}") + logging.warning("Could not establish a wifi-msgs baseline timestamp; falling back to 'NA' for this reset.") + return "NA" logging.info(f"Last WiFi Message Time Stamp: {last}") return last @@ -348,7 +354,17 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi # print("Waiting for 20 sec to fetch the logs...") # time.sleep(20) a = self.json_get_with_retry("/wifi-msgs/since=time/" + str(timee), debug_=True) - values = a['wifi-messages'] + try: + values = a['wifi-messages'] + except (TypeError, KeyError) as e: + logging.error( + f"get_time_from_wifi_msgs: LANforge response is not in expected format ({e}) while fetching " + f"wifi-msgs for device {phn_name}. Data received: {a}") + logging.warning(f"Marking device {phn_name} stats as 'NA' for this reset and continuing.") + for key in ("ConnectAttempt", "Disconnected", "Scanning", "Association Rejection", "Connected", + "Remarks", "cx time (us)"): + local_dict[str(phn_name)][key] = "NA" + return local_dict # print("Wifi msgs Response : ", values) logging.info( f"Counting the DISCONNECTIONS, SCANNING, ASSOC ATTEMPTS, ASSOC RECJECTIONS, CONNECTS for device {phn_name}") @@ -365,7 +381,17 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi # print("Key list", keys_list) # android (flag) check for clustered lanforge cases android = False - for device_data in self.json_get_with_retry('/adb/')['devices']: + adb_response = self.json_get_with_retry('/adb/') + try: + adb_devices = adb_response['devices'] + except (TypeError, KeyError) as e: + logging.error( + f"get_time_from_wifi_msgs: LANforge response is not in expected format ({e}) while fetching " + f"/adb/ devices. Data received: {adb_response}") + logging.warning( + f"Could not fetch adb device list; relying only on the '1.1.' prefix heuristic for {phn_name}.") + adb_devices = [] + for device_data in adb_devices: device_name, _ = list(device_data.keys())[0], list(device_data.values())[0] if phn_name in device_name: android = True From 47c63cfb858f65b106426740da3f262afcd9f4b4 Mon Sep 17 00:00:00 2001 From: Narayana-CT Date: Thu, 23 Jul 2026 18:03:25 +0530 Subject: [PATCH 4/5] lf_interop_port_reset_test.py: handle malformed LANforge responses in wifi-msgs/adb/port lookups get_last_wifi_msg, get_time_from_wifi_msgs, and its /adb/ and port lookups all indexed straight into the json_get_with_retry() response (a['wifi-messages']['time-stamp'], port_ssid_query['interface']['ssid'], etc.) with no guard, so a bad or None response would raise TypeError/KeyError and crash whichever reset iteration was in flight. Wrap each in try/except, log the error with the raw response received, and degrade instead of crashing: - get_last_wifi_msg falls back to a 'NA' baseline timestamp. That 'NA' harmlessly propagates into get_time_from_wifi_msgs's own fallback below rather than needing special-case handling. - get_time_from_wifi_msgs's wifi-msgs fetch falls back to marking just that device's row 'NA' for the reset and returns early, so one bad response doesn't take down every other device's stats. - The /adb/ device-list lookup (a secondary android-detection signal used only for clustered LANforge setups) falls back to an empty list, so classification relies on the '1.1.' prefix heuristic alone. This is logged as an error since it can misclassify an android device outside '1.1.' in a clustered setup. - The android resource-id + cx-time lookup, and the Windows/Other ssid-ip double-check + cx-time lookups, all fall back to 'NA' for cx time; the ssid-ip double-checks default to "not connected" (0) when the port state can't be verified, since we can't confirm the connection without a readable response. Signed-off-by: Narayana-CT --- py-scripts/lf_interop_port_reset_test.py | 65 ++++++++++++++++++------ 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/py-scripts/lf_interop_port_reset_test.py b/py-scripts/lf_interop_port_reset_test.py index 8976e8159..40ab5e2eb 100755 --- a/py-scripts/lf_interop_port_reset_test.py +++ b/py-scripts/lf_interop_port_reset_test.py @@ -451,12 +451,19 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi local_dict[str(phn_name)]["Association Rejection"] = adb_association_rejection if adb_connected_count > 0: _, shelf, serial = phn_name.split('.') - resource_id = self.json_get_with_retry('/adb/1/{}/{}?fields=resource-id'.format(shelf, serial)) - resource_id = resource_id['devices']['resource-id'] - - port_ssid_query = self.json_get_with_retry('port/1/{}/wlan0?fields=cx time (us)'.format(resource_id.split('.')[1])) - uptime = port_ssid_query['interface']['cx time (us)'] - local_dict[str(phn_name)]['cx time (us)'] = uptime + resource_id_resp = self.json_get_with_retry('/adb/1/{}/{}?fields=resource-id'.format(shelf, serial)) + port_ssid_query = None + try: + resource_id = resource_id_resp['devices']['resource-id'] + port_ssid_query = self.json_get_with_retry('port/1/{}/wlan0?fields=cx time (us)'.format(resource_id.split('.')[1])) + uptime = port_ssid_query['interface']['cx time (us)'] + local_dict[str(phn_name)]['cx time (us)'] = uptime + except (TypeError, KeyError) as e: + logging.error( + f"get_time_from_wifi_msgs: could not fetch cx time (us) for device {phn_name}; LANforge " + f"response is not in expected format ({e}). resource-id response: {resource_id_resp}, " + f"port response: {port_ssid_query}") + local_dict[str(phn_name)]['cx time (us)'] = 'NA' else: local_dict[str(phn_name)]['cx time (us)'] = 'NA' else: @@ -499,9 +506,16 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi if win_connected_count > 1 or win_connected_count == 0: port_name = phn_name.split(".") port_ssid_query = self.json_get_with_retry(f"port/{port_name[0]}/{port_name[1]}/{port_name[2]}?fields=ssid,ip") - if port_ssid_query['interface']['ssid'] == self.ssid and port_ssid_query['interface']['ip'] != "0.0.0.0": - win_connected_count = 1 - else: + try: + if port_ssid_query['interface']['ssid'] == self.ssid and port_ssid_query['interface']['ip'] != "0.0.0.0": + win_connected_count = 1 + else: + win_connected_count = 0 + except (TypeError, KeyError) as e: + logging.error( + f"get_time_from_wifi_msgs: could not verify connection state for device " + f"{phn_name}; LANforge response is not in expected format ({e}). Data " + f"received: {port_ssid_query}") win_connected_count = 0 logging.info("Final Connected Count for %s: %s" % (phn_name, win_connected_count)) local_dict[str(phn_name)]["Connected"] = win_connected_count @@ -519,8 +533,14 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi if win_connected_count > 0: port_name = phn_name.split(".") port_ssid_query = self.json_get_with_retry(f"port/{port_name[0]}/{port_name[1]}/{port_name[2]}?fields=cx time (us)") - uptime = port_ssid_query['interface']['cx time (us)'] - local_dict[str(phn_name)]['cx time (us)'] = uptime + try: + uptime = port_ssid_query['interface']['cx time (us)'] + local_dict[str(phn_name)]['cx time (us)'] = uptime + except (TypeError, KeyError) as e: + logging.error( + f"get_time_from_wifi_msgs: could not fetch cx time (us) for device {phn_name}; " + f"LANforge response is not in expected format ({e}). Data received: {port_ssid_query}") + local_dict[str(phn_name)]['cx time (us)'] = 'NA' else: local_dict[str(phn_name)]['cx time (us)'] = 'NA' else: # other means (for linux, mac) @@ -563,9 +583,16 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi if other_connected_count > 1 or other_connected_count == 0: port_name = phn_name.split(".") port_ssid_query = self.json_get_with_retry(f"port/{port_name[0]}/{port_name[1]}/{port_name[2]}?fields=ssid,ip") - if port_ssid_query['interface']['ssid'] == self.ssid and port_ssid_query['interface']['ip'] != "0.0.0.0": - other_connected_count = 1 - else: + try: + if port_ssid_query['interface']['ssid'] == self.ssid and port_ssid_query['interface']['ip'] != "0.0.0.0": + other_connected_count = 1 + else: + other_connected_count = 0 + except (TypeError, KeyError) as e: + logging.error( + f"get_time_from_wifi_msgs: could not verify connection state for device " + f"{phn_name}; LANforge response is not in expected format ({e}). Data " + f"received: {port_ssid_query}") other_connected_count = 0 logging.info("Final Connected Count for %s: %s" % (phn_name, other_connected_count)) local_dict[str(phn_name)]["Connected"] = other_connected_count @@ -583,8 +610,14 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi if other_connected_count > 0: port_name = phn_name.split(".") port_ssid_query = self.json_get_with_retry(f"port/{port_name[0]}/{port_name[1]}/{port_name[2]}?fields=cx time (us)") - uptime = port_ssid_query['interface']['cx time (us)'] - local_dict[str(phn_name)]['cx time (us)'] = uptime + try: + uptime = port_ssid_query['interface']['cx time (us)'] + local_dict[str(phn_name)]['cx time (us)'] = uptime + except (TypeError, KeyError) as e: + logging.error( + f"get_time_from_wifi_msgs: could not fetch cx time (us) for device {phn_name}; " + f"LANforge response is not in expected format ({e}). Data received: {port_ssid_query}") + local_dict[str(phn_name)]['cx time (us)'] = 'NA' else: local_dict[str(phn_name)]['cx time (us)'] = 'NA' logging.info("local_dict " + str(local_dict)) From 36700ede70fd15c50dd8e2356c0bc32bca28f883 Mon Sep 17 00:00:00 2001 From: Narayana-CT Date: Fri, 24 Jul 2026 10:59:37 +0530 Subject: [PATCH 5/5] lf_interop_port_reset_test.py: avoid false pass/fail data from wifi-msgs/port fallbacks Two issues surfaced with the 'NA' fallbacks added in 47c63cfb: 1. Setting ConnectAttempt/Disconnected/Scanning/Association Rejection/ Connected to the string 'NA' crashes every numeric aggregator that consumes them: create_dict_csv does `0 += stats.get(...) or 0`, and generate_overall_graph/generate_overall_graph_table/ individual_client_info all do raw `+`/`sum()`/`int()` over these fields with no type guard. A single LANforge hiccup on one device's wifi-msgs fetch would crash the very next create_dict_csv() call in the default (non-robot_test) reset loop. Switch that fallback to 0 for the numeric fields instead (matches the existing convention for ambiguous counts elsewhere in this file), and use the Remarks field - which is never summed - to flag the row as "Data unavailable - LANforge API error, stats defaulted to 0" so it isn't misread as a genuinely clean reset. The early return in this fallback also skipped the per-device CSV write at the bottom of get_time_from_wifi_msgs, silently dropping that reset's snapshot if the failing device was the last one processed. Extract that write into write_reset_csvs() and call it from both the fallback and the normal return path. 2. The Windows/Other ssid-ip double-check (used to resolve an ambiguous connected-count) forces connected_count to 0 when the verification call fails. That 0 then flows into the existing Remarks logic a few lines later, which can assert "The Disconnections are seen but Client did not connected to user given SSID." - a confident, specific claim that the device failed to reconnect, when the real cause was an unrelated API error during verification, not a real disconnect. Track the failure with a flag and override Remarks with "Connection state unverified - LANforge API error while double-checking connect count" instead, so a verification failure can no longer masquerade as a real connection failure in the report. Robot-test reporting (generate_report_for_robo and friends) has separate, pre-existing gaps in Remarks visibility and is left untouched for now. VERIFIED CLI: python3 lf_interop_port_reset_test.py --host 192.168.207.75 --mgr_ip 192.168.9.14 --dut DemoDUT --ssid NETGEAR_2G_wpa2 --passwd Password@123 --encryp psk2 --reset 3 --time_int 5 --release 11 Signed-off-by: Narayana-CT --- py-scripts/lf_interop_port_reset_test.py | 25 ++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/py-scripts/lf_interop_port_reset_test.py b/py-scripts/lf_interop_port_reset_test.py index 40ab5e2eb..51f8dc3f0 100755 --- a/py-scripts/lf_interop_port_reset_test.py +++ b/py-scripts/lf_interop_port_reset_test.py @@ -360,10 +360,12 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi logging.error( f"get_time_from_wifi_msgs: LANforge response is not in expected format ({e}) while fetching " f"wifi-msgs for device {phn_name}. Data received: {a}") - logging.warning(f"Marking device {phn_name} stats as 'NA' for this reset and continuing.") - for key in ("ConnectAttempt", "Disconnected", "Scanning", "Association Rejection", "Connected", - "Remarks", "cx time (us)"): - local_dict[str(phn_name)][key] = "NA" + logging.warning(f"Defaulting device {phn_name} stats to 0 for this reset and continuing.") + for key in ("ConnectAttempt", "Disconnected", "Scanning", "Association Rejection", "Connected"): + local_dict[str(phn_name)][key] = 0 + local_dict[str(phn_name)]["Remarks"] = "Data unavailable - LANforge API error, stats defaulted to 0" + local_dict[str(phn_name)]["cx time (us)"] = "NA" + self.write_reset_csvs(local_dict, reset_cnt) return local_dict # print("Wifi msgs Response : ", values) logging.info( @@ -490,6 +492,7 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi local_dict[str(phn_name)]["Association Rejection"] = win_association_rejection win_connected_count = self.get_count(value=values, keys_list=keys_list, device=phn_name, filter="connected") + win_connection_state_unverified = False # assoc-rejection based logic if win_association_rejection: # Updating the connects @@ -517,6 +520,7 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi f"{phn_name}; LANforge response is not in expected format ({e}). Data " f"received: {port_ssid_query}") win_connected_count = 0 + win_connection_state_unverified = True logging.info("Final Connected Count for %s: %s" % (phn_name, win_connected_count)) local_dict[str(phn_name)]["Connected"] = win_connected_count # Updating the association-rejections @@ -529,6 +533,8 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi remarks = "No Disconnections are seen but Client is UP and connected to user given SSID." elif win_disconnect_count >= 1 and win_connected_count == 0: remarks = "The Disconnections are seen but Client did not connected to user given SSID." + if win_connection_state_unverified: + remarks = "Connection state unverified - LANforge API error while double-checking connect count" local_dict[str(phn_name)]["Remarks"] = remarks if win_connected_count > 0: port_name = phn_name.split(".") @@ -567,6 +573,7 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi other_connected_count = self.get_count(value=values, keys_list=keys_list, device=phn_name, filter="<3>CTRL-EVENT-CONNECTED") + other_connection_state_unverified = False # assoc-rejection based logic if other_association_rejection: # Updating the connects @@ -594,6 +601,7 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi f"{phn_name}; LANforge response is not in expected format ({e}). Data " f"received: {port_ssid_query}") other_connected_count = 0 + other_connection_state_unverified = True logging.info("Final Connected Count for %s: %s" % (phn_name, other_connected_count)) local_dict[str(phn_name)]["Connected"] = other_connected_count # Updating the association-rejections @@ -606,6 +614,8 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi remarks = "No Disconnections are seen but Client is UP and connected to user given SSID." elif other_disconnect_count >= 1 and other_connected_count == 0: remarks = "The Disconnections are seen but Client did not connected to user given SSID." + if other_connection_state_unverified: + remarks = "Connection state unverified - LANforge API error while double-checking connect count" local_dict[str(phn_name)]["Remarks"] = remarks if other_connected_count > 0: port_name = phn_name.split(".") @@ -621,14 +631,17 @@ def get_time_from_wifi_msgs(self, local_dict=None, phn_name=None, timee=None, fi else: local_dict[str(phn_name)]['cx time (us)'] = 'NA' logging.info("local_dict " + str(local_dict)) + self.write_reset_csvs(local_dict, reset_cnt) + + return local_dict + + def write_reset_csvs(self, local_dict, reset_cnt): # storing results in csv file for each reset for interface_name, metrics in local_dict.items(): df = pd.DataFrame([metrics]) filename = f"{self.report_path}/{interface_name}_{reset_cnt}.csv" df.to_csv(filename, index=False) - return local_dict - def aggregate_reset_dict(self, reset_dict): aggregated = {} for reset_id in sorted(reset_dict.keys()):