Skip to content

Commit 4712633

Browse files
sivakondri-CTmemnochproxy
authored andcommitted
test_l3.py : Integrate IoT test results and reporting into multicast report
VERIFIED CLI 1: ./test_l3.py / --lfmgr 192.168.207.78 / --test_duration 1m / --polling_interval 5s / --upstream_port eth1 / --endp_type mc_udp / --rates_are_totals / --side_b_min_bps=10000000 / --test_tag test_l3 / --use_existing_station_list / --existing_station_list 1.20.en0 / --cleanup_cx / --tos VO / --test_name Multcast / --iot_test / --iot_testname "Multicast_IoT_Test" VERIFIED CLI 2: ./test_l3.py / --lfmgr 192.168.207.78 / --test_duration 1m / --polling_interval 5s / --upstream_port eth1 / --endp_type mc_udp / --rates_are_totals / --side_b_min_bps=10000000 / --test_tag test_l3 / --use_existing_station_list / --existing_station_list 1.20.en0 / --cleanup_cx / --tos VO / --test_name Multcast Signed-off-by: sivakondri-CT <kondru.sankar@candelatech.com>
1 parent e5bcb6f commit 4712633

File tree

1 file changed

+227
-21
lines changed

1 file changed

+227
-21
lines changed

py-scripts/test_l3.py

Lines changed: 227 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,43 @@
312312
--debug
313313
--no_cleanup
314314
315+
# Example : Command Line Interface to run the Test along with IOT without device list
316+
./test_l3.py
317+
--lfmgr 192.168.207.78
318+
--test_duration 1m
319+
--polling_interval 5s
320+
--upstream_port eth1
321+
--endp_type mc_udp
322+
--rates_are_totals
323+
--side_b_min_bps=10000000
324+
--test_tag test_l3
325+
--use_existing_station_list
326+
--existing_station_list 1.20.en0
327+
--cleanup_cx
328+
--tos VO
329+
--test_name Multcast
330+
--iot_test
331+
--iot_testname "Multicast_IoT_Test"
332+
333+
# Example : Command Line Interface to run the Test along with IOT with device list
334+
./test_l3.py
335+
--lfmgr 192.168.207.78
336+
--test_duration 1m
337+
--polling_interval 5s
338+
--upstream_port eth1
339+
--endp_type mc_udp
340+
--rates_are_totals
341+
--side_b_min_bps=10000000
342+
--test_tag test_l3
343+
--use_existing_station_list
344+
--existing_station_list 1.20.en0
345+
--cleanup_cx
346+
--tos VO
347+
--test_name Multcast
348+
--iot_test
349+
--iot_testname "Multicast_IoT_Test"
350+
--iot_device_list "switch.smart_plug_1_socket_1"
351+
315352
316353
SCRIPT_CLASSIFICATION: Creation & Runs Traffic
317354
@@ -646,7 +683,7 @@
646683
import json
647684
import shutil
648685
import threading
649-
686+
from collections import OrderedDict
650687

651688
import asyncio
652689
import copy
@@ -663,6 +700,7 @@
663700
lf_logger_config = importlib.import_module("py-scripts.lf_logger_config")
664701
LFUtils = importlib.import_module("py-json.LANforge.LFUtils")
665702
realm = importlib.import_module("py-json.realm")
703+
LFCliBase = realm.LFCliBase
666704
DeviceConfig = importlib.import_module("py-scripts.DeviceConfig")
667705
lf_attenuator = importlib.import_module("py-scripts.lf_atten_mod_test")
668706
lf_modify_radio = importlib.import_module("py-scripts.lf_modify_radio")
@@ -6014,11 +6052,31 @@ def add_live_view_images_to_report(self):
60146052
self.report.set_custom_html(f'<img src="file://{rssi_image_path}"></img>')
60156053
self.report.build_custom()
60166054

6017-
def generate_report(self, config_devices=None, group_device_map=None):
6018-
self.report.set_obj_html("Objective", "The Layer 3 Traffic Generation Test is designed to test the performance of the "
6019-
"Access Point by running layer 3 Cross-Connect Traffic. Layer-3 Cross-Connects represent a stream "
6020-
"of data flowing through the system under test. A Cross-Connect (CX) is composed of two Endpoints, "
6021-
"each of which is associated with a particular Port (physical or virtual interface).")
6055+
def generate_report(self, config_devices=None, group_device_map=None, iot_summary=None):
6056+
if iot_summary:
6057+
self.report.set_obj_html(
6058+
"Objective",
6059+
"The Candela Multicast Test Including IoT Devices is designed to evaluate an Access "
6060+
"Point’s efficiency, reliability, and scalability in handling multicast communication "
6061+
"across both Real clients (Android, Windows, Linux, iOS) and IoT devices (controlled "
6062+
"via Home Assistant). "
6063+
"For Real clients, the test simulates multicast traffic and measures key metrics such "
6064+
"as performance, latency, and packet delivery to assess how well the AP sustains "
6065+
"multicast communication under real-world conditions. "
6066+
"For IoT clients, the test concurrently executes device-specific actions (e.g., camera "
6067+
"streaming, switch toggling, lock/unlock) while multicast traffic is active, monitoring "
6068+
"success rate, latency, and failure rate. The goal is to validate that the AP can "
6069+
"reliably manage multicast traffic for Real clients while ensuring consistent "
6070+
"responsiveness and control of IoT devices."
6071+
)
6072+
else:
6073+
self.report.set_obj_html(
6074+
"Objective",
6075+
"The Layer 3 Traffic Generation Test is designed to test the performance of the "
6076+
"Access Point by running layer 3 Cross-Connect Traffic. Layer-3 Cross-Connects represent a stream "
6077+
"of data flowing through the system under test. A Cross-Connect (CX) is composed of two Endpoints, "
6078+
"each of which is associated with a particular Port (physical or virtual interface)."
6079+
)
60226080

60236081
self.report.build_objective()
60246082
test_setup_info = {
@@ -6030,6 +6088,8 @@ def generate_report(self, config_devices=None, group_device_map=None):
60306088

60316089
self.report.set_table_title("Device Under Test Information")
60326090
self.report.build_table_title()
6091+
if iot_summary:
6092+
test_setup_info = with_iot_params_in_table(test_setup_info, iot_summary)
60336093
self.report.test_setup_table(value="Device Under Test",
60346094
test_setup_data=test_setup_info)
60356095
# For real devices when groups specified for configuration
@@ -6416,6 +6476,8 @@ def generate_report(self, config_devices=None, group_device_map=None):
64166476
self.report.build_table_title()
64176477
self.report.set_table_dataframe(last_row)
64186478
self.report.build_table()
6479+
if iot_summary:
6480+
self.build_iot_report_section(self.report, iot_summary)
64196481

64206482
def write_report(self):
64216483
"""Write out HTML and PDF report as configured."""
@@ -6609,6 +6671,123 @@ def get_pass_fail_list(self, tos, up, down):
66096671
pass_fail_list.append('FAIL')
66106672
return test_input_list, pass_fail_list
66116673

6674+
def build_iot_report_section(self, report, iot_summary):
6675+
"""
6676+
Handles all IoT-related charts, tables, and increment-wise reports.
6677+
"""
6678+
outdir = report.path_date_time
6679+
os.makedirs(outdir, exist_ok=True)
6680+
6681+
def copy_into_report(raw_path, new_name):
6682+
"""Resolve and copy image into report dir."""
6683+
if not raw_path:
6684+
return None
6685+
6686+
abs_src = os.path.abspath(raw_path)
6687+
if not os.path.exists(abs_src):
6688+
# Search recursively under 'results' if absolute path missing
6689+
for root, _, files in os.walk(os.path.join(os.getcwd(), "results")):
6690+
if os.path.basename(raw_path) in files:
6691+
abs_src = os.path.join(root, os.path.basename(raw_path))
6692+
break
6693+
else:
6694+
return None
6695+
6696+
dst = os.path.join(outdir, new_name)
6697+
if os.path.abspath(abs_src) != os.path.abspath(dst):
6698+
shutil.copy2(abs_src, dst)
6699+
return new_name
6700+
6701+
# section header
6702+
report.set_custom_html('<div style="page-break-before: always;"></div>')
6703+
report.build_custom()
6704+
report.set_custom_html('<h2><u>IoT Results</u></h2>')
6705+
report.build_custom()
6706+
6707+
# Statistics
6708+
stats_png = copy_into_report(iot_summary.get("statistics_img"), "iot_statistics.png")
6709+
if stats_png:
6710+
report.build_chart_title("Test Statistics")
6711+
report.set_custom_html(f'<img src="{stats_png}" style="width:100%; height:auto;">')
6712+
report.build_custom()
6713+
6714+
# Request vs Latency
6715+
rvl_png = copy_into_report(iot_summary.get("req_vs_latency_img"), "iot_request_vs_latency.png")
6716+
if rvl_png:
6717+
report.build_chart_title("Request vs Average Latency")
6718+
report.set_custom_html(f'<img src="{rvl_png}" style="width:100%;">')
6719+
report.build_custom()
6720+
6721+
# Overall results table
6722+
ort = iot_summary.get("overall_result_table") or {}
6723+
if ort:
6724+
rows = [{
6725+
"Device": dev,
6726+
"Min Latency (ms)": stats.get("min_latency"),
6727+
"Avg Latency (ms)": stats.get("avg_latency"),
6728+
"Max Latency (ms)": stats.get("max_latency"),
6729+
"Total Iterations": stats.get("total_iterations"),
6730+
"Success Iters": stats.get("success_iterations"),
6731+
"Failed Iters": stats.get("failed_iterations"),
6732+
"No-Response Iters": stats.get("no_response_iterations"),
6733+
} for dev, stats in ort.items()]
6734+
6735+
df_overall = pd.DataFrame(rows).round(2)
6736+
6737+
report.set_custom_html('<div style="page-break-inside: avoid;">')
6738+
report.build_custom()
6739+
report.set_obj_html(_obj_title="Overall IoT Result Table", _obj=" ")
6740+
report.build_objective()
6741+
report.set_table_dataframe(df_overall)
6742+
report.build_table()
6743+
report.set_custom_html('</div>')
6744+
report.build_custom()
6745+
6746+
# Increment reports
6747+
inc = iot_summary.get("increment_reports") or {}
6748+
if inc:
6749+
report.set_custom_html('<h3>Reports by Increment Steps</h3>')
6750+
report.build_custom()
6751+
6752+
for step_name, rep in inc.items():
6753+
6754+
report.set_custom_html(f'<h4><u>{step_name.replace("_", " ")}</u></h4>')
6755+
report.build_custom()
6756+
6757+
# Latency graph
6758+
lat_png = copy_into_report(rep.get("latency_graph"), f"iot_{step_name}_latency.png")
6759+
if lat_png:
6760+
report.build_chart_title("Average Latency")
6761+
report.set_custom_html(f'<img src="{lat_png}" style="width:100%; height:auto;">')
6762+
report.build_custom()
6763+
6764+
# Success count graph
6765+
res_png = copy_into_report(rep.get("result_graph"), f"iot_{step_name}_results.png")
6766+
if res_png:
6767+
report.build_chart_title("Success Count")
6768+
report.set_custom_html(f'<img src="{res_png}" style="width:100%; height:auto;">')
6769+
report.build_custom()
6770+
6771+
# Tabular data for detailed iteration-level results
6772+
data_rows = rep.get("data") or []
6773+
if data_rows:
6774+
df = pd.DataFrame(data_rows).rename(
6775+
columns={"latency__ms": "Latency_ms", "latency_ms": "Latency_ms"}
6776+
)
6777+
if "Latency_ms" in df.columns:
6778+
df["Latency_ms"] = pd.to_numeric(df["Latency_ms"], errors="coerce").round(3)
6779+
if "Result" in df.columns:
6780+
df["Result"] = df["Result"].map(lambda x: "Success" if bool(x) else "Failure")
6781+
6782+
desired_cols = ["Iteration", "Device", "Current State", "Latency_ms", "Result"]
6783+
df = df[[c for c in desired_cols if c in df.columns]]
6784+
6785+
report.set_table_dataframe(df)
6786+
report.build_table()
6787+
6788+
report.set_custom_html('<hr>')
6789+
report.build_custom()
6790+
66126791

66136792
# Converting the upstream_port to IP address for configuration purposes
66146793
def change_port_to_ip(upstream_port, lfclient_host, lfclient_port):
@@ -7968,6 +8147,38 @@ def parse_args():
79688147
# https://stackoverflow.com/questions/37304799/cross-platform-safe-to-use-command-line-string-separator
79698148
#
79708149
# Safe to exit in this function, as this should only be called by this script
8150+
def with_iot_params_in_table(base: dict, iot_summary) -> dict:
8151+
"""
8152+
Append IoT params into the existing Throughput Input Parameters table.
8153+
Adds: IoT Test name, IoT Iterations, IoT Delay (s), IoT Increment.
8154+
Accepts dict or JSON string.
8155+
"""
8156+
try:
8157+
if not iot_summary:
8158+
return base
8159+
if isinstance(iot_summary, str):
8160+
try:
8161+
iot_summary = json.loads(iot_summary)
8162+
except Exception:
8163+
start = iot_summary.find("{")
8164+
end = iot_summary.rfind("}")
8165+
if start == -1 or end == -1 or end <= start:
8166+
return base
8167+
try:
8168+
iot_summary = json.loads(iot_summary[start:end + 1])
8169+
except Exception:
8170+
return base
8171+
8172+
ti = (iot_summary.get("test_input_table") or {})
8173+
out = OrderedDict(base)
8174+
out["Iot Device List"] = ti.get("Device List", "")
8175+
out["IoT Iterations"] = ti.get("Iterations", "")
8176+
out["IoT Delay (s)"] = ti.get("Delay (seconds)", "")
8177+
out["IoT Increment"] = ti.get("Increment Pattern", "")
8178+
return out
8179+
except Exception:
8180+
return base
8181+
79718182

79728183
def trigger_iot(ip, port, iterations, delay, device_list, testname, increment):
79738184
"""
@@ -8042,18 +8253,6 @@ async def run_iot(ip: str = '127.0.0.1',
80428253
logger.info('Iot Test Completed.')
80438254

80448255

8045-
def duration_to_seconds(duration: str) -> int:
8046-
duration = duration.strip().lower()
8047-
if duration.endswith("s"):
8048-
return int(duration[:-1])
8049-
elif duration.endswith("m"):
8050-
return int(duration[:-1]) * 60
8051-
elif duration.endswith("h"):
8052-
return int(duration[:-1]) * 3600
8053-
else:
8054-
return int(duration)
8055-
8056-
80578256
def main():
80588257
endp_types = "lf_udp"
80598258

@@ -8186,7 +8385,7 @@ def main():
81868385
thread = threading.Thread(target=trigger_iot, args=(iot_ip, iot_port, iot_iterations, iot_delay, iot_device_list, iot_testname, iot_increment))
81878386
thread.start()
81888387
else:
8189-
total_secs = duration_to_seconds(args.test_duration)
8388+
total_secs = int(LFCliBase.parse_time(args.test_duration).total_seconds())
81908389
iot_iterations = max(1, total_secs // args.iot_delay)
81918390
iot_thread = threading.Thread(
81928391
target=trigger_iot,
@@ -8769,12 +8968,19 @@ def main():
87698968
ip_var_test.set_report_obj(report=report)
87708969
if args.dowebgui:
87718970
ip_var_test.webgui_finalize()
8971+
iot_summary = None
8972+
if args.iot_test and args.iot_testname:
8973+
base = os.path.join("results", args.iot_testname)
8974+
p = os.path.join(base, "iot_summary.json")
8975+
if os.path.exists(p):
8976+
with open(p) as f:
8977+
iot_summary = json.load(f)
87728978
# Generate and write out test report
87738979
logger.info("Generating test report")
87748980
if args.real:
8775-
ip_var_test.generate_report(config_devices, group_device_map)
8981+
ip_var_test.generate_report(config_devices, group_device_map, iot_summary=iot_summary)
87768982
else:
8777-
ip_var_test.generate_report()
8983+
ip_var_test.generate_report(iot_summary=iot_summary)
87788984
ip_var_test.write_report()
87798985

87808986
# TODO move to after reporting

0 commit comments

Comments
 (0)