Files
MeshCore/test/test_regular_file_reads.py
mikecarper ddf625e9d0 Fix packet, delivery, clock and radio stability regressions
Reject truncated anonymous reply paths and advert metadata before copying,
while preserving timestamp-only empty-password logins. Limit room posting to
read-write/admin ACL roles, retain inbox frames until the requesting transport
accepts them, and lock raw LittleFS traversal against concurrent writes.

Separate ISR event generations from main-loop radio state. Confirm RX/TX IRQs,
detach concrete wrapper callbacks safely, and discard inconsistent LR2021 FIFO
snapshots before rearming. Preserve bridge RX frames through packet-pool
exhaustion and bound ESP-NOW retries and missing-callback recovery. Screen RTC
register layouts without losing DS1307 startup, and ignore poisoned future
contact timestamps during clock bootstrap.

Keep the nRF52 loop stack at 8 KiB while reducing the OTA listing call chain
from 7,832 to 6,056 static bytes. Retain all target names through lossless Full
Companion compression and the canonical size optimizer. Preserve constrained
STM32 filesystems and bind corrected ESP32 UART/OLED budgets to source size
assertions. Document the separately measured preexisting W12 package limits.

Adapted for this branch from meshcore-dev/MeshCore PRs #3521, #3340, #3362,
#2062, #3503, #3038, #3534, #3544 and #3512.

Validation: 1,829 native tests; sanitized parser, inbox, LittleFS, ACL, clock,
RTC, IRQ/FIFO and bridge fault tests; 332 additional Companion cases; real
RAK4631 Full and both repeater profiles, G2 Full/standard, V3 Full and both
constrained STM32 release recipes. Radio-family compile coverage continues
separately; W12 full/portable package guards remain enforced.
2026-10-09 19:56:08 -07:00

264 lines
11 KiB
Python

#!/usr/bin/env python3
"""Execute production file readers against SPIFFS-style phantom directories."""
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]
def section(path, start, end):
text = (ROOT / path).read_text(encoding="utf-8")
begin = text.index(start)
return text[begin:text.index(end, begin)]
HARNESS = r'''
#include <algorithm>
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <map>
#include <memory>
#include <stdexcept>
#include <string>
#include <iostream>
#include <helpers/FileRead.h>
#define FILE_O_READ 0
static void check(bool ok, const char* why) {
if (!ok) throw std::runtime_error(why);
}
struct Entry { std::string text; bool directory = false; };
struct FakeFS;
struct File {
std::shared_ptr<Entry> entry;
size_t position = 0;
FakeFS* owner = nullptr;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
// Actual Adafruit LittleFS requires an owner even for a closed File.
File() = delete;
#else
File() = default;
#endif
explicit File(FakeFS& fs) : owner(&fs) {}
File(FakeFS& fs, std::shared_ptr<Entry> data) : entry(data), owner(&fs) {}
explicit operator bool() const { return bool(entry); }
bool isDirectory() const { return entry && entry->directory; }
size_t size() const { return entry && !isDirectory() ? entry->text.size() : 0; }
size_t available() const { return size() - position; }
size_t read(uint8_t* dest, size_t count) {
if (!entry || isDirectory()) return 0;
count = std::min(count, available());
memcpy(dest, entry->text.data() + position, count);
position += count;
return count;
}
void close() { entry.reset(); }
};
struct FakeFS {
std::map<std::string, std::shared_ptr<Entry>> entries;
bool fail_open = false;
bool report_directory_exists = false;
unsigned missing_opens = 0;
unsigned open_calls = 0;
bool exists(const char* path) {
auto it = entries.find(path);
return it != entries.end() &&
(!it->second->directory || report_directory_exists);
}
File fixtureOpen(const char* path) {
++open_calls;
if (fail_open) return File(*this);
auto it = entries.find(path);
if (it != entries.end()) return File(*this, it->second);
++missing_opens;
return File(*this, std::make_shared<Entry>(Entry{"", true}));
}
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
File open(const char* path, uint8_t = FILE_O_READ) { return fixtureOpen(path); }
#elif defined(RP2040_PLATFORM)
// Philhower FS requires the mode argument, unlike Arduino ESP32.
File open(const char* path, const char*) { return fixtureOpen(path); }
#else
File open(const char* path, const char* = "r", bool = false) { return fixtureOpen(path); }
#endif
void put(const char* path, const std::string& content, bool directory = false) {
entries[path] = std::make_shared<Entry>(Entry{content, directory});
}
} SPIFFS;
FakeFS InternalFS;
using FILESYSTEM = FakeFS;
class DataStore {
public:
FILESYSTEM* _fs;
explicit DataStore(FILESYSTEM* fs) : _fs(fs) {}
File openRead(const char*);
File openRead(FILESYSTEM*, const char*);
File openDirectory(const char*);
File openDirectory(FILESYSTEM*, const char*);
};
@DATASTORE@
struct WiFiClient {
std::string output;
bool connected() const { return true; }
size_t write(const uint8_t* data, size_t size) {
output.append(reinterpret_cast<const char*>(data), size);
return size;
}
void printf(const char* format, ...) {
char buffer[512];
va_list args;
va_start(args, format);
int size = vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
check(size >= 0 && static_cast<size_t>(size) < sizeof(buffer), "HTTP overflow");
output.append(buffer, size);
}
};
struct LogServer {
bool running = true;
@RESPONSE@
@SENDLOG@
};
@FLOOD_VERIFY@
int main() {
try {
FakeFS fs;
DataStore store(&fs);
File unopened = mesh::emptyFile(&fs);
File unavailable = mesh::emptyFile(static_cast<FILESYSTEM*>(nullptr));
check(!unopened && !unavailable, "empty helper returned an open file");
check(fs.open_calls == 0 && InternalFS.open_calls == 0,
"closed file construction must never access either filesystem");
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
check(unopened.owner == &fs, "closed LittleFS file lost its selected owner");
check(unavailable.owner == &InternalFS, "null filesystem lacks a valid fallback owner");
#endif
File phantom = fs.fixtureOpen("/missing");
check(bool(phantom) && phantom.isDirectory(), "fixture must reproduce SPIFFS");
unsigned before = fs.missing_opens;
check(!store.openRead("/missing"), "missing file accepted");
check(fs.missing_opens == before, "missing regular file should not be opened");
check(!store.openRead(nullptr, "/file"), "unavailable FS accepted");
check(!store.openRead(nullptr), "null path accepted");
check(!store.openDirectory(nullptr, "/"), "unavailable directory FS accepted");
check(!store.openDirectory(nullptr), "null directory path accepted");
fs.put("/empty", "");
fs.put("/real", "data");
check(bool(store.openRead("/empty")), "real empty file rejected");
File real = store.openRead("/real");
uint8_t bytes[4];
check(real.read(bytes, sizeof(bytes)) == 4 && memcmp(bytes, "data", 4) == 0,
"real file changed");
check(!store.openDirectory("/real"), "regular file accepted as directory");
check(store.openDirectory("/").isDirectory(), "SPIFFS root listing broken");
fs.put("/directory", "", true);
fs.report_directory_exists = true;
check(!store.openRead("/directory"), "directory accepted when exists is true");
check(store.openDirectory("/directory").isDirectory(), "real listing broken");
fs.fail_open = true;
check(!store.openRead("/real"), "failed read-open accepted");
fs.fail_open = false;
// Empty expected images must not validate a missing file/directory as data.
check(!verifyFloodSettingsWrite(&fs, "/missing", 0, 2166136261UL),
"missing flood file passed empty verification");
check(!verifyFloodSettingsWrite(&fs, "/directory", 0, 2166136261UL),
"directory passed empty verification");
check(verifyFloodSettingsWrite(&fs, "/empty", 0, 2166136261UL),
"real empty image should verify");
check(verifyFloodSettingsWrite(&fs, "/real", 4,
updateFloodSettingsHash(2166136261UL, reinterpret_cast<const uint8_t*>("data"), 4)),
"valid flood image rejected");
LogServer server;
WiFiClient missing;
server.sendLog(missing);
check(missing.output.find("HTTP/1.1 404 Not Found\r\n") == 0,
"missing packet log must return HTTP404");
SPIFFS.put("/packet_log", "");
WiFiClient empty;
server.sendLog(empty);
check(empty.output.find("HTTP/1.1 200 OK\r\n") == 0 &&
empty.output.find("Content-Length: 0\r\n") != std::string::npos,
"existing empty packet log must remain HTTP200");
SPIFFS.put("/packet_log", "packet\n");
WiFiClient log;
server.sendLog(log);
check(log.output.find("Content-Length: 7\r\n") != std::string::npos &&
log.output.substr(log.output.size() - 7) == "packet\n", "log download changed");
SPIFFS.put("/packet_log", "", true);
SPIFFS.report_directory_exists = true;
WiFiClient directory;
server.sendLog(directory);
check(directory.output.find("HTTP/1.1 404 Not Found\r\n") == 0,
"directory log must return HTTP404");
std::cout << "regular-file readers: PASS\n";
return 0;
} catch (const std::exception& e) { std::cerr << e.what() << '\n'; return 1; }
}
'''
class RegularFileReadTest(unittest.TestCase):
def test_production_readers(self):
compiler = shutil.which("g++") or shutil.which("clang++")
if not compiler:
self.skipTest("A host C++ compiler is required")
program = HARNESS.replace("@DATASTORE@", section(
"examples/companion_radio/DataStore.cpp", "File DataStore::openRead(",
"bool DataStore::removeFile("))
program = program.replace("@RESPONSE@", section(
"src/helpers/ESP32Board.cpp", " static void sendResponse(", " bool readLine("))
program = program.replace("@SENDLOG@", section(
"src/helpers/ESP32Board.cpp", " void sendLog(", " void sendUpdateError("))
read_helper = section("examples/simple_repeater/MyMesh.cpp",
"static File openFloodSettingsRead(",
"static File openFloodSettingsWrite(")
verification = section("examples/simple_repeater/MyMesh.cpp",
"static uint32_t updateFloodSettingsHash(",
"static uint8_t batteryPercentFromMilliVolts(")
program = program.replace("@FLOOD_VERIFY@", read_helper + verification)
for platform in ("ESP32_PLATFORM", "NRF52_PLATFORM", "RP2040_PLATFORM", "STM32_PLATFORM"):
with self.subTest(platform=platform), tempfile.TemporaryDirectory() as tmp:
executable = Path(tmp) / ("readers.exe" if os.name == "nt" else "readers")
built = subprocess.run(
[compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-O0",
f"-D{platform}", "-I", str(ROOT / "test/fixtures/regular_file_reads"),
"-I", str(ROOT / "src"), "-x", "c++", "-",
"-o", str(executable)],
input=program, text=True, capture_output=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
run = subprocess.run([str(executable)], capture_output=True, text=True, timeout=15)
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
def test_directory_listing_uses_explicit_directory_api(self):
cli = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
self.assertIn("File root = _store->openDirectory(path);", cli)
self.assertIn("File root2 = _store->openDirectory(_store->getSecondaryFS(), path);", cli)
self.assertIn("File file = _store->openRead(selected_fs, path);", cli)
def test_audited_read_helpers_remain_regular_file_only(self):
for path, marker, end in (
("src/helpers/ClientACL.cpp", "static File openRead(", "#if !defined(NRF52_PLATFORM)"),
("examples/simple_room_server/FloodRuleEngine.cpp", "static File openRead(", "static File openWrite("),
):
with self.subTest(path=path):
self.assertIn("mesh::openFileRead(", section(path, marker, end))
for role in ("simple_repeater", "simple_room_server"):
source = (ROOT / f"examples/{role}/MyMesh.cpp").read_text(encoding="utf-8")
dump = extract_braced(source, "void MyMesh::dumpLogFile()")
# Command-output, cooperative USB and immediate USB destinations
# must all reject phantom directories through the guarded reader.
self.assertEqual(dump.count("mesh::openFileRead(_fs, PACKET_LOG_FILE)"), 3)
self.assertNotIn("_fs->open(", dump)
if __name__ == "__main__":
unittest.main()