#!/usr/bin/env python3 """Exercise the Full overlay, real linker reserve, and sensor allocation failure.""" from pathlib import Path import re import subprocess import tempfile import unittest ROOT = Path(__file__).resolve().parents[1] def method(text, signature): start = text.index(signature) end = text.index("{", start) + 1 depth = 1 while depth: depth += (text[end] == "{") - (text[end] == "}") end += 1 return text[start:end] def full_flags(target): result = subprocess.run([ "bash", "-c", r''' source build.sh PIO_ENV_PLATFORM_BY_NAME["$1"]=NRF52_PLATFORM pio_env_option_contains() { return 0; } apply_companion_radio_full_profile "$1" "$1" printf '%s\n%s\n' "$PLATFORMIO_BUILD_FLAGS" "$PLATFORMIO_BUILD_UNFLAGS" ''', "test", target], cwd=ROOT, text=True, capture_output=True, check=True) return result.stdout class T096FullMemoryTest(unittest.TestCase): def test_t096_full_overlay_preserves_features_and_reserves_heap(self): for suffix in ("femon", "femoff"): with self.subTest(suffix=suffix): flags = full_flags("Heltec_t096_companion_radio_full_" + suffix) self.assertIn("-DOFFLINE_QUEUE_SIZE=256", flags.splitlines()[0]) self.assertNotIn("-DOFFLINE_QUEUE_SIZE=128", flags.splitlines()[0]) self.assertIn("-DOTA_SHARED_COMPANION_QUEUE=1", flags) self.assertIn("--defsym=__mesh_nrf52_min_heap_size=73728", flags) for feature in ("ENABLE_USB_INTERFACE", "OTA_SEEDER_ONLY", "COMPANION_FEATURE_BLE_MOTA_SOURCE", "COMPANION_FEATURE_USB_MOTA_SOURCE", "COMPANION_FEATURE_DEDICATED_USB_LOGGING"): self.assertIn("-D" + feature + "=1", flags) self.assertNotIn("-DMAX_CONTACTS", flags) self.assertNotIn("-DMAX_GROUP_CHANNELS", flags) def test_all_nrf52_full_profiles_share_without_changing_legacy(self): for target in ("RAK_4631_companion_radio_full", "WioTrackerL1Eink_companion_radio_full", "Heltec_t114_companion_radio_full", "RAK_3401_companion_radio_full"): with self.subTest(target=target): flags = full_flags(target) self.assertIn("-DOTA_SHARED_COMPANION_QUEUE=1", flags) self.assertIn("-DOFFLINE_QUEUE_SIZE=256", flags.splitlines()[0]) flags = full_flags("Heltec_t096_companion_radio_ble_femon") self.assertNotIn("-DOTA_SHARED_COMPANION_QUEUE", flags) self.assertNotIn("__mesh_nrf52_min_heap_size", flags) def test_real_linker_rejects_release_heap_and_enforces_boundary(self): flags = full_flags("Heltec_t096_companion_radio_full_femon") reserve = int(re.search(r"__mesh_nrf52_min_heap_size=(\d+)", flags)[1]) self.assertIn('INCLUDE "boards/nrf52_heap_reserve.ld"', (ROOT / "boards/nrf52840_s140_v6_extrafs.ld").read_text()) with tempfile.TemporaryDirectory(prefix="meshcore-heap-") as temp: obj = Path(temp) / "empty.o" subprocess.run(["cc", "-x", "c", "-c", "-", "-o", str(obj)], input="int linked_fixture;", text=True, check=True) # Released 26303793 leaves 54,724 bytes between the real heap # symbols. Static RAM fitting alone previously admitted it. for size, minimum, accepted in ( (54724, reserve, False), (reserve - 1, reserve, False), (reserve, reserve, True), (reserve + 16384, reserve, True), (-1, reserve, False), (1024, None, True), ): with self.subTest(size=size, minimum=minimum): command = ["ld", "-r", str(obj), "-o", str(Path(temp) / "linked.o"), "-T", str(ROOT / "boards/nrf52_heap_reserve.ld"), "--defsym=__HeapBase=0x20006008", f"--defsym=__HeapLimit={0x20006008 + size}"] if minimum is not None: command += [f"--defsym=__mesh_nrf52_min_heap_size={minimum}"] result = subprocess.run(command, text=True, capture_output=True) self.assertEqual(result.returncode == 0, accepted, result.stderr) if not accepted: self.assertIn("nRF52", result.stderr) def test_sensor_page_survives_failed_cayenne_allocation(self): refresh = method((ROOT / "examples/companion_radio/ui-new/UITask.cpp").read_text(), "void refresh_sensors()") harness = r''' #include #include #include #define AUTO_OFF_MILLIS 15000 #define UI_RECENT_LIST_SIZE 3 #define TELEM_CHANNEL_SELF 0 unsigned long millis() { return 100; } struct Lpp { uint8_t storage[8]; bool allocated = false; uint8_t size = 0; uint8_t* getBuffer() { return allocated ? storage : nullptr; } void reset() { size = 0; } uint8_t getSize() { return size; } void addVoltage(int, float) { if (!getBuffer()) throw std::runtime_error("sensor menu NULL write"); size = 4; } }; struct LPPReader { bool pending; LPPReader(uint8_t* buf, int size) : pending(buf && size) {} bool readHeader(uint8_t&, uint8_t&) { bool p = pending; pending = false; return p; } void skipData(uint8_t) {} }; struct Board { int getBattMilliVolts() { return 4000; } } board; struct Sensors { int calls = 0; void querySensors(int, Lpp&) { ++calls; } } sensors; struct Home { Lpp sensors_lpp; int sensors_nb = 5, sensors_scroll_offset = 4, next_sensors_refresh = 0; bool sensors_scroll = true; @REFRESH@ }; int main() { Home home; home.refresh_sensors(); assert(home.sensors_nb == 0 && !home.sensors_scroll); assert(home.sensors_scroll_offset == 0 && sensors.calls == 0); home.sensors_lpp.allocated = true; home.refresh_sensors(); assert(home.sensors_nb == 1 && sensors.calls == 1); assert(home.next_sensors_refresh == 5100); home.refresh_sensors(); assert(sensors.calls == 1); } '''.replace("@REFRESH@", refresh) with tempfile.TemporaryDirectory(prefix="meshcore-sensor-oom-") as temp: binary = Path(temp) / "menu" built = subprocess.run(["c++", "-std=c++17", "-x", "c++", "-", "-o", str(binary)], input=harness, text=True, capture_output=True) self.assertEqual(built.returncode, 0, built.stderr) result = subprocess.run([str(binary)], text=True, capture_output=True) self.assertEqual(result.returncode, 0, result.stderr) def test_shared_queue_preserves_fifo_across_wrap_and_workspace_reuse(self): harness = r''' #include #include #include #include #include struct Frame { uint8_t len; uint8_t buf[176]; }; struct Workspace { static int alive; uint32_t words[4906]; // matches the roughly 19 KiB embedded mOTA context Workspace() { ++alive; memset(words, 0xA5, sizeof words); } ~Workspace() { --alive; memset(words, 0x5A, sizeof words); } }; int Workspace::alive = 0; using Buffer = mesh::BorrowableFrameBuffer; int main() { Buffer buffer; int head = 0, count = 0; unsigned sequence = 0; std::deque expected; auto add = [&]() { assert(count < int(buffer.capacity())); Frame& frame = buffer.at((head + count) % buffer.capacity()); frame.len = 176; memset(frame.buf, sequence % 251, sizeof frame.buf); memcpy(frame.buf, &sequence, sizeof sequence); expected.push_back(sequence++); ++count; }; auto pop = [&]() { assert(count > 0); const Frame& frame = buffer.at(head); unsigned id; memcpy(&id, frame.buf, sizeof id); assert(frame.len == 176 && id == expected.front()); for (unsigned i = sizeof id; i < sizeof frame.buf; ++i) assert(frame.buf[i] == id % 251); expected.pop_front(); --count; head = count ? (head + 1) % buffer.capacity() : 0; }; for (int cycle = 0; cycle < 300; ++cycle) { while (count < 256) add(); // A full queue refuses the loan without dropping or reordering messages. int previous_head = head; assert(!buffer.acquire(count, head)); assert(count == 256 && head == previous_head && Workspace::alive == 0); while (count > 129) pop(); assert(!buffer.acquire(count, head)); pop(); // Exercise every possible wrapped head at the exact 128-frame boundary. for (int i = 0; i < cycle % 256; ++i) { pop(); add(); } Workspace* workspace = buffer.acquire(count, head); assert(workspace && head == 0 && buffer.capacity() == 128); assert(Workspace::alive == 1 && buffer.acquire(count, head) == workspace); for (auto& word : workspace->words) word = 0xC0DEC0DE; for (int i = 0; i < cycle % 128; ++i) { pop(); add(); } buffer.release(head); assert(head == 0 && buffer.capacity() == 256 && Workspace::alive == 0); buffer.release(head); // repeated stop is harmless while (count < 256) add(); while (count) pop(); } assert(expected.empty()); assert(buffer.acquire(count, head)); // empty queue / destructor while borrowed } ''' with tempfile.TemporaryDirectory(prefix="meshcore-shared-queue-") as temp: binary = Path(temp) / "shared_queue" built = subprocess.run([ "c++", "-std=c++11", "-fsanitize=address,undefined", "-g", "-I", str(ROOT / "src"), "-x", "c++", "-", "-o", str(binary), ], input=harness, text=True, capture_output=True) self.assertEqual(built.returncode, 0, built.stderr) result = subprocess.run([str(binary)], text=True, capture_output=True) self.assertEqual(result.returncode, 0, result.stderr) if __name__ == "__main__": unittest.main()