447 lines
20 KiB
Python
447 lines
20 KiB
Python
"""Run actual emergency RF/wireless ownership and restoration with fake hardware."""
|
|
from pathlib import Path
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_replay_reset_integration import extract_braced
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
HARNESS = r'''
|
|
#include <helpers/EmergencyPowerRuntime.h>
|
|
#include <cassert>
|
|
#include <cstdlib>
|
|
#include <initializer_list>
|
|
#include <string>
|
|
#include <vector>
|
|
using namespace mesh::wireless;
|
|
namespace power = mesh::power;
|
|
static std::vector<std::string> events;
|
|
void delay(uint32_t milliseconds) {
|
|
events.push_back("backoff:" + std::to_string(milliseconds));
|
|
}
|
|
static std::string serviceName(uint8_t bit) {
|
|
return bit == WiFi ? "wifi" : bit == EspNow ? "espnow" : "ble";
|
|
}
|
|
struct Wireless : Backend {
|
|
uint8_t active = All, supported = All, sessions = Independent;
|
|
uint8_t pendingOn = 0, pendingOff = 0, failedOn = 0, failedOff = 0;
|
|
uint8_t available() const override { return supported; }
|
|
uint8_t enabled() const override { return active; }
|
|
uint8_t clients() const override { return sessions; }
|
|
Result set(uint8_t bit, bool on) override {
|
|
events.push_back(serviceName(bit) + (on ? ":on" : ":off"));
|
|
if ((on ? pendingOn : pendingOff) & bit) return Result::Pending;
|
|
if ((on ? failedOn : failedOff) & bit) return Result::Failed;
|
|
if (on) active |= bit; else active &= ~bit;
|
|
return Result::Done;
|
|
}
|
|
};
|
|
struct Mesh {
|
|
bool available = true, inFlight = false;
|
|
unsigned queuedFuturePackets = 20;
|
|
bool hasOutbound() const { return inFlight; }
|
|
protected:
|
|
@RUNTIME_FRIEND@
|
|
bool isRadioAvailable() const { return available; }
|
|
void setRadioAvailable(bool value) {
|
|
events.push_back(value ? "mesh:on" : "mesh:off"); available = value;
|
|
}
|
|
};
|
|
struct Radio {
|
|
bool morseTx = false, morseRx = false, failSleep = false, failWake = false;
|
|
bool recoveryOk = true;
|
|
bool isMorseTxActive() const { return morseTx; }
|
|
bool isMorseRxActive() const { return morseRx; }
|
|
bool setTrackerSleep(bool sleeping) {
|
|
events.push_back(sleeping ? "radio:sleep" : "radio:wake");
|
|
return !(sleeping ? failSleep : failWake);
|
|
}
|
|
bool recoverRadio(bool hard) {
|
|
assert(hard); events.push_back("radio:recover"); return recoveryOk;
|
|
}
|
|
};
|
|
struct Board {
|
|
bool supported = true, ota = false, radioTest = false, sleepOk = true;
|
|
unsigned sleeps = 0;
|
|
uint32_t lastBudget = 0;
|
|
bool supportsEmergencySleep() const { return supported; }
|
|
bool isOTAUpdateRunning() const { return ota; }
|
|
bool isRadioTestActive() const { return radioTest; }
|
|
virtual void setEmergencyRadioSleeping(bool) {}
|
|
bool emergencySleepMilliseconds(uint32_t budget) {
|
|
events.push_back("cpu:" + std::to_string(budget));
|
|
++sleeps; lastBudget = budget; return sleepOk;
|
|
}
|
|
};
|
|
static constexpr int HIGH = 1, LOW = 0;
|
|
static constexpr unsigned SX126X_POWER_EN = @FEM_PIN@, PIN_3V3_EN = @RAIL_PIN@;
|
|
static int levels[64] = {};
|
|
static int digitalRead(unsigned pin) {
|
|
assert(pin == SX126X_POWER_EN);
|
|
events.push_back("fem:read");
|
|
return levels[pin];
|
|
}
|
|
static void digitalWrite(unsigned pin, int value) {
|
|
assert(pin == SX126X_POWER_EN); // Shared IO2/storage rail must never change.
|
|
levels[pin] = value;
|
|
events.push_back(value ? "fem:on" : "fem:off");
|
|
}
|
|
struct RAK3401Board : Board {
|
|
@RAK_FIELDS@
|
|
void setEmergencyRadioSleeping(bool sleeping) override;
|
|
};
|
|
@RAK_FEM_METHOD@
|
|
struct CLI {
|
|
uint32_t budget = 2700000;
|
|
bool radioSleeping = false;
|
|
uint32_t emergencySleepMilliseconds() const { return budget; }
|
|
void setEmergencyRadioSleeping(bool value) {
|
|
events.push_back(value ? "status:sleep" : "status:awake"); radioSleeping = value;
|
|
}
|
|
};
|
|
struct Fixture {
|
|
Wireless wireless;
|
|
Mesh mesh;
|
|
Radio radio;
|
|
Board board;
|
|
CLI cli;
|
|
mesh::power::EmergencyPowerRuntime runtime;
|
|
Fixture() { control().begin(wireless); }
|
|
bool service(bool entryBusy = false, bool workRequired = false) {
|
|
return runtime.service(mesh, radio, board, cli, entryBusy, workRequired);
|
|
}
|
|
};
|
|
static void command(Control& c, const char* text, uint32_t now = 100) {
|
|
char reply[160] = {};
|
|
assert(c.handle(text, reply, sizeof(reply), now, Independent));
|
|
assert(strncmp(reply, "OK", 2) == 0);
|
|
c.service(now + 250);
|
|
assert(!c.pending());
|
|
}
|
|
static void refused(Control& c, const char* text) {
|
|
char reply[160] = {};
|
|
assert(c.handle(text, reply, sizeof(reply), 1000, Independent));
|
|
assert(strstr(reply, "emergency sleep") && !c.pending());
|
|
}
|
|
int main(int argc, char** argv) {
|
|
assert(argc == 2);
|
|
const int scenario = std::atoi(argv[1]);
|
|
if (scenario == 0) {
|
|
Fixture f;
|
|
assert(f.service());
|
|
assert((events == std::vector<std::string>{"wifi:off", "espnow:off", "ble:off",
|
|
"radio:sleep", "mesh:off", "status:sleep", "cpu:10000"}));
|
|
assert(!f.mesh.available && f.cli.radioSleeping && f.wireless.active == 0);
|
|
events.clear(); f.cli.budget = 417;
|
|
assert(f.service(true)); // Reply-drain/partial-input guard does not steal already-suspended RF.
|
|
assert((events == std::vector<std::string>{"cpu:417"}));
|
|
events.clear(); f.cli.budget = 0;
|
|
assert(!f.service());
|
|
assert((events == std::vector<std::string>{"radio:wake", "mesh:on", "status:awake",
|
|
"espnow:on", "wifi:on", "ble:on"}));
|
|
assert(f.mesh.available && !f.cli.radioSleeping && f.wireless.active == All);
|
|
events.clear(); assert(!f.service() && events.empty());
|
|
} else if (scenario == 1) {
|
|
Fixture f;
|
|
f.board.supported = false; assert(!f.service());
|
|
f.board.supported = true;
|
|
assert(!f.service(true));
|
|
assert(!f.service(false, true));
|
|
f.board.ota = true; assert(!f.service()); f.board.ota = false;
|
|
f.board.radioTest = true; assert(!f.service()); f.board.radioTest = false;
|
|
f.mesh.inFlight = true; assert(!f.service()); f.mesh.inFlight = false;
|
|
f.radio.morseTx = true; assert(!f.service()); f.radio.morseTx = false;
|
|
f.radio.morseRx = true; assert(!f.service()); f.radio.morseRx = false;
|
|
assert(events.empty() && f.mesh.available && !f.cli.radioSleeping && !f.board.sleeps);
|
|
// Queued future packets are intentionally not consulted: normal mesh work
|
|
// stays suspended, so an off-hour backlog cannot prevent battery protection.
|
|
assert(f.mesh.queuedFuturePackets == 20 && f.service());
|
|
assert(f.board.sleeps == 1 && !f.mesh.available);
|
|
} else if (scenario == 2) {
|
|
Fixture f;
|
|
f.radio.failSleep = true;
|
|
assert(!f.service() && f.mesh.available && !f.cli.radioSleeping && !f.board.sleeps);
|
|
assert(f.wireless.active == 0);
|
|
f.radio.failSleep = false; events.clear();
|
|
assert(f.service() && !f.mesh.available && f.board.sleeps == 1);
|
|
f.radio.failWake = true; f.radio.recoveryOk = false;
|
|
f.cli.budget = 0; events.clear();
|
|
assert(!f.service());
|
|
assert((events == std::vector<std::string>{"radio:wake", "radio:recover", "mesh:on",
|
|
"status:awake", "espnow:on", "wifi:on", "ble:on"}));
|
|
// Return ownership to Dispatcher even after hardware recovery reports an
|
|
// error, so its ordinary watchdog is not permanently disabled.
|
|
assert(f.mesh.available && !f.cli.radioSleeping);
|
|
} else if (scenario == 3) {
|
|
Fixture f;
|
|
for (unsigned cause = 0; cause != 5; ++cause) {
|
|
assert(f.service() && !f.mesh.available);
|
|
const unsigned sleeps = f.board.sleeps;
|
|
f.board.ota = cause == 0;
|
|
f.board.radioTest = cause == 1;
|
|
f.radio.morseTx = cause == 2;
|
|
f.radio.morseRx = cause == 3;
|
|
const bool work = cause == 4;
|
|
assert(!f.service(false, work));
|
|
assert(f.mesh.available && !f.cli.radioSleeping && f.wireless.active == All);
|
|
assert(f.board.sleeps == sleeps);
|
|
assert(!f.service(false, work));
|
|
assert(f.board.sleeps == sleeps); // Active work also prevents immediate re-entry.
|
|
f.board.ota = f.board.radioTest = f.radio.morseTx = f.radio.morseRx = false;
|
|
}
|
|
} else if (scenario == 4) {
|
|
Wireless b; Control c; c.begin(b);
|
|
assert(c.suspendForEmergency());
|
|
assert(b.active == 0 && c.blocked(All));
|
|
assert(!c.allowService(Bluetooth));
|
|
assert(!c.allowService(WiFi | EspNow));
|
|
assert(!c.allowWiFiForOTA());
|
|
assert(c.blocked(All) && !c.masterOff());
|
|
for (const char* text : {"set wifi on", "set espnow on", "set 2.4ghz on all",
|
|
"set 2.4ghz off force"}) refused(c, text);
|
|
char reply[160];
|
|
assert(c.handle("get 2.4ghz", reply, sizeof(reply), 1000, Independent));
|
|
assert(strstr(reply, "wifi=off") && strstr(reply, "bluetooth=off"));
|
|
assert(c.resumeAfterEmergency() && b.active == All && !c.blocked(All));
|
|
assert(c.allowService(Bluetooth) && c.allowWiFiForOTA());
|
|
} else if (scenario == 5) {
|
|
for (uint8_t mask = 0; mask <= All; ++mask) {
|
|
Wireless b; b.active = mask;
|
|
Control c; c.begin(b);
|
|
command(c, "set 2.4ghz off"); command(c, "set 2.4ghz on", 500);
|
|
assert(b.active == mask && !c.masterOff());
|
|
for (uint8_t bit : {uint8_t(WiFi), uint8_t(Bluetooth), uint8_t(EspNow)})
|
|
assert(c.blocked(bit) == !(mask & bit));
|
|
assert(c.suspendForEmergency() && b.active == 0 && c.blocked(All));
|
|
assert(c.resumeAfterEmergency() && b.active == mask && !c.masterOff());
|
|
for (uint8_t bit : {uint8_t(WiFi), uint8_t(Bluetooth), uint8_t(EspNow)})
|
|
assert(c.blocked(bit) == !(mask & bit));
|
|
const size_t calls = events.size();
|
|
assert(c.resumeAfterEmergency() && events.size() == calls);
|
|
}
|
|
Wireless b; b.active = WiFi | Bluetooth;
|
|
Control c; c.begin(b);
|
|
command(c, "set 2.4ghz off");
|
|
assert(c.masterOff() && b.active == 0);
|
|
const size_t calls = events.size();
|
|
assert(c.suspendForEmergency() && c.resumeAfterEmergency());
|
|
assert(c.masterOff() && c.blocked(All) && b.active == 0 && events.size() == calls);
|
|
char reply[160];
|
|
assert(c.handle("set wifi on", reply, sizeof(reply), 500, Independent));
|
|
assert(strstr(reply, "2.4ghz is off"));
|
|
command(c, "set 2.4ghz on", 1000);
|
|
assert(b.active == (WiFi | Bluetooth)); // Original ordinary restore snapshot survived.
|
|
} else if (scenario == 6) {
|
|
Fixture f;
|
|
f.wireless.pendingOff = WiFi;
|
|
assert(!f.service() && f.wireless.active == All && !f.board.sleeps && f.mesh.available);
|
|
refused(control(), "set wifi on");
|
|
assert(!control().allowService(WiFi) && !control().allowWiFiForOTA());
|
|
f.wireless.pendingOff = 0;
|
|
f.wireless.failedOff = EspNow;
|
|
assert(!f.service() && f.wireless.active == (EspNow | Bluetooth));
|
|
assert(f.mesh.available && !f.cli.radioSleeping && !f.board.sleeps);
|
|
// The hour/lease can change before RF ever went to sleep. Restore services
|
|
// already stopped by the incomplete suspend, then allow a later retry.
|
|
f.wireless.failedOff = 0; f.cli.budget = 0;
|
|
assert(!f.service() && f.wireless.active == All);
|
|
f.cli.budget = 2700000; assert(f.service());
|
|
f.cli.budget = 0; f.wireless.pendingOn = EspNow;
|
|
assert(!f.service() && f.mesh.available && f.wireless.active == 0);
|
|
refused(control(), "set 2.4ghz on all");
|
|
assert(!control().allowService(Bluetooth));
|
|
f.wireless.pendingOn = 0; f.wireless.failedOn = WiFi;
|
|
assert(!f.service() && f.wireless.active == EspNow);
|
|
refused(control(), "set wifi on");
|
|
f.wireless.failedOn = 0;
|
|
assert(!f.service() && f.wireless.active == All && !control().blocked(All));
|
|
} else if (scenario == 7) {
|
|
Fixture f;
|
|
char reply[160];
|
|
assert(control().handle("set wifi off", reply, sizeof(reply), 100, Independent));
|
|
assert(control().pending());
|
|
assert(!f.service() && events.empty() && f.mesh.available);
|
|
control().service(350);
|
|
assert(f.wireless.active == (Bluetooth | EspNow) && !control().pending());
|
|
assert(f.service() && f.wireless.active == 0);
|
|
f.cli.budget = 0;
|
|
assert(!f.service() && f.wireless.active == (Bluetooth | EspNow));
|
|
assert(control().blocked(WiFi) && !control().blocked(Bluetooth | EspNow));
|
|
} else if (scenario == 8) {
|
|
Fixture f;
|
|
f.mesh.available = false; // Sensor Tracker already owns this radio's sleep.
|
|
assert(f.service() && f.cli.radioSleeping && f.board.sleeps == 1);
|
|
for (const auto& event : events)
|
|
assert(event != "radio:sleep" && event != "mesh:off");
|
|
events.clear(); f.cli.budget = 0;
|
|
assert(!f.service() && !f.mesh.available);
|
|
for (const auto& event : events)
|
|
assert(event != "radio:wake" && event != "radio:recover" && event != "mesh:on");
|
|
assert(f.wireless.active == All);
|
|
} else if (scenario == 9) {
|
|
Fixture f;
|
|
f.board.sleepOk = false;
|
|
assert(f.service() && !f.mesh.available && f.cli.radioSleeping);
|
|
assert(events.back() == "backoff:10");
|
|
events.clear();
|
|
assert(f.service() && !f.mesh.available && f.cli.radioSleeping);
|
|
assert((events == std::vector<std::string>{"cpu:10000", "backoff:10"}));
|
|
// A later successful attempt keeps the existing suspension and skips backoff.
|
|
events.clear(); f.board.sleepOk = true;
|
|
assert(f.service());
|
|
assert((events == std::vector<std::string>{"cpu:10000"}));
|
|
// An awake lease still wakes RF after a failed CPU wait.
|
|
events.clear(); f.cli.budget = 0;
|
|
assert(!f.service() && f.mesh.available && !f.cli.radioSleeping);
|
|
assert(f.wireless.active == All);
|
|
} else if (scenario == 10) {
|
|
for (int starting_level : {LOW, HIGH}) {
|
|
Wireless wireless; control().begin(wireless);
|
|
Mesh mesh; Radio radio; RAK3401Board board; CLI cli;
|
|
mesh::power::EmergencyPowerRuntime runtime;
|
|
levels[SX126X_POWER_EN] = starting_level; levels[PIN_3V3_EN] = HIGH;
|
|
events.clear();
|
|
// Failed warm sleep never captures/disables the FEM.
|
|
radio.failSleep = true;
|
|
assert(!runtime.service(mesh, radio, board, cli, false));
|
|
assert(levels[SX126X_POWER_EN] == starting_level);
|
|
assert(events.back() == "radio:sleep");
|
|
radio.failSleep = false; events.clear();
|
|
assert(runtime.service(mesh, radio, board, cli, false));
|
|
assert((events == std::vector<std::string>{"wifi:off", "espnow:off", "ble:off",
|
|
"radio:sleep", "mesh:off", "fem:read", "fem:off", "status:sleep", "cpu:10000"}));
|
|
assert(levels[SX126X_POWER_EN] == LOW && levels[PIN_3V3_EN] == HIGH);
|
|
events.clear();
|
|
// Maintenance and duplicate suspension must not overwrite the snapshot.
|
|
board.setEmergencyRadioSleeping(true);
|
|
assert(runtime.service(mesh, radio, board, cli, false));
|
|
assert((events == std::vector<std::string>{"cpu:10000"}));
|
|
cli.budget = 0; events.clear();
|
|
radio.failWake = true;
|
|
assert(!runtime.service(mesh, radio, board, cli, false));
|
|
std::vector<std::string> expected = {starting_level ? "fem:on" : "fem:off"};
|
|
if (starting_level) expected.push_back("backoff:1");
|
|
const std::vector<std::string> after = {"radio:wake", "radio:recover", "mesh:on",
|
|
"status:awake", "espnow:on", "wifi:on", "ble:on"};
|
|
expected.insert(expected.end(), after.begin(), after.end());
|
|
assert(events == expected);
|
|
assert(levels[SX126X_POWER_EN] == starting_level && levels[PIN_3V3_EN] == HIGH);
|
|
events.clear(); board.setEmergencyRadioSleeping(false); assert(events.empty());
|
|
}
|
|
Wireless wireless; control().begin(wireless);
|
|
Mesh mesh; mesh.available = false; // Tracker owns RF; its FEM is untouched.
|
|
Radio radio; RAK3401Board board; CLI cli;
|
|
mesh::power::EmergencyPowerRuntime runtime;
|
|
levels[SX126X_POWER_EN] = HIGH; events.clear();
|
|
assert(runtime.service(mesh, radio, board, cli, false));
|
|
cli.budget = 0; assert(!runtime.service(mesh, radio, board, cli, false));
|
|
for (const auto& event : events) assert(event.find("fem:") == std::string::npos);
|
|
assert(levels[SX126X_POWER_EN] == HIGH);
|
|
} else assert(false);
|
|
}
|
|
'''
|
|
|
|
|
|
class EmergencyPowerRuntimeTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.folder = tempfile.TemporaryDirectory(prefix="emergency-runtime-")
|
|
work = Path(cls.folder.name)
|
|
source, cls.binary = work / "main.cpp", work / "check"
|
|
(work / "Arduino.h").write_text(
|
|
"#pragma once\n#include <stdint.h>\nvoid delay(uint32_t);\n", encoding="ascii")
|
|
board_header = (ROOT / "variants/rak3401/RAK3401Board.h").read_text()
|
|
board_source = (ROOT / "variants/rak3401/RAK3401Board.cpp").read_text()
|
|
dispatcher = (ROOT / "src/Dispatcher.h").read_text()
|
|
variant = (ROOT / "variants/rak3401/variant.h").read_text()
|
|
fields = re.search(r"class RAK3401Board[^\n]*\{\n#if MESH_ENABLE_EMERGENCY_POWER\n(.*?)#endif",
|
|
board_header, re.S).group(1)
|
|
code = HARNESS.replace("@RAK_FIELDS@", fields)
|
|
code = code.replace("@RAK_FEM_METHOD@", extract_braced(
|
|
board_source, "void RAK3401Board::setEmergencyRadioSleeping("))
|
|
code = code.replace("@RUNTIME_FRIEND@", re.search(
|
|
r"friend class power::EmergencyPowerRuntime;", dispatcher).group(0))
|
|
for marker, name, body in (("FEM_PIN", "SX126X_POWER_EN", variant),
|
|
("RAIL_PIN", "PIN_3V3_EN", board_header)):
|
|
pin = re.search(rf"#define\s+{name}\s+\(?\s*(\d+)", body).group(1)
|
|
code = code.replace("@" + marker + "@", pin)
|
|
source.write_text(code, encoding="ascii")
|
|
result = subprocess.run([
|
|
os.environ.get("CXX", "g++"), "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-Werror",
|
|
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-omit-frame-pointer",
|
|
"-fno-pie", "-no-pie", "-DMESH_ENABLE_EMERGENCY_POWER=1", "-I", str(work),
|
|
"-I", str(ROOT / "src"),
|
|
str(source), "-o", str(cls.binary)], capture_output=True, text=True, timeout=60)
|
|
if result.returncode:
|
|
cls.folder.cleanup()
|
|
raise AssertionError(result.stdout + result.stderr)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.folder.cleanup()
|
|
|
|
def scenario(self, number):
|
|
result = subprocess.run([str(self.binary), str(number)], capture_output=True,
|
|
text=True, timeout=30)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
|
|
def test_successful_sleep_wake_order_repeat_ownership_and_ten_second_budget(self):
|
|
self.scenario(0)
|
|
|
|
def test_live_work_prevents_entry_but_future_queue_does_not(self):
|
|
self.scenario(1)
|
|
|
|
def test_radio_refusal_and_failed_wake_return_dispatcher_watchdog_ownership(self):
|
|
self.scenario(2)
|
|
|
|
def test_critical_work_started_while_suspended_wakes_before_further_cpu_sleep(self):
|
|
self.scenario(3)
|
|
|
|
def test_all_wireless_start_paths_are_refused_during_emergency_ownership(self):
|
|
self.scenario(4)
|
|
|
|
def test_wireless_subset_blocked_mask_master_off_and_saved_snapshot_survive(self):
|
|
self.scenario(5)
|
|
|
|
def test_async_pending_errors_and_cancelled_partial_suspend_restore_correctly(self):
|
|
self.scenario(6)
|
|
|
|
def test_existing_wireless_request_finishes_before_emergency_ownership(self):
|
|
self.scenario(7)
|
|
|
|
def test_sensor_tracker_radio_ownership_is_not_stolen(self):
|
|
self.scenario(8)
|
|
|
|
def test_cpu_sleep_refusal_yields_without_resuming_normal_rf_work(self):
|
|
self.scenario(9)
|
|
|
|
def test_rak3401_actual_fem_pin_restores_before_radio_wake_without_cutting_shared_rail(self):
|
|
self.scenario(10)
|
|
|
|
def test_room_maintenance_runs_before_emergency_early_return(self):
|
|
source = (ROOT / "examples/simple_room_server/main.cpp").read_text()
|
|
loop = source[source.index("void loop() {"):]
|
|
gate = loop.index("emergency_power.service(")
|
|
for maintenance in ("mesh::power::serviceRegulatorSettings();",
|
|
"mesh::solar::service();", "board.loop();"):
|
|
self.assertEqual(loop.count(maintenance), 1, maintenance)
|
|
self.assertLess(loop.index(maintenance), gate, maintenance)
|
|
|
|
def test_esp_uart_console_restore_stays_live_during_emergency_sleep(self):
|
|
for role in ("simple_repeater", "simple_room_server", "simple_sensor"):
|
|
source = (ROOT / f"examples/{role}/main.cpp").read_text()
|
|
loop = source[source.index("void loop() {"):]
|
|
gate = loop.index("emergency_power.service(")
|
|
service = "board.servicePowerTestControls();"
|
|
self.assertLess(loop.index(service), gate, role)
|
|
self.assertIn("#if !MESH_ENABLE_EMERGENCY_POWER\n " + service + "\n#endif",
|
|
loop[gate:], role)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|