From 2c18a068b63f3120895a16e76ce371ca474b8216 Mon Sep 17 00:00:00 2001 From: TIM ANDERSON Date: Sun, 25 Oct 2026 09:50:21 +1000 Subject: [PATCH] Fix viewer receive statistics during redraw --- can/viewer.py | 2 +- doc/changelog.d/1407.fixed.rst | 2 + test/test_viewer.py | 77 ++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 doc/changelog.d/1407.fixed.rst diff --git a/can/viewer.py b/can/viewer.py index 8d9d228bb..41f1fd8eb 100644 --- a/can/viewer.py +++ b/can/viewer.py @@ -341,7 +341,7 @@ def redraw_screen(self): # Trigger a complete redraw self.draw_header() for ids in self.ids.values(): - self.draw_can_bus_message(ids["msg"]) + self.draw_can_bus_message(ids["msg"], sorting=True) class SmartFormatter(argparse.HelpFormatter): diff --git a/doc/changelog.d/1407.fixed.rst b/doc/changelog.d/1407.fixed.rst new file mode 100644 index 000000000..692a96b14 --- /dev/null +++ b/doc/changelog.d/1407.fixed.rst @@ -0,0 +1,2 @@ +Fixed the CAN viewer increasing message counts and resetting receive intervals +when scrolling or resizing the terminal. diff --git a/test/test_viewer.py b/test/test_viewer.py index e71d06dc8..ef3a1d3d3 100644 --- a/test/test_viewer.py +++ b/test/test_viewer.py @@ -162,6 +162,83 @@ def tearDown(self): # are parsed correctly self.can_viewer.run() + def _receive_statistics_fixture(self): + for index, (identifier, extended) in enumerate( + [(0x321, False), (0x123, False), (0x123, True)] + ): + for timestamp in (10.0 + index, 10.5 + index): + self.can_viewer.draw_can_bus_message( + can.Message( + timestamp=timestamp, + arbitration_id=identifier, + is_extended_id=extended, + data=[index], + ) + ) + return { + key: (value["count"], value["dt"], value["msg"]) + for key, value in self.can_viewer.ids.items() + } + + def _assert_receive_statistics(self, expected): + self.assertEqual(self.can_viewer.ids.keys(), expected.keys()) + for key, (count, dt, message) in expected.items(): + self.assertEqual(self.can_viewer.ids[key]["count"], count) + self.assertEqual(self.can_viewer.ids[key]["dt"], dt) + self.assertIs(self.can_viewer.ids[key]["msg"], message) + + def test_redraw_preserves_receive_statistics(self): + expected = self._receive_statistics_fixture() + start_time = self.can_viewer.start_time + for scroll in (1, 2, 0): + self.can_viewer.scroll = scroll + self.can_viewer.redraw_screen() + self._assert_receive_statistics(expected) + self.assertEqual(self.can_viewer.start_time, start_time) + + # A genuine subsequent reception must still update count and interval. + for key, (_, _, previous) in expected.items(): + self.can_viewer.draw_can_bus_message( + can.Message( + timestamp=previous.timestamp + 0.5, + arbitration_id=previous.arbitration_id, + is_extended_id=previous.is_extended_id, + data=previous.data, + ) + ) + self.assertEqual(self.can_viewer.ids[key]["count"], 3) + self.assertEqual(self.can_viewer.ids[key]["dt"], 0.5) + + def test_scrolling_preserves_receive_statistics(self): + expected = self._receive_statistics_fixture() + self.can_viewer.paused = True + # The legacy teardown can then exit immediately after this run. + self.stdscr_dummy.key_counter = 200 + with ( + patch.object( + self.stdscr_dummy, + "getch", + side_effect=[curses.KEY_DOWN, curses.KEY_UP, ord("q")], + ), + patch("curses.is_term_resized", return_value=False), + ): + self.can_viewer.run() + self.assertEqual(self.can_viewer.scroll, 0) + self._assert_receive_statistics(expected) + + def test_resizing_preserves_receive_statistics(self): + expected = self._receive_statistics_fixture() + self.can_viewer.paused = True + self.stdscr_dummy.key_counter = 200 + with ( + patch.object(self.stdscr_dummy, "getch", side_effect=[-1, ord("q")]), + patch.object(self.stdscr_dummy, "getmaxyx", return_value=(2, 100)), + patch("curses.is_term_resized", return_value=True), + ): + self.can_viewer.run() + self.assertEqual(self.can_viewer.y, 2) + self._assert_receive_statistics(expected) + def test_send(self): # CANopen EMCY data = [1, 2, 3, 4, 5, 6, 7] # Wrong length