From 35331d235e014949ca9f97ce691326c592acf377 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 17 Sep 2026 13:31:00 -0700 Subject: [PATCH] Tighten inbox coverage --- .github/workflows/run-unit-tests.yml | 2 +- test/test_companion_inbox.py | 64 ++++++++++++++++++- ...anion_john.py => test_companion_reader.py} | 8 ++- test/test_indicator_messages_profile.py | 2 + 4 files changed, 69 insertions(+), 7 deletions(-) rename test/{test_companion_john.py => test_companion_reader.py} (97%) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 7a4fc56b..df7dd2d8 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -195,7 +195,7 @@ jobs: working-directory: test run: >- python3 -B -m unittest - test_message_navigation test_companion_john test_reader_touch_coordinates + test_message_navigation test_companion_reader test_reader_touch_coordinates test_touch_debug_overlay test_touch_debug_cli test_color_theme test_indicator_messages_profile test_t096_message_footer_profile test_indicator_display_profile test_indicator_render_profile diff --git a/test/test_companion_inbox.py b/test/test_companion_inbox.py index 4d72e9c7..0274a40f 100644 --- a/test/test_companion_inbox.py +++ b/test/test_companion_inbox.py @@ -158,6 +158,24 @@ int main() { assert(task.getPreviewCount() == 32 && task.getMsgCount() == 0); displayPowerPrefs().inbox = DisplayInboxMode::Pending; assert(!messages.hasMessages()); + +#if COMPANION_FEATURE_JOHN + // Receiving and downloading messages must leave an open reader alone. + Screen reader; + task.john_reader = task.curr = &reader; + for (bool connected : {false, true}) { + task.connected = connected; + for (int i = 0; i < 2; ++i) { + mesh.receive("while reading"); + assert(task.isJohnReaderActive()); + } + for (int remaining : {1, 0}) { + mesh.download(); + assert(task.getMsgCount() == remaining); + assert(task.isJohnReaderActive()); + } + } +#endif } ''' @@ -260,13 +278,13 @@ int main() { ''' self.compile_and_run(preamble + implementation) - def compile_and_run(self, source): + def compile_and_run(self, source, reader_enabled=0): compiler = shutil.which("g++") or shutil.which("clang++") self.assertIsNotNone(compiler) with tempfile.TemporaryDirectory(prefix="mesh-inbox-test-") as directory: executable = Path(directory) / "inbox.exe" result = subprocess.run([ - compiler, "-std=c++17", "-DCOMPANION_FEATURE_JOHN=0", + compiler, "-std=c++17", f"-DCOMPANION_FEATURE_JOHN={reader_enabled}", "-I" + str(ROOT / "src"), "-I" + str(ROOT / "test/mocks"), "-x", "c++", "-", str(ROOT / "src/helpers/ui/MomentaryButton.cpp"), str(ROOT / "src/helpers/ui/DisplayDriver.cpp"), "-o", str(executable), @@ -287,7 +305,47 @@ int main() { implementation += QUEUE for signature in ("bool MyMesh::addToOfflineQueue(", "int MyMesh::getFromOfflineQueue("): implementation += extract_braced(mesh, signature) + "\n" - self.compile_and_run(preamble + implementation + SCENARIOS) + for reader_enabled in (0, 1): + with self.subTest(reader_enabled=reader_enabled): + self.compile_and_run(preamble + implementation + SCENARIOS, reader_enabled) + + def test_summary_shows_newest_visible_message_per_thread(self): + ui = (ROOT / "examples/companion_radio/ui-new/UITask.cpp").read_text() + implementation = extract_braced(ui, "class MsgPreviewScreen :") + ";\n" + scenarios = r''' +struct SummaryDisplay : Display { + SummaryDisplay() { setDimensions(160, 160); } +}; +int main() { + using namespace mesh::ui; + SummaryDisplay display; + UITask task(display); + MsgPreviewScreen messages(&task); + messages.addPreview(1, "Alice", "old public", 0, "Public", 0); + messages.addPreview(1, "Bob", "pending public", 0, "Public", 1); + messages.addPreview(1, "Alice", "delivered public", 0, "Public"); + messages.addPreview(1, "Carol", "second channel", 2, "Second", 2); + messages.addPreview(0xFF, "Dan", "old direct", -1, nullptr, 3); + messages.addPreview(0xFF, "Dan", "new direct", -1, nullptr, 4); + + for (auto mode : {DisplayInboxMode::History, DisplayInboxMode::Pending, + DisplayInboxMode::Unread}) { + displayPowerPrefs().inbox = mode; + display.clear(); + messages.renderSummary(display); + assert(display.rectangles.size() == 3); // One divider per thread. + assert(display.contains("Public") && display.contains("Second")); + assert(display.contains("Direct: Dan")); + assert(display.contains("second channel") && display.contains("new direct")); + assert(!display.contains("old public") && !display.contains("old direct")); + // A delivered newer entry cannot hide the newest pending entry. + const bool pending = mode == DisplayInboxMode::Pending; + assert(display.contains("pending public") == pending); + assert(display.contains("delivered public") == !pending); + } +} +''' + self.compile_and_run(PREAMBLE + implementation + scenarios) def test_summary_uses_newest_visible_message_per_thread(self): ui = (ROOT / "examples/companion_radio/ui-new/UITask.cpp").read_text() diff --git a/test/test_companion_john.py b/test/test_companion_reader.py similarity index 97% rename from test/test_companion_john.py rename to test/test_companion_reader.py index 0215d1a7..56063c84 100644 --- a/test/test_companion_john.py +++ b/test/test_companion_reader.py @@ -20,7 +20,7 @@ packer = importlib.util.module_from_spec(spec) spec.loader.exec_module(packer) -class CompanionJohnTest(unittest.TestCase): +class CompanionReaderTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.document = json.loads(packer.SOURCE.read_text(encoding="utf-8")) @@ -83,11 +83,11 @@ class CompanionJohnTest(unittest.TestCase): cc, cxx = shutil.which("gcc"), shutil.which("g++") if not cc or not cxx: self.skipTest("host GCC and G++ required") - with tempfile.TemporaryDirectory(prefix="meshcore-john-") as directory: + with tempfile.TemporaryDirectory(prefix="meshcore-reader-") as directory: directory = Path(directory) for platform in ("ESP32_PLATFORM", "NRF52_PLATFORM"): flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"] - obj, binary = directory / "tinf.o", directory / "john.exe" + obj, binary = directory / "tinf.o", directory / "reader.exe" self.run_checked([cc, "-std=c11", "-Os", *flags, "-c", str(ROOT / "src/helpers/ota/OtaTinf.c"), "-o", str(obj)]) self.run_checked([cxx, "-std=c++17", "-Os", "-Wall", "-Wextra", "-Werror", @@ -151,6 +151,8 @@ class CompanionJohnTest(unittest.TestCase): self.assertIn("#if COMPANION_FEATURE_JOHN\n#include \"JohnReaderScreen.h\"", ui) self.assertLess(ui.index("#define UI_BUTTON_READER_HINT 1"), ui.index('#include "JohnReaderScreen.h"')) + # Queue downloads preserve the current screen unconditionally now; + # test_companion_inbox exercises this with the reader enabled. self.assertIn("else if (isJohnReaderActive())", ui) self.assertIn("c = handleLongPress(KEY_ENTER);", ui) self.assertIn("handleDoubleClick(KEY_PREV)", ui) diff --git a/test/test_indicator_messages_profile.py b/test/test_indicator_messages_profile.py index 79793e99..6ed7cc95 100644 --- a/test/test_indicator_messages_profile.py +++ b/test/test_indicator_messages_profile.py @@ -99,6 +99,8 @@ int main() {{ return 0; }} self.assertIn("esp_timer_get_time()", ui) # Thread selection must respect the selected inbox view. The compiled # summary regression in test_companion_inbox covers this behavior. + # Summary deduplication must consider the selected inbox mode. + # test_companion_inbox checks the rendered rows for each mode. if __name__ == "__main__":