mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 22:42:25 +00:00
Repair CI regressions for display policy and dual-radio sleep guards
Replace obsolete reader-timeout and message-thread source assertions with compiled regressions for bookmark saving across battery/USB transitions and newest-visible message summaries in History, Pending, and Unread modes. Extend the MQTT sleep harness with dual-radio state and verify both blockers release when disabled. All 19 post-native verification groups pass on Linux, including the complete UI and browser runtime suites.
This commit is contained in:
1 parent
088c88507a
commit
831a50b69f
4 files changed
+135
-2
No files matched your search
@@ -289,6 +289,45 @@ int main() {
|
||||
implementation += extract_braced(mesh, signature) + "\n"
|
||||
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()
|
||||
preamble = PREAMBLE.replace("int getMsgCount() const { return 0; }", UI_MEMBERS)
|
||||
implementation = extract_braced(ui, "class MsgPreviewScreen :") + ";\n"
|
||||
scenarios = r'''
|
||||
int main() {
|
||||
using namespace mesh::ui;
|
||||
resetArduinoMock();
|
||||
displayPowerPrefs().battery.mode = DisplayMode::On;
|
||||
Display display;
|
||||
display.servicePower(false);
|
||||
UITask task(display);
|
||||
MsgPreviewScreen messages(&task);
|
||||
task.msg_preview = task.curr = &messages;
|
||||
messages.addPreview(1, "Alice", "older pending", 0, "Public", 0);
|
||||
messages.addPreview(1, "Bob", "newer delivered", 0, "Public");
|
||||
auto expect = [&](DisplayInboxMode mode, const char* wanted, const char* hidden) {
|
||||
displayPowerPrefs().inbox = mode;
|
||||
display.clear(); messages.renderSummary(display);
|
||||
if (!display.contains(wanted) || display.contains(hidden)) {
|
||||
fprintf(stderr, "summary mode=%u expected=%s hidden=%s\n", unsigned(mode), wanted, hidden);
|
||||
for (const auto& line : display.lines) fprintf(stderr, "rendered: %s\n", line.text.c_str());
|
||||
}
|
||||
assert(display.contains(wanted) && !display.contains(hidden));
|
||||
};
|
||||
expect(DisplayInboxMode::History, "newer delivered", "older pending");
|
||||
expect(DisplayInboxMode::Pending, "older pending", "newer delivered");
|
||||
// Unread changes the count; read previews remain browseable as documented.
|
||||
displayPowerPrefs().inbox = DisplayInboxMode::History;
|
||||
display.clear(); messages.render(display);
|
||||
expect(DisplayInboxMode::Unread, "newer delivered", "older pending");
|
||||
assert(messages.messageCount() == 1);
|
||||
messages.queueRemoved(0);
|
||||
expect(DisplayInboxMode::Pending, "No messages heard", "older pending");
|
||||
expect(DisplayInboxMode::History, "newer delivered", "older pending");
|
||||
}
|
||||
'''
|
||||
self.compile_and_run(preamble + implementation + scenarios)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -10,6 +10,9 @@ import tempfile
|
||||
import unittest
|
||||
import zlib
|
||||
|
||||
from test_message_navigation import PREAMBLE
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURE = ROOT / "test/fixtures/companion_john"
|
||||
spec = importlib.util.spec_from_file_location("pack_john", ROOT / "tools/bible/pack_john.py")
|
||||
@@ -148,11 +151,93 @@ 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"'))
|
||||
self.assertIn("&& !isJohnReaderActive()", ui)
|
||||
self.assertIn("else if (isJohnReaderActive())", ui)
|
||||
self.assertIn("c = handleLongPress(KEY_ENTER);", ui)
|
||||
self.assertIn("handleDoubleClick(KEY_PREV)", ui)
|
||||
|
||||
def test_reader_bookmark_follows_display_power_transitions(self):
|
||||
ui = (ROOT / "examples/companion_radio/ui-new/UITask.cpp").read_text(encoding="utf-8")
|
||||
stubs = r'''
|
||||
struct Board {
|
||||
bool external = false, host = false;
|
||||
bool isExternalPowered() const { return external; }
|
||||
bool isUsbHostConnected() const { return host; }
|
||||
};
|
||||
struct Interfaces { bool takePairingRequest() { return false; } };
|
||||
struct JohnReaderScreen : Screen { int saves = 0; void flush() { ++saves; } };
|
||||
'''
|
||||
members = r'''
|
||||
Board* _board;
|
||||
Interfaces* _interfaceManager;
|
||||
uint32_t _pairing_screen_until = 0;
|
||||
bool hasConnection() const { return false; }
|
||||
bool isPairingScreenActive() const { return false; }
|
||||
void showPairingPin() {}
|
||||
void finishPairingScreen(bool) {}
|
||||
void servicePairingState();
|
||||
'''
|
||||
source = PREAMBLE.replace("class UITask {", stubs + "class UITask {")
|
||||
source = source.replace("int getMsgCount() const { return 0; }", members)
|
||||
source += extract_braced(ui, "void UITask::servicePairingState(") + "\n"
|
||||
source += extract_braced(ui, "char UITask::checkDisplayOn(") + r'''
|
||||
int main() {
|
||||
using namespace mesh::ui;
|
||||
resetArduinoMock();
|
||||
displayPowerPrefs().battery = {DisplayMode::Button, 5};
|
||||
displayPowerPrefs().usb = {DisplayMode::On, 5};
|
||||
Display display;
|
||||
Screen home;
|
||||
JohnReaderScreen reader;
|
||||
Board board;
|
||||
Interfaces interfaces;
|
||||
UITask task(display);
|
||||
task._board = &board; task._interfaceManager = &interfaces;
|
||||
task.curr = task.home = &home; task.john_reader = &reader;
|
||||
task.servicePairingState();
|
||||
assert(!display.isOn() && reader.saves == 0);
|
||||
task.curr = &reader;
|
||||
assert(task.checkDisplayOn(KEY_NEXT) == 0); // Wake consumes navigation.
|
||||
assert(display.isOn());
|
||||
g_mock_millis = 4999; task.servicePairingState();
|
||||
assert(display.isOn() && reader.saves == 0);
|
||||
g_mock_millis = 5000; task.servicePairingState();
|
||||
assert(!display.isOn());
|
||||
const int expected_saves = COMPANION_FEATURE_JOHN ? 1 : 0;
|
||||
assert(reader.saves == expected_saves);
|
||||
task.servicePairingState();
|
||||
assert(reader.saves == expected_saves); // No repeated writes while dark.
|
||||
for (bool* plugged : {&board.external, &board.host}) {
|
||||
*plugged = true; task.servicePairingState();
|
||||
assert(display.isOn());
|
||||
g_mock_millis += 10000; task.servicePairingState();
|
||||
assert(display.isOn()); // USB's always-on policy applies to the reader.
|
||||
int before = reader.saves;
|
||||
*plugged = false; task.servicePairingState();
|
||||
assert(!display.isOn() && reader.saves == before + expected_saves);
|
||||
}
|
||||
task.curr = &home;
|
||||
task.checkDisplayOn(KEY_NEXT);
|
||||
int before = reader.saves;
|
||||
g_mock_millis += 5000; task.servicePairingState();
|
||||
assert(!display.isOn() && reader.saves == before);
|
||||
task._display = nullptr; task.servicePairingState();
|
||||
}
|
||||
'''
|
||||
compiler = shutil.which("g++")
|
||||
self.assertIsNotNone(compiler, "host G++ required")
|
||||
with tempfile.TemporaryDirectory(prefix="mesh-reader-power-") as directory:
|
||||
directory = Path(directory)
|
||||
(directory / "power.cpp").write_text(source, encoding="utf-8")
|
||||
for enabled in (0, 1):
|
||||
with self.subTest(reader_enabled=enabled):
|
||||
binary = directory / "power.exe"
|
||||
self.run_checked([compiler, "-std=c++17", f"-DCOMPANION_FEATURE_JOHN={enabled}",
|
||||
"-I" + str(ROOT / "src"), "-I" + str(ROOT / "test/mocks"),
|
||||
str(directory / "power.cpp"),
|
||||
str(ROOT / "src/helpers/ui/MomentaryButton.cpp"),
|
||||
str(ROOT / "src/helpers/ui/DisplayDriver.cpp"), "-o", str(binary)])
|
||||
self.run_checked([str(binary)])
|
||||
|
||||
def run_checked(self, command):
|
||||
result = subprocess.run(command, capture_output=True, encoding="utf-8",
|
||||
errors="backslashreplace", timeout=60)
|
||||
|
||||
@@ -96,7 +96,8 @@ int main() {{ return 0; }}
|
||||
"companionMessageElapsedMillis(entry->heard_millis)", ui
|
||||
)
|
||||
self.assertIn("esp_timer_get_time()", ui)
|
||||
self.assertIn("history.hasNewerEntryForThread(age)", ui)
|
||||
# Thread selection must respect the selected inbox view. The compiled
|
||||
# summary regression in test_companion_inbox covers this behavior.
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -58,6 +58,8 @@ struct MyMesh {
|
||||
bool millisHasNowPassed(unsigned long) const { return false; }
|
||||
bool getNextQueueWakeDelay(uint32_t&) const { return false; }
|
||||
bool getNextRetryWakeDelay(uint32_t&) const { return false; }
|
||||
bool dual_radio = false;
|
||||
bool isDualRadioActive() const { return dual_radio; }
|
||||
uint32_t limitSleepToMillisTimer(unsigned long, uint32_t secs) const { return secs; }
|
||||
uint32_t limitSleepToScheduledRadioWork(uint32_t secs) const { return secs; }
|
||||
bool hasPendingWork() const;
|
||||
@@ -81,6 +83,12 @@ int main() {
|
||||
logger._initialized = false;
|
||||
require(node.getPowerSaveSleepSeconds(30) == 30,
|
||||
"stopping MQTT did not release its sleep blocker");
|
||||
node.dual_radio = true;
|
||||
require(node.hasPendingWork(), "dual-radio scanning lost its sleep blocker");
|
||||
require(node.getPowerSaveSleepSeconds(30) == 0, "dual-radio scanning can enter device sleep");
|
||||
node.dual_radio = false;
|
||||
require(node.getPowerSaveSleepSeconds(30) == 30,
|
||||
"single-radio mode did not release its sleep blocker");
|
||||
require(node.getPowerSaveSleepSeconds(0) == 0, "zero sleep limit changed");
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << e.what() << '\n';
|
||||
|
||||
Reference in new issue
Block a user