Files
MeshCore/test/test_nrf52_shutdown_leds.py
T

144 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""Run real nRF52 shutdown paths with queued Bluefruit LED callbacks on the host.
No radio/current measurements are implied. The Bluefruit mock models the SDK's
NULL-safe autoConnLed(false), deferred timer stop, and unguarded blinky callback.
"""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
BOARD = ROOT / "src/helpers/NRF52Board.cpp"
FIXTURE = ROOT / "test/fixtures/nrf52_shutdown_leds.cpp"
ARDUINO = r'''
#pragma once
#include <cstdint>
#include <vector>
#include <string>
#include <cassert>
#define LOW 0
#define HIGH 1
#define OUTPUT 1
#define INPUT 0
#define PINS_COUNT 64
struct Event { std::string name; int pin; int value; };
extern std::vector<Event> events;
extern int levels[PINS_COUNT];
inline int digitalPinToPinName(int pin) { return pin == 62 ? 0xff : pin; }
inline void digitalWrite(int pin, int value) {
assert(pin >= 0 && pin < PINS_COUNT && pin != 62);
levels[pin] = value; events.push_back({"write", pin, value});
}
inline void pinMode(int pin, int mode) {
assert(pin >= 0 && pin < PINS_COUNT && pin != 62);
events.push_back({"mode", pin, mode});
}
'''
def compile_source(testcase, source, defines=(), expect_success=True):
compiler = shutil.which("c++") or shutil.which("g++")
testcase.assertIsNotNone(compiler, "run this host-only suite where c++ is available")
with tempfile.TemporaryDirectory(prefix="nrf52-shutdown-leds-") as folder:
work = Path(folder)
(work / "Arduino.h").write_text(ARDUINO)
(work / "case.cpp").write_text(source)
binary = work / ("case.exe" if os.name == "nt" else "case")
command = [compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror",
"-I", str(work), "-I", str(ROOT / "src"),
*["-D" + flag for flag in defines], str(work / "case.cpp"),
"-o", str(binary)]
if os.name != "nt":
command[1:1] = ["-fsanitize=address,undefined", "-fno-omit-frame-pointer",
"-fno-pie", "-no-pie"]
built = subprocess.run(command, text=True, capture_output=True)
testcase.assertEqual(built.returncode, 0, built.stderr)
ran = subprocess.run([str(binary)], text=True, capture_output=True)
if expect_success:
testcase.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
else:
testcase.assertNotEqual(ran.returncode, 0, "negative control did not fail")
def lifecycle_source(mutation=None):
source = BOARD.read_text()
if mutation == "automatic-owner-left-running":
source = source.replace("Bluefruit.autoConnLed(false);", "// negative control")
elif mutation == "final-latch-left-on":
source = source.replace("mesh_nrf52::turnOffShutdownLeds();", "// negative control")
methods = "\n".join(extract_braced(source, signature) for signature in (
"void NRF52Board::shutdownPeripherals()",
"void NRF52Board::powerOff()",
"void NRF52Board::enterDeepSleep(uint32_t secs)",
"void NRF52Board::enterSystemOff(uint8_t reason)",
))
x1 = (ROOT / "variants/meshtracker_x1/MeshTrackerX1Board.h").read_text()
hook = extract_braced(x1, "void shutdownPeripherals() override")
hook = hook.replace("void shutdownPeripherals() override",
"void MeshTrackerX1Board::shutdownPeripherals()", 1)
return FIXTURE.read_text().replace("@METHODS@", methods).replace("@X1_HOOK@", hook)
class Nrf52ShutdownLedsTests(unittest.TestCase):
def test_real_shutdown_paths_stop_led_owner_then_clear_latches(self):
compile_source(self, lifecycle_source())
def test_automatic_led_owner_negative_control(self):
compile_source(self, lifecycle_source("automatic-owner-left-running"),
expect_success=False)
def test_final_latch_negative_control(self):
compile_source(self, lifecycle_source("final-latch-left-on"),
expect_success=False)
def test_aliases_polarity_and_absent_led_sentinels(self):
source = r'''
#include <Arduino.h>
#include <helpers/nrf52/ShutdownLeds.h>
std::vector<Event> events;
int levels[PINS_COUNT];
int main() {
for (int& level : levels) level = LED_STATE_ON;
mesh_nrf52::turnOffShutdownLeds();
for (const Event& e : events) {
assert(e.pin == EXPECT_PIN);
if (e.name == "write") assert(e.value == !LED_STATE_ON);
}
assert(events.size() == EXPECT_EVENTS);
for (size_t i = 0; i < events.size(); i += 2) {
assert(events[i].name == "write" && events[i + 1].name == "mode");
}
}
'''
cases = (
# Preferred LED_BLUE avoids expanding an obsolete broken alias.
("LED_STATE_ON=1", "LED_BLUE=36", "LED_CONN=UNUSED_PIN_NAME",
"LED_BUILTIN=36", "LED_PIN=36", "LED_RED=36", "LED_GREEN=36",
"EXPECT_PIN=36", "EXPECT_EVENTS=10"),
("LED_STATE_ON=0", "LED_BLUE=-1", "LED_BUILTIN=35", "LED_PIN=255",
"LED_RED=PINS_COUNT", "LED_GREEN=62", "EXPECT_PIN=35", "EXPECT_EVENTS=2"),
("LED_STATE_ON=0", "LED_CONN=12", "EXPECT_PIN=12", "EXPECT_EVENTS=2"),
("LED_STATE_ON=1", "EXPECT_PIN=-1", "EXPECT_EVENTS=0"),
)
for case in cases:
with self.subTest(defines=case):
compile_source(self, source, case)
def test_meshtracker_inherits_base_poweroff_and_keeps_peripheral_hook(self):
x1 = (ROOT / "variants/meshtracker_x1/MeshTrackerX1Board.h").read_text()
self.assertNotIn("void powerOff(", x1)
self.assertIn("NRF52Board::shutdownPeripherals();", x1)
# Catch an include-only helper that never reaches production shutdown.
self.assertIn('#include "nrf52/ShutdownLeds.h"', BOARD.read_text())
if __name__ == "__main__":
unittest.main()