652 lines
29 KiB
Python
652 lines
29 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);
|
|
}
|
|
}
|
|
'''
|
|
|
|
|
|
EINK_TEST = r'''
|
|
#include <cassert>
|
|
#include <helpers/ui/DisplayPowerSettings.h>
|
|
using namespace mesh::ui;
|
|
int main() {
|
|
for (bool ble : {false, true}) for (bool companion : {false, true}) {
|
|
FakeFS fs;
|
|
assert(!loadDisplayPowerSettings(&fs, ble, companion));
|
|
const auto& prefs = displayPowerPrefs();
|
|
assert(prefs.battery.seconds == 900 && prefs.usb.seconds == 900);
|
|
for (bool usb : {false, true}) {
|
|
DisplayPowerPolicy policy;
|
|
const uint32_t start = UINT32_MAX - 1000;
|
|
policy.update(prefs, usb, false, false, start);
|
|
assert(policy.on() == companion);
|
|
assert(policy.wake(DisplayWake::Button, start));
|
|
// Redraw/service calls and an app connection do not count as input.
|
|
policy.update(prefs, usb, true, false, start + 899999);
|
|
assert(policy.on());
|
|
policy.update(prefs, usb, true, false, start + 900000);
|
|
assert(!policy.on());
|
|
assert(policy.wake(DisplayWake::Button, start + 900001));
|
|
assert(policy.on());
|
|
}
|
|
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");
|
|
}
|
|
const auto saved = fs.files.at("/display_prefs");
|
|
assert(saved.size() == 12 && saved[2] == 2);
|
|
assert(loadDisplayPowerSettings(&fs, ble, companion));
|
|
assert(prefs.battery.seconds == 27 && prefs.usb.seconds == 123);
|
|
assert(fs.files.at("/display_prefs") == saved);
|
|
for (bool usb : {false, true}) {
|
|
DisplayPowerPolicy policy;
|
|
policy.update(prefs, usb, false, false, 0);
|
|
policy.wake(DisplayWake::Button, 0);
|
|
const uint32_t timeout = usb ? 123000 : 27000;
|
|
policy.update(prefs, usb, false, false, timeout - 1);
|
|
assert(policy.on());
|
|
policy.update(prefs, usb, false, false, timeout);
|
|
assert(!policy.on());
|
|
}
|
|
// A legacy explicit always-on setting must not become a timed profile.
|
|
migrateLegacyDisplayTimeout(0);
|
|
assert(loadDisplayPowerSettings(&fs, ble, companion));
|
|
for (bool usb : {false, true}) {
|
|
DisplayPowerPolicy policy;
|
|
policy.update(prefs, usb, false, false, 0);
|
|
policy.update(prefs, usb, false, false, 3600000);
|
|
assert(policy.on());
|
|
}
|
|
}
|
|
}
|
|
'''
|
|
|
|
|
|
class DisplayPowerTest(unittest.TestCase):
|
|
def test_all_eink_drivers_default_to_fifteen_minute_idle_without_overriding_saved_profiles(self):
|
|
compiler = shutil.which("g++") or shutil.which("clang++")
|
|
self.assertIsNotNone(compiler, "host C++ compiler required")
|
|
with tempfile.TemporaryDirectory(prefix="mesh-eink-timeout-test-") as name:
|
|
work = Path(name)
|
|
(work / "helpers").mkdir()
|
|
(work / "helpers/IdentityStore.h").write_text(FS_STUB)
|
|
(work / "test.cpp").write_text(EINK_TEST)
|
|
for driver in ("GxEPDDisplay", "E213Display", "E290Display"):
|
|
with self.subTest(driver=driver):
|
|
executable = work / (driver + ".exe")
|
|
result = subprocess.run([
|
|
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
|
"-DDISPLAY_CLASS=" + driver, "-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_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()
|