Allow Auto, SSD1306 or SH1106 through display.controller on display-enabled RAK4631 and RAK3401 roles. Persist the choice before restart without changing the active screen policy, and let saved Auto override legacy build defaults. Share checked settings I/O and the existing atomic writer to keep Full Companion within its flash boundary while retaining features and existing record formats. Cover save failures, backup recovery, startup selection and ambiguous OLED replies. Validation: 35 targeted host tests passed. RAK4631 Full Companion and RAK3401 unified LoRa OTA repeater build and package checks passed with release-length version strings.
572 lines
26 KiB
Python
572 lines
26 KiB
Python
"""Exercise the shared display policy and real settings/CLI code on the host."""
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
FS_STUB = r'''
|
|
#pragma once
|
|
#include <map>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <cstring>
|
|
#include <cstdint>
|
|
#define FILE_O_READ 1
|
|
#define FILE_O_WRITE 2
|
|
struct FakeFS;
|
|
struct File {
|
|
FakeFS* fs = nullptr;
|
|
std::string name;
|
|
File() = default;
|
|
explicit File(FakeFS& backing) : fs(&backing) {}
|
|
File(FakeFS* backing, const char* path) : fs(backing), name(path) {}
|
|
explicit operator bool() const { return fs != nullptr; }
|
|
bool open(const char*, int);
|
|
size_t size() const;
|
|
size_t read(uint8_t*, size_t);
|
|
size_t write(const uint8_t*, size_t);
|
|
void flush() {}
|
|
bool isDirectory() const { return false; }
|
|
void close() {}
|
|
};
|
|
struct FakeFS {
|
|
std::map<std::string, std::vector<uint8_t>> files;
|
|
bool fail_write = false, fail_publish = false;
|
|
bool corrupt_write = false, fail_temp_read = false, fail_open_write = false;
|
|
bool false_publish = false, late_publish_error = false, fail_public_read = false;
|
|
bool corrupt_public = false, replace_after_validation = false, published = false;
|
|
unsigned calls = 0, public_reads = 0;
|
|
bool exists(const char* name) { ++calls; return files.count(name); }
|
|
bool remove(const char* name) { ++calls; files.erase(name); return true; }
|
|
bool rename(const char* from, const char* to) {
|
|
++calls;
|
|
if (fail_publish && std::string(from) == "/display_prefs.tmp") return false;
|
|
if (!exists(from)) return false;
|
|
#if !defined(NRF52_PLATFORM)
|
|
if (exists(to)) return false;
|
|
#endif
|
|
if (false_publish && std::string(from) == "/display_prefs.tmp") return true;
|
|
files[to] = files[from]; files.erase(from);
|
|
if (std::string(from) == "/display_prefs.tmp") {
|
|
published = true; public_reads = 0;
|
|
if (corrupt_public) files[to][0] ^= 1;
|
|
if (late_publish_error) return false;
|
|
}
|
|
return true;
|
|
}
|
|
File open(const char* name, const char* mode, bool = false) {
|
|
++calls;
|
|
if (*mode == 'w') {
|
|
if (fail_open_write) return {};
|
|
files[name].clear();
|
|
}
|
|
if (!exists(name)) return {};
|
|
return File(this, name);
|
|
}
|
|
};
|
|
inline bool File::open(const char* path, int mode) {
|
|
*this = fs->open(path, mode == FILE_O_WRITE ? "w" : "r");
|
|
return bool(*this);
|
|
}
|
|
inline size_t File::size() const { return fs->files[name].size(); }
|
|
inline size_t File::read(uint8_t* out, size_t count) {
|
|
if (fs->fail_temp_read && name == "/display_prefs.tmp") return 0;
|
|
if (fs->published && name == "/display_prefs") {
|
|
if (fs->fail_public_read) return 0;
|
|
// Change to another valid image only after AtomicFileWriter's public
|
|
// verification, exercising the caller's independent exact-format readback.
|
|
if (++fs->public_reads == 2 && fs->replace_after_validation) {
|
|
auto& altered = fs->files[name];
|
|
altered[4] ^= 1; altered[11] ^= 1;
|
|
}
|
|
}
|
|
const auto& bytes = fs->files[name];
|
|
count = count < bytes.size() ? count : bytes.size();
|
|
memcpy(out, bytes.data(), count); return count;
|
|
}
|
|
inline size_t File::write(const uint8_t* in, size_t count) {
|
|
const size_t written = fs->fail_write ? count / 2 : count;
|
|
fs->files[name].assign(in, in + written);
|
|
if (fs->corrupt_write && written) fs->files[name][0] ^= 1;
|
|
return written;
|
|
}
|
|
#define FILESYSTEM FakeFS
|
|
'''
|
|
|
|
TEST = r'''
|
|
#include <cassert>
|
|
#include <helpers/ui/DisplayPowerSettings.h>
|
|
#if defined(NRF52_PLATFORM)
|
|
#define ATOMIC_FILE_WRITER_IMPLEMENTATION
|
|
#include <helpers/AtomicFileWriter.h>
|
|
#endif
|
|
using namespace mesh::ui;
|
|
void policyTests() {
|
|
for (unsigned mode = 0; mode <= 5; ++mode) {
|
|
DisplayPowerPrefs prefs;
|
|
prefs.battery.mode = DisplayMode(mode);
|
|
DisplayPowerPolicy p;
|
|
p.update(prefs, false, false, false, 0);
|
|
assert(p.on() == (mode == 1 || mode == 5));
|
|
p.update(prefs, false, false, false, 4000);
|
|
assert(p.on() == (mode == 1));
|
|
bool button = mode == 1 || mode == 2 || mode == 4 || mode == 5;
|
|
assert(p.wake(DisplayWake::Button, 5000) == button);
|
|
assert(p.on() == button);
|
|
p.update(prefs, false, false, false, 20000);
|
|
assert(p.on() == (mode == 1));
|
|
assert(p.wake(DisplayWake::Message, 21000) == (mode == 1 || mode == 5));
|
|
p.update(prefs, false, true, false, 22000);
|
|
assert(p.on() == (mode == 1));
|
|
assert(p.wake(DisplayWake::Message, 23000) == (mode == 1));
|
|
assert(p.wake(DisplayWake::Button, 24000) == button);
|
|
p.update(prefs, false, true, false, 40000);
|
|
p.update(prefs, false, false, true, 41000);
|
|
assert(p.on() == (mode == 1 || mode == 3 || mode == 4 || mode == 5));
|
|
p.update(prefs, false, false, true, 100000);
|
|
assert(p.on() == (mode == 1 || mode == 3 || mode == 4 || mode == 5));
|
|
p.update(prefs, false, true, false, 100001);
|
|
assert(p.on() == (mode == 1));
|
|
}
|
|
DisplayPowerPrefs prefs;
|
|
prefs.usb.mode = DisplayMode::On;
|
|
DisplayPowerPolicy p;
|
|
p.update(prefs, false, false, false, 100);
|
|
p.wake(DisplayWake::Button, 200);
|
|
p.update(prefs, true, false, false, 300);
|
|
assert(p.on());
|
|
p.update(prefs, false, false, false, 400);
|
|
assert(!p.on()); // USB wake does not leak into the battery profile.
|
|
p.wake(DisplayWake::Button, 500);
|
|
prefs.battery.seconds = 1;
|
|
p.update(prefs, false, false, false, 600);
|
|
assert(p.on());
|
|
p.update(prefs, false, false, false, 1600);
|
|
assert(!p.on());
|
|
p.wake(DisplayWake::Button, UINT32_MAX - 500);
|
|
p.update(prefs, false, false, false, 498);
|
|
assert(p.on());
|
|
p.update(prefs, false, false, false, 499);
|
|
assert(!p.on());
|
|
p.wake(DisplayWake::Button, 1000);
|
|
prefs.battery.mode = DisplayMode::Off;
|
|
p.update(prefs, false, false, true, 1001);
|
|
assert(!p.on()); // Off wins over a pending button wake and pairing.
|
|
}
|
|
void companionBootTests() {
|
|
// The startup exception is role-wide, independent of transport, power
|
|
// source, or whether an app has already connected by the first UI frame.
|
|
for (bool usb : {false, true}) for (bool connected : {false, true}) {
|
|
for (unsigned mode = 0; mode <= 5; ++mode) {
|
|
DisplayPowerPrefs prefs;
|
|
prefs.wake_on_boot = true;
|
|
prefs.battery.mode = prefs.usb.mode = DisplayMode::Off;
|
|
auto& profile = usb ? prefs.usb : prefs.battery;
|
|
profile.mode = DisplayMode(mode);
|
|
DisplayPowerPolicy p;
|
|
p.update(prefs, usb, connected, false, 100);
|
|
const bool button_mode = mode == 2 || mode == 4;
|
|
assert(p.on() == (mode == 1 || button_mode || (mode == 5 && !connected)));
|
|
p.update(prefs, usb, connected, false, 4100);
|
|
assert(p.on() == (mode == 1 || button_mode)); // Normal 15s, not 4s.
|
|
p.update(prefs, usb, connected, false, 15099);
|
|
assert(p.on() == (mode == 1 || button_mode));
|
|
p.update(prefs, usb, connected, false, 15100);
|
|
assert(p.on() == (mode == 1));
|
|
p.update(prefs, usb, connected, false, 30000);
|
|
assert(p.on() == (mode == 1)); // Refresh cannot retrigger startup.
|
|
}
|
|
}
|
|
for (DisplayMode mode : {DisplayMode::Button, DisplayMode::ButtonPairing}) {
|
|
DisplayPowerPrefs prefs;
|
|
prefs.wake_on_boot = true;
|
|
prefs.battery = prefs.usb = {mode, 27};
|
|
DisplayPowerPolicy p;
|
|
p.update(prefs, false, false, false, 100);
|
|
p.update(prefs, false, true, false, 101);
|
|
assert(p.on()); // Connecting does not dismiss a button-mode boot screen.
|
|
assert(!p.wake(DisplayWake::Message, 200));
|
|
p.update(prefs, false, true, false, 27099);
|
|
assert(p.on());
|
|
assert(p.wake(DisplayWake::Button, 27099));
|
|
p.update(prefs, false, true, false, 27100);
|
|
assert(p.on()); // Real input extends the boot timeout.
|
|
p.update(prefs, false, true, false, 54099);
|
|
assert(!p.on());
|
|
p.update(prefs, false, true, true, 54100);
|
|
assert(p.on() == (mode == DisplayMode::ButtonPairing));
|
|
p.update(prefs, false, true, true, 100000);
|
|
assert(p.on() == (mode == DisplayMode::ButtonPairing));
|
|
p.update(prefs, false, true, false, 100001);
|
|
assert(!p.on());
|
|
|
|
DisplayPowerPolicy switched;
|
|
switched.update(prefs, false, false, false, 0);
|
|
assert(switched.on());
|
|
switched.update(prefs, true, false, false, 1);
|
|
assert(!switched.on()); // No synthetic boot when the power source changes.
|
|
switched.update(prefs, false, false, false, 2);
|
|
assert(!switched.on());
|
|
prefs.battery.mode = DisplayMode::Off;
|
|
switched.update(prefs, false, false, false, 3);
|
|
prefs.battery.mode = mode;
|
|
switched.update(prefs, false, false, false, 4);
|
|
assert(!switched.on()); // No synthetic boot when the mode changes.
|
|
|
|
DisplayPowerPolicy dismissed;
|
|
dismissed.update(prefs, false, false, false, 0);
|
|
dismissed.dismiss();
|
|
dismissed.update(prefs, false, false, false, 1);
|
|
assert(!dismissed.on());
|
|
|
|
DisplayPowerPolicy rollover;
|
|
rollover.update(prefs, false, false, false, UINT32_MAX - 500);
|
|
rollover.update(prefs, false, false, false, 26498);
|
|
assert(rollover.on());
|
|
rollover.update(prefs, false, false, false, 26499);
|
|
assert(!rollover.on());
|
|
}
|
|
}
|
|
void companionSettingsTests() {
|
|
for (bool ble : {false, true}) {
|
|
FakeFS fs;
|
|
assert(!loadDisplayPowerSettings(&fs, ble, true));
|
|
assert(displayPowerPrefs().wake_on_boot);
|
|
const DisplayMode mode = ble ? DisplayMode::ButtonPairing : DisplayMode::Button;
|
|
assert(displayPowerPrefs().battery.mode == mode);
|
|
assert(displayPowerPrefs().usb.mode == mode);
|
|
char reply[160];
|
|
for (const char* command : {"set display.timeout 27", "set display.usb.timeout 123"}) {
|
|
assert(handleDisplayPowerCommand(command, reply, sizeof(reply)));
|
|
assert(std::string(reply) == "OK");
|
|
}
|
|
assert(fs.files["/display_prefs"].size() == 12); // No on-device format change.
|
|
assert(fs.files["/display_prefs"][10] == 0);
|
|
assert(loadDisplayPowerSettings(&fs, ble, true));
|
|
assert(displayPowerPrefs().wake_on_boot);
|
|
for (bool usb : {false, true}) {
|
|
DisplayPowerPolicy p;
|
|
p.update(displayPowerPrefs(), usb, false, false, 0);
|
|
assert(p.on()); // Previously saved button modes also show startup.
|
|
const uint32_t timeout = usb ? 123000 : 27000;
|
|
p.update(displayPowerPrefs(), usb, false, false, timeout - 1);
|
|
assert(p.on());
|
|
p.update(displayPowerPrefs(), usb, false, false, timeout);
|
|
assert(!p.on());
|
|
}
|
|
for (const char* command : {"set display.mode off", "set display.usb.mode off"}) {
|
|
assert(handleDisplayPowerCommand(command, reply, sizeof(reply)));
|
|
assert(std::string(reply) == "OK");
|
|
}
|
|
for (unsigned version : {1, 2}) {
|
|
auto& saved = fs.files["/display_prefs"];
|
|
saved[2] = version; saved[11] = 0;
|
|
for (unsigned i = 0; i < 11; ++i) saved[11] ^= saved[i];
|
|
assert(loadDisplayPowerSettings(&fs, ble, true));
|
|
assert(displayPowerPrefs().wake_on_boot);
|
|
for (bool usb : {false, true}) {
|
|
DisplayPowerPolicy p;
|
|
p.update(displayPowerPrefs(), usb, false, true, 0);
|
|
assert(!p.on()); // Saved off wins over startup and pairing.
|
|
assert(!p.wake(DisplayWake::Button, 1));
|
|
}
|
|
}
|
|
assert(loadDisplayPowerSettings(&fs, ble));
|
|
assert(!displayPowerPrefs().wake_on_boot); // Infrastructure does not opt in.
|
|
assert(!loadDisplayPowerSettings(nullptr, ble, true));
|
|
assert(displayPowerPrefs().wake_on_boot); // Defaults still work without flash.
|
|
DisplayPowerPolicy p;
|
|
p.update(displayPowerPrefs(), false, false, false, 0);
|
|
assert(p.on());
|
|
assert(!loadDisplayPowerSettings(nullptr, ble));
|
|
assert(!displayPowerPrefs().wake_on_boot);
|
|
}
|
|
}
|
|
void commandTests() {
|
|
FakeFS fs;
|
|
assert(!loadDisplayPowerSettings(&fs, true));
|
|
assert(displayPowerPrefs().battery.mode == DisplayMode::ButtonPairing);
|
|
char reply[160];
|
|
auto run = [&](const char* text, const char* expected) {
|
|
assert(handleDisplayPowerCommand(text, reply, sizeof(reply)));
|
|
assert(std::string(reply) == expected);
|
|
};
|
|
run("set display.mode automatic", "OK");
|
|
run("set display.timeout 27", "OK");
|
|
run("set display.usb.mode on", "OK");
|
|
run("set display.usb.timeout 123", "OK");
|
|
assert(loadDisplayPowerSettings(&fs, true));
|
|
run("get display.mode", "> automatic");
|
|
run("get display.timeout", "> 27");
|
|
run("get display.usb.mode", "> on");
|
|
run("get display.usb.timeout", "> 123");
|
|
// Earlier 12-byte v1 profiles retain their power settings and gain History.
|
|
auto v1 = fs.files["/display_prefs"];
|
|
v1[2] = 1; v1[9] = 0; v1[11] = 0;
|
|
for (unsigned i = 0; i < 11; ++i) v1[11] ^= v1[i];
|
|
fs.files["/display_prefs"] = v1;
|
|
assert(loadDisplayPowerSettings(&fs, true));
|
|
assert(displayPowerPrefs().inbox == DisplayInboxMode::History);
|
|
run("get display.timeout", "> 27");
|
|
run("get display.inbox", "Error: inbox modes unsupported");
|
|
auto inbox = [&](const char* text, const char* expected) {
|
|
assert(handleDisplayPowerCommand(text, reply, sizeof(reply), true));
|
|
assert(std::string(reply) == expected);
|
|
};
|
|
inbox("get display.inbox", "> history");
|
|
for (const char* mode : {"pending", "unread", "history"}) {
|
|
inbox((std::string("set display.inbox ") + mode).c_str(), "OK");
|
|
assert(loadDisplayPowerSettings(&fs, true));
|
|
inbox("get display.inbox", (std::string("> ") + mode).c_str());
|
|
run("get display.timeout", "> 27");
|
|
run("get display.usb.timeout", "> 123");
|
|
}
|
|
inbox("set display.inbox typo", "Error: use history|pending|unread");
|
|
inbox("set display.inbox", "Error: use history|pending|unread");
|
|
inbox("get display.inbox extra", "Error: unexpected argument");
|
|
assert(!handleDisplayPowerCommand("get display.inboxes", reply, sizeof(reply), true));
|
|
const auto saved = fs.files["/display_prefs"];
|
|
for (const char* value : {"", "0", "-1", "+2", "1.5", "15seconds", "3601", "9999999999999999999999"}) {
|
|
const std::string command = std::string("set display.timeout ") + value;
|
|
run(command.c_str(), "Error: timeout must be 1-3600 seconds");
|
|
assert(fs.files["/display_prefs"] == saved);
|
|
}
|
|
run("set display.mode typo", "Error: use off|on|button|pairing|button-pairing|automatic");
|
|
assert(!handleDisplayPowerCommand("set display.model off", reply, sizeof(reply)));
|
|
fs.fail_write = true;
|
|
inbox("set display.inbox unread", "Error: display settings save failed");
|
|
assert(displayPowerPrefs().inbox == DisplayInboxMode::History);
|
|
run("set display.mode off", "Error: display settings save failed");
|
|
assert(displayPowerPrefs().battery.mode == DisplayMode::Automatic);
|
|
assert(fs.files["/display_prefs"] == saved);
|
|
fs.fail_write = false; fs.fail_publish = true;
|
|
run("set display.mode off", "Error: display settings save failed");
|
|
assert(loadDisplayPowerSettings(&fs, true));
|
|
run("get display.mode", "> automatic");
|
|
fs.fail_publish = false;
|
|
fs.files["/display_prefs.bak"] = saved;
|
|
fs.files["/display_prefs"][4] ^= 0x80;
|
|
assert(loadDisplayPowerSettings(&fs, true));
|
|
run("get display.timeout", "> 27");
|
|
assert(loadDisplayPowerSettings(&fs, false));
|
|
run("set display.mode pairing", "Error: BLE pairing display unsupported");
|
|
run("set display.usb.mode button-pairing", "Error: BLE pairing display unsupported");
|
|
FakeFS legacy;
|
|
assert(!loadDisplayPowerSettings(&legacy, false));
|
|
migrateLegacyDisplayTimeout(0);
|
|
assert(loadDisplayPowerSettings(&legacy, false));
|
|
run("get display.mode", "> on");
|
|
run("get display.usb.mode", "> on");
|
|
migrateLegacyDisplayTimeout(42);
|
|
assert(loadDisplayPowerSettings(&legacy, false));
|
|
run("get display.mode", "> automatic");
|
|
run("get display.timeout", "> 42");
|
|
run("get display.usb.timeout", "> 42");
|
|
}
|
|
int main() {
|
|
policyTests(); companionBootTests(); companionSettingsTests(); commandTests();
|
|
}
|
|
'''
|
|
|
|
|
|
NRF_ATOMIC_TESTS = r'''
|
|
// Independent persisted bytes preserve the existing v2 disk layout.
|
|
std::vector<uint8_t> powerRecord(uint16_t timeout) {
|
|
std::vector<uint8_t> data = {'D', 'P', 2, 4, uint8_t(timeout),
|
|
uint8_t(timeout >> 8), 4, 15, 0, 0, 0, 0};
|
|
for (unsigned i = 0; i < 11; ++i) data[11] ^= data[i];
|
|
return data;
|
|
}
|
|
void atomicPersistenceTests() {
|
|
const auto previous = powerRecord(15), candidate = powerRecord(27);
|
|
for (unsigned failure = 0; failure < 9; ++failure) {
|
|
FakeFS fs;
|
|
fs.files["/display_prefs"] = previous;
|
|
fs.files["/display_prefs.bak"] = previous;
|
|
assert(loadDisplayPowerSettings(&fs, true));
|
|
switch (failure) {
|
|
case 0: fs.fail_write = true; break;
|
|
case 1: fs.corrupt_write = true; break;
|
|
case 2: fs.fail_temp_read = true; break;
|
|
case 3: fs.fail_open_write = true; break;
|
|
case 4: fs.fail_publish = true; break;
|
|
case 5: fs.false_publish = true; break;
|
|
case 6: fs.fail_public_read = true; break;
|
|
case 7: fs.corrupt_public = true; break;
|
|
case 8: fs.replace_after_validation = true; break;
|
|
}
|
|
char reply[160];
|
|
assert(handleDisplayPowerCommand("set display.timeout 27", reply, sizeof(reply)));
|
|
assert(std::string(reply) == "Error: display settings save failed");
|
|
assert(displayPowerPrefs().battery.seconds == 15);
|
|
assert(fs.files.at("/display_prefs.bak") == previous);
|
|
assert(!fs.files.count("/display_prefs.tmp"));
|
|
if (failure < 6) assert(fs.files.at("/display_prefs") == previous);
|
|
if (failure == 6) assert(fs.files.at("/display_prefs") == candidate);
|
|
// A post-publication failure is not proof that the old record survived.
|
|
if (failure >= 7) assert(fs.files.at("/display_prefs") != previous);
|
|
}
|
|
for (bool late_error : {false, true}) {
|
|
FakeFS fs;
|
|
fs.files["/display_prefs"] = previous;
|
|
fs.files["/display_prefs.bak"] = previous;
|
|
fs.files["/display_prefs.tmp"] = {'s', 't', 'a', 'l', 'e'};
|
|
assert(loadDisplayPowerSettings(&fs, true));
|
|
fs.late_publish_error = late_error;
|
|
char reply[160];
|
|
assert(handleDisplayPowerCommand("set display.timeout 27", reply, sizeof(reply)));
|
|
assert(std::string(reply) == "OK");
|
|
assert(fs.files.at("/display_prefs") == candidate);
|
|
assert(!fs.files.count("/display_prefs.bak"));
|
|
assert(!fs.files.count("/display_prefs.tmp"));
|
|
assert(displayPowerPrefs().battery.seconds == 27);
|
|
assert(loadDisplayPowerSettings(&fs, true));
|
|
assert(displayPowerPrefs().battery.seconds == 27);
|
|
}
|
|
}
|
|
'''
|
|
|
|
|
|
HEADLESS_TEST = r'''
|
|
#include <cassert>
|
|
#include <helpers/ui/DisplayPowerSettings.h>
|
|
using namespace mesh::ui;
|
|
int main() {
|
|
FakeFS fs;
|
|
fs.files["/display_prefs"] = {'D', 'P', 2, 5, 42, 0, 1, 57, 0, 2, 0, 0};
|
|
fs.files["/display_prefs.bak"] = {'b', 'a', 'c', 'k', 'u', 'p'};
|
|
fs.files["/display_prefs.tmp"] = {'t', 'e', 'm', 'p'};
|
|
const auto original = fs.files;
|
|
for (bool pairing : {false, true}) for (bool wake : {false, true}) {
|
|
displayPowerPrefs().battery = {DisplayMode::Off, 123};
|
|
displayPowerPrefs().usb = {DisplayMode::On, 456};
|
|
displayPowerPrefs().inbox = DisplayInboxMode::Unread;
|
|
assert(!loadDisplayPowerSettings(&fs, pairing, wake));
|
|
const auto& prefs = displayPowerPrefs();
|
|
const auto mode = pairing ? DisplayMode::ButtonPairing : DisplayMode::Button;
|
|
assert(prefs.battery.mode == mode && prefs.usb.mode == mode);
|
|
assert(prefs.battery.seconds == 15 && prefs.usb.seconds == 15);
|
|
assert(prefs.inbox == DisplayInboxMode::History && prefs.wake_on_boot == wake);
|
|
assert(displayPairingSupported() == pairing);
|
|
for (const char* key : {"display.mode", "display.timeout", "display.usb.mode",
|
|
"display.usb.timeout", "display.inbox"}) {
|
|
for (const char* prefix : {"get ", "set "}) for (const char* suffix : {"", " off", " typo"}) {
|
|
char reply[160] = {};
|
|
const std::string text = std::string(prefix) + key + suffix;
|
|
assert(handleDisplayPowerCommand(text.c_str(), reply, sizeof(reply), true));
|
|
assert(std::string(reply) == "Error: display unsupported");
|
|
}
|
|
}
|
|
char reply[32] = "untouched";
|
|
assert(!handleDisplayPowerCommand("get display.model", reply, sizeof(reply)));
|
|
assert(std::string(reply) == "untouched");
|
|
char bounded[3] = {'x', 'y', 'z'};
|
|
assert(handleDisplayPowerCommand("get display.mode", bounded, 1));
|
|
assert(bounded[0] == 0 && bounded[1] == 'y' && bounded[2] == 'z');
|
|
migrateLegacyDisplayTimeout(0);
|
|
assert(displayPowerPrefs().battery.mode == DisplayMode::On);
|
|
assert(displayPowerPrefs().usb.mode == DisplayMode::On);
|
|
migrateLegacyDisplayTimeout(42);
|
|
assert(displayPowerPrefs().battery.mode == DisplayMode::Automatic);
|
|
assert(displayPowerPrefs().usb.mode == DisplayMode::Automatic);
|
|
assert(displayPowerPrefs().battery.seconds == 42 && displayPowerPrefs().usb.seconds == 42);
|
|
assert(displayPowerPrefs().wake_on_boot == wake && displayPairingSupported() == pairing);
|
|
migrateLegacyDisplayTimeout(3601);
|
|
assert(displayPowerPrefs().battery.seconds == 42 && displayPowerPrefs().usb.seconds == 42);
|
|
assert(fs.calls == 0 && fs.files == original);
|
|
assert(!loadDisplayPowerSettings(nullptr, pairing, wake));
|
|
assert(displayPowerPrefs().battery.mode == mode && displayPowerPrefs().wake_on_boot == wake);
|
|
assert(fs.calls == 0 && fs.files == original);
|
|
}
|
|
}
|
|
'''
|
|
|
|
|
|
class DisplayPowerTest(unittest.TestCase):
|
|
def test_companion_boot_wake_is_transport_and_layout_independent(self):
|
|
mesh = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
|
self.assertRegex(mesh, re.compile(
|
|
r"loadDisplayPowerSettings\(_store->getPrimaryFS\(\),\s*"
|
|
r"#ifdef BLE_PIN_CODE\s*true,\s*#else\s*false,\s*#endif\s*true\b"))
|
|
main = (ROOT / "examples/companion_radio/main.cpp").read_text()
|
|
setup = main[main.index("void setup()"):main.index("\nvoid loop()")]
|
|
self.assertLess(setup.index("the_mesh.begin("), setup.index("ui_task.begin("))
|
|
for layout in ("ui-new", "ui-orig", "ui-tiny"):
|
|
with self.subTest(layout=layout):
|
|
ui = (ROOT / "examples/companion_radio" / layout / "UITask.cpp").read_text()
|
|
start = ui.index("void UITask::begin(")
|
|
end = ui.index("\n}", start)
|
|
self.assertIn("_display->servicePower(", ui[start:end])
|
|
infrastructure = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
|
|
self.assertIn("loadDisplayPowerSettings(fs, false);", infrastructure)
|
|
|
|
def test_headless_defaults_commands_and_legacy_migration_do_not_touch_files(self):
|
|
compiler = shutil.which("g++") or shutil.which("clang++")
|
|
self.assertIsNotNone(compiler, "host C++ compiler required")
|
|
with tempfile.TemporaryDirectory(prefix="mesh-headless-display-test-") as name:
|
|
work = Path(name)
|
|
(work / "helpers").mkdir()
|
|
(work / "helpers/IdentityStore.h").write_text(FS_STUB)
|
|
(work / "test.cpp").write_text(HEADLESS_TEST)
|
|
for label, definitions in (("absent", ()), ("null", ("-DDISPLAY_CLASS=NullDisplayDriver",))):
|
|
with self.subTest(display=label):
|
|
executable = work / (label + ".exe")
|
|
result = subprocess.run([
|
|
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
|
*definitions, "-I" + str(work), "-I" + str(ROOT / "src"),
|
|
str(work / "test.cpp"), str(ROOT / "src/helpers/ui/DisplayPowerSettings.cpp"),
|
|
"-o", str(executable),
|
|
], capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
result = subprocess.run([str(executable)], capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
|
|
def test_policy_and_persistent_commands(self):
|
|
compiler = shutil.which("g++") or shutil.which("clang++")
|
|
self.assertIsNotNone(compiler, "host C++ compiler required")
|
|
with tempfile.TemporaryDirectory(prefix="mesh-display-test-") as name:
|
|
work = Path(name)
|
|
(work / "helpers").mkdir()
|
|
(work / "helpers/IdentityStore.h").write_text(FS_STUB)
|
|
(work / "test.cpp").write_text(TEST)
|
|
executable = work / "display-test.exe"
|
|
result = subprocess.run([
|
|
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
|
"-DDISPLAY_CLASS=SSD1306Display", "-I" + str(work),
|
|
"-I" + str(ROOT / "src"), str(work / "test.cpp"),
|
|
str(ROOT / "src/helpers/ui/DisplayPowerSettings.cpp"),
|
|
"-o", str(executable),
|
|
], capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
result = subprocess.run([str(executable)], capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
|
|
def test_nrf52_atomic_publication_failures_and_legacy_format(self):
|
|
compiler = shutil.which("g++") or shutil.which("clang++")
|
|
self.assertIsNotNone(compiler, "host C++ compiler required")
|
|
with tempfile.TemporaryDirectory(prefix="mesh-nrf-display-test-") as name:
|
|
work = Path(name)
|
|
(work / "helpers").mkdir()
|
|
(work / "helpers/IdentityStore.h").write_text(FS_STUB, encoding="ascii")
|
|
code = TEST.replace("int main() {", NRF_ATOMIC_TESTS + "\nint main() {\n atomicPersistenceTests();")
|
|
(work / "test.cpp").write_text(code, encoding="ascii")
|
|
executable = work / "nrf-display-test.exe"
|
|
result = subprocess.run([
|
|
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
|
"-DNRF52_PLATFORM", "-DDISPLAY_CLASS=SSD1306Display",
|
|
"-I" + str(work), "-I" + str(ROOT / "src"),
|
|
str(work / "test.cpp"), str(ROOT / "src/helpers/ui/DisplayPowerSettings.cpp"),
|
|
"-o", str(executable),
|
|
], capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
result = subprocess.run([str(executable)], capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|