Skip to content

lf_interop_ping_plotter.py: fix crashes, retry transient API failures#297

Merged
goyalsaurabh06 merged 5 commits into
greearb:masterfrom
goyalsaurabh06:ping_logs_latest
Jul 20, 2026
Merged

lf_interop_ping_plotter.py: fix crashes, retry transient API failures#297
goyalsaurabh06 merged 5 commits into
greearb:masterfrom
goyalsaurabh06:ping_logs_latest

Conversation

@Narayana-CT

Copy link
Copy Markdown
Contributor

Bug-fix batch for lf_interop_ping_plotter.py addressing several crashes
paths and one data-correctness bug found during a review of the report
generation and the LANforge API polling loops.

  • Report-generation crashes on bad device data. The per-device
    summary loop in generate_report()/generate_report_robo() accessed
    fields directly with no fallback, so a single device with empty
    ping_stats, a missing key, or an rtts dict made up entirely of the
    0.11 "no data" placeholder (a latent ZeroDivisionError) would abort
    report generation for every device. Replaced with a shared
    build_device_summary() helper that falls back to safe defaults
    (0/'NA'/'Unknown') per field.

  • None API responses logged but dereferenced anyway. GET /adb/
    and GET /ports/all/ responses were checked for None and logged,
    then immediately dereferenced regardless, raising TypeError on a
    transient failure — in get_pass_fail_list(), generate_dataframe(),
    and three /ports/all/ call sites (two of which built a value that
    was never actually read afterward, so removed outright; the third,
    inside the main per-poll loop, now retries).

  • Retry-before-giving-up instead of instant exit(0). Total
    Generic-endpoint loss previously killed the process immediately, even
    on a transient LANforge hiccup and even with hours of prior polling
    data already collected. has_valid_endpoint_data() +
    wait_for_valid_endpoints(timeout=40) now retry for up to 40s before
    The loop breaks (instead of exiting) into the existing
    stop/report/cleanup sequence. The same retry-then-fallback pattern was
    added for /ports/all/ (wait_for_ports_response(), falls back to
    exit(1) if still unresolved) and /adb/.
    (wait_for_adb_response(), falls back to each caller's existing
    graceful degradation — unresolved Android clients fall back to their
    own name; a group table is skipped for that one group).
    generate_report()/generate_report_robo() now guard against
    result_json/coordinate_json being empty instead of building a
    Broken report of zero data.

  • Data-misalignment bug in get_pass_fail_list(). An Android client
    That didn't resolve via /adb/ left a gap in res_list, silently
    shifting every later PASS/FAIL verdict onto the wrong device (or
    raising a pandas length-mismatch error). Fixed so every client always
    contributes exactly one aligned entry.

  • Case-mismatch bug in generate_dataframe(). Group-membership
    matching compared against lowercase 'android', but every place that
    actually sets a device's OS uses 'Android' — so the condition could
    never match, and Android devices always fall through to the slower
    Per-username /adb/ lookup instead of the direct name-match branch.

  • Endpoint status logging. Adds endpoint_status_log.csv, recording
    a row per endpoint whenever its status actually changes (not every
    poll), and dedupes the "missing endpoints" warning to fire once per
    endpoint instead of repeating every poll for the rest of the test.

Adds log_endpoint_status_change(), called from get_results() for
every endpoint on each poll, which records a timestamp/resource_id/
endpoint_name/status row to endpoint_status_log.csv whenever an
endpoint's status actually changes (including first-seen), rather
than dumping full state every poll. Resource id is resolved from
self.sta_list, branching on real vs. virtual naming since
GenCXProfile.create() names them differently (full eid for real
clients, port segment only for virtual). The CSV is created at
startup (works for both CLI and WebUI runs) and moved into the
report directory once generate_report()/generate_report_robo()
establish it.

Also dedupes the "missing endpoints" warning added earlier: each
endpoint is now only logged once when it first goes missing,
tracked via self.reported_missing_endpoints, instead of repeating
the same warning on every poll for the rest of the test.

get_results() now returns immediately when the manager gives no
response, instead of crashing on results.keys(). Both polling loops
(monitor() and the equivalent inline loop in main()) handle that
None return by pacing with the loop's normal time.sleep(1) and
loop_timer advance before continuing, so a transient API outage
skips a poll instead of crashing the test or busy-looping.

Signed-off-by: Narayana-CT <narayana.pinapatruni@candelatech.com>
…salignment

Adds build_device_summary(), a shared helper that derives the
per-device summary fields (packets sent/received/dropped, mac/ip/
bssid/ssid, channel, mode, packet loss %, min/avg/max RTT, report
name) used by every table and graph in the report. The inline loops
in generate_report() and generate_report_robo() previously accessed
device_data fields directly with no fallback, so a single device
with an empty ping_stats, a missing key, or an rtts dict made up
entirely of the 0.11 "no data" placeholder would raise (including a
latent ZeroDivisionError on sum()/len() of an emptied rtts list) and
abort report generation for every device, not just the bad one.
build_device_summary() now falls back to safe defaults (0 / 'NA' /
'Unknown') per field instead, keeping every device's contribution
positionally aligned with self.result_json's iteration order, which
generate_uptime_graph() depends on for per-device color mapping.
Also drops a leftover debug print() from the generate_report_robo()
copy of this loop.

Fixes get_pass_fail_list(): GET /adb/ returning None was logged but
then dereferenced anyway (adb_response["devices"]), crashing the
whole report; it's now checked before use, matching the equivalent
fix already applied elsewhere. Separately, and more importantly,
res_list only appended an entry for an Android client when the /adb/
lookup found a match, so any unresolved Android device silently
shifted every later entry in res_list/test_input_list one position
out of alignment with self.report_names/self.packets_sent -
producing either a pandas length-mismatch crash when the mismatched
test_input_list was assigned onto individual_report_df, or worse,
a PASS/FAIL verdict silently attributed to the wrong device. res_list
now always appends exactly one entry per client (falling back to the
client's own name when unresolved), and pass_fail_list is indexed by
range(len(self.report_names)) instead of range(len(test_input_list)).
Also hoists the /adb/ call out of the loop instead of re-fetching it
once per Android client.

Fixes generate_dataframe(): GET /adb/ returning None was logged but
then dereferenced anyway, same crash as above - now returns None
early. Also fixes an 'android' vs 'Android' case mismatch in the
group-membership check: every place in this file that actually sets
a device's os field uses the capitalized string "Android", so the
lowercase comparison here could never match, meaning Android devices
always fell through to the slower per-username /adb/ lookup branch
instead of the direct name-match branch.

Signed-off-by: Narayana-CT <narayana.pinapatruni@candelatech.com>
…loss

When every generic endpoint disappears from a poll response (all
'UNKNOWN' or no valid entries), both polling loops (monitor() and
the equivalent inline loop in main()) previously called exit(0)
immediately - killing the process before report generation ever ran,
even if the loss was a transient LANforge hiccup and even though
minutes or hours of prior polling data was already sitting in
result_json/coordinate_json.

Adds has_valid_endpoint_data(), extracted from the duplicated
isinstance/UNKNOWN-filtering check in both loops, and
wait_for_valid_endpoints(timeout=40), which retries get_results()
every 2s for up to 40s before giving up. If valid endpoint data
reappears during that window, the loop resumes normal polling; if
the timeout elapses, the loop now breaks instead of exiting, falling
through to the existing stop_generic() / report-generation / cleanup
sequence in both call sites.

Since report generation can now be reached with partial or even zero
collected data (a device that never once returned data still never
gets a result_json entry), generate_report() and generate_report_robo()
each gained a guard at the top that logs and returns early if
self.result_json / self.coordinate_json is empty, rather than
building an empty or broken report.

Signed-off-by: Narayana-CT <narayana.pinapatruni@candelatech.com>
Three call sites fetched GET /ports/all/ and dereferenced the response
(ports_response['interfaces']) without checking for None first - a
transient failure would raise TypeError.

Two of the three built a ports_data dict that was never actually read
afterward: the per-coordinate fetch in perform_robo(), and the one-time
initial fetch in main() before the polling loop (its result was always
superseded by the in-loop rebuild under `if args.virtual:` before ever
being read, even on the very first iteration). Both were dead code as
well as a crash risk, so removed outright rather than just guarded.

The third site - the in-loop rebuild under `if args.virtual:` - is the
one that matters: ports_data[station] is read a few lines later in the
same iteration, so a transient failure there would abort the whole
test run. Adds wait_for_ports_response(timeout=40), mirroring
wait_for_valid_endpoints() added for the endpoint-loss case: retries
GET /ports/all/ every 2s for up to 40s, and if it still hasn't
recovered, stops the test with exit(1) rather than crashing on the
None dereference.

Signed-off-by: Narayana-CT <narayana.pinapatruni@candelatech.com>
…ponse

Adds wait_for_adb_response(timeout=40), mirroring
wait_for_valid_endpoints()/wait_for_ports_response(): retries GET
/adb/ every 2s for up to 40s when it returns no response, in case
the failure is transient, before falling back to whatever the caller
already did for a missing response.

Unlike get_results()/ports/all/, /adb/ is only used as enrichment -
matching Android clients to CSV pass/fail thresholds
(get_pass_fail_list()) or filtering devices into group tables
(generate_dataframe()) - so neither caller's fallback changes: an
unresolved Android client in get_pass_fail_list() still falls back
to its own name (already fixed in the res_list alignment work),
and generate_dataframe() still returns None for that one group's
table on a caller loop that already treats None as "skip this
group, keep going" - confirmed by tracing group_device_map.items()
in both generate_report() and generate_report_robo(), where the
result is only consumed inside `if dataframe:`. So this only adds
a wait before either fallback kicks in, without changing what
happens after.

Verified CLI : python3 lf_interop_ping_plotter.py --mgr 192.168.207.75 --real --ping_interval 1 --ping_duration 1m --target www.google.com --use_default_config

Signed-off-by: Narayana-CT <narayana.pinapatruni@candelatech.com>
@haricharan-candela
haricharan-candela requested review from a-gavin and haricharan-candela and removed request for a-gavin and haricharan-candela July 17, 2026 07:15
@goyalsaurabh06
goyalsaurabh06 merged commit dfb2f51 into greearb:master Jul 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants