diff --git a/test/test_companion_primary_radio_persistence.py b/test/test_companion_primary_radio_persistence.py index 9ae779a51..5ef6ad5be 100644 --- a/test/test_companion_primary_radio_persistence.py +++ b/test/test_companion_primary_radio_persistence.py @@ -9,6 +9,7 @@ import unittest from test_companion_preferences_transaction import HARNESS as PREFERENCES_HARNESS from test_companion_preferences_transaction import esp_recovery_helpers +from test_radio_profile_spiffs_missing import STAT_HEADER from test_replay_reset_integration import extract_braced ROOT = Path(__file__).resolve().parents[1] @@ -17,8 +18,22 @@ HARNESS = r''' #include #include #include +#include #include #define constrain(value,low,high) ((value)<(low)?(low):((value)>(high)?(high):(value))) +// The production SPIFFS metadata syscall sees the same mounted filesystem as +// the File peripheral. The standalone header keeps this fake away from libc. +static MemoryFS* profile_metadata_fs = nullptr; +static bool profile_metadata_error = false; +static unsigned profile_metadata_probes = 0; +int stat(const char* path, struct stat* info) { + assert(profile_metadata_fs && !strncmp(path, "/spiffs/radio_profiles", 22)); + ++profile_metadata_probes; + if (profile_metadata_error) { errno = EIO; return -1; } + if (!profile_metadata_fs->exists(path + 7)) { errno = ENOENT; return -1; } + info->st_mode = 0100000; + return 0; +} struct Clock : mesh::RTCClock { uint32_t getCurrentTime() override { return 1700000000; } void setCurrentTime(uint32_t) override {} @@ -55,6 +70,7 @@ struct MyMesh { _prefs.freq=909.5f; _prefs.bw=62.5f; _prefs.sf=7; _prefs.cr=5; _prefs.airtime_factor=1; _prefs.rx_ps_level=1; _prefs.rx_ps_preamble=16; _prefs.rx_ps_rx_us=3000; _prefs.rx_ps_sleep_us=4000; + profile_metadata_fs=&profile_fs; _radio_profiles.begin(&profile_fs,&radio,&clock); assert(_radio_profiles.savePrimaryPreamble(48)); assert(savePrefs()); @@ -63,6 +79,7 @@ struct MyMesh { void bootSanitize() { @COMPANION_SANITIZE@ } bool handleCommand(const char*,uint32_t,char*); const char* command(const char* value,uint32_t timestamp=0) { + profile_metadata_fs=&profile_fs; memset(reply,0,sizeof(reply)); assert(handleCommand(value,timestamp,reply)); assert(memchr(reply,0,sizeof(reply))); @@ -73,6 +90,7 @@ struct MyMesh { assert(store.loadPrefs(loaded,lat,lon)); } uint16_t rebootPreamble() { + profile_metadata_fs=&profile_fs; Radio reboot_radio;mesh::RadioProfileCLI reboot_cli; reboot_cli.begin(&profile_fs,&reboot_radio,&clock); return reboot_cli.primaryPreamble(); @@ -181,6 +199,18 @@ int main() { assert(error(m.command(command)));assert(m.store.fs.files["/new_prefs"]==prefs); assert(m._prefs.airtime_factor==1);++checks; } + { // Unreadable metadata must protect the existing committed profile image. + MyMesh m; + const auto files=m.profile_fs.files; + Radio reboot_radio;mesh::RadioProfileCLI reboot_cli; + profile_metadata_error=true; + reboot_cli.begin(&m.profile_fs,&reboot_radio,&m.clock); + assert(!reboot_cli.savePrimaryPreamble(32)); + assert(m.profile_fs.files==files); + profile_metadata_error=false; + m.expectOriginal();++checks; + } + assert(profile_metadata_probes>0); printf("%u production primary-radio persistence scenarios passed\n",checks); } ''' @@ -228,6 +258,13 @@ def production_primary_radio_harness(): return harness +def prepare_profile_metadata_boundary(work): + """Supply only the SPIFFS syscall declaration, shared with the app test.""" + work = Path(work) + (work / 'sys').mkdir(exist_ok=True) + (work / 'sys/stat.h').write_text(STAT_HEADER, encoding='ascii') + + class CompanionPrimaryRadioPersistenceTests(unittest.TestCase): def test_production_validation_rollback_and_dutycycle_boot(self): harness = production_primary_radio_harness() @@ -235,6 +272,7 @@ class CompanionPrimaryRadioPersistenceTests(unittest.TestCase): self.assertIsNotNone(compiler) with tempfile.TemporaryDirectory(prefix='companion-primary-radio-') as directory: work = Path(directory) + prepare_profile_metadata_boundary(work) # Reuse the profile fixture's identity/crypto mocks and add only # unused JSON conversion links required by real NodePrefs adapters. utils = (ROOT/'test/mocks/Utils.h').read_text() diff --git a/test/test_companion_usb_default.py b/test/test_companion_usb_default.py index bc19dba8f..bead629e6 100644 --- a/test/test_companion_usb_default.py +++ b/test/test_companion_usb_default.py @@ -343,7 +343,11 @@ class CompanionUsbDefaultTest(unittest.TestCase): self.assertLess(loop.index("serviceUsbLoggingReplyBarrier();"), loop.index("the_mesh.loop();")) self.assertLess(loop.index("serviceUsbLoggingOwnership(mesh::isUsbLoggingEnabled());"), loop.index("the_mesh.loop();")) self.assertLess(loop.index("expireUsbBinaryStartupProbeBeforeDispatch();"), loop.index("the_mesh.loop();")) - self.assertNotIn("COMPANION_RADIO_FULL", MAIN) + # USB defaults apply to ordinary and Full builds. Unrelated WiFi OTA + # ownership may legitimately use the Full profile elsewhere in main. + for name in ("beginUsbDefaultSession", "resetUsbTerminalHostSession", + "serviceUsbTerminalHostSessionReset", "serviceUsbAsciiSessionDefault"): + self.assertNotIn("COMPANION_RADIO_FULL", function("static void " + name + "() {")) source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text() self.assertNotIn("mesh::setUsbLoggingEnabled(", source) logging_reply = source[source.index('if (strncmp(command, "set logging.output ", 19)'): diff --git a/test/test_official_app_compatibility.py b/test/test_official_app_compatibility.py index 38f748e88..6b2400f3a 100644 --- a/test/test_official_app_compatibility.py +++ b/test/test_official_app_compatibility.py @@ -19,7 +19,8 @@ import unittest import urllib.request from test_client_acl_response import production_acl_methods -from test_companion_primary_radio_persistence import production_primary_radio_harness +from test_companion_primary_radio_persistence import ( + prepare_profile_metadata_boundary, production_primary_radio_harness) from test_companion_delayed_reply_delivery import production_delayed_reply_inputs from test_replay_reset_integration import extract_braced @@ -244,6 +245,7 @@ public: transaction = (ROOT / "src/helpers/ContactFileTransaction.h").read_text() (work / "ContactFileTransaction.h").write_text(transaction.replace( '#include "IdentityStore.h"', '#include '), encoding="ascii") + prepare_profile_metadata_boundary(work) (work / "radio.cpp").write_text(harness, encoding="ascii") binary = work / "radio.exe" checked([compiler, "-std=c++17", "-Wall", "-Wextra", "-Wno-unused-function",