mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 07:52:54 +00:00
627 lines
27 KiB
Python
627 lines
27 KiB
Python
#!/usr/bin/env python3
|
|
"""Run the production boot-only SPIFFS inventory over SDK/POSIX doubles.
|
|
|
|
The actual FS decorator, owner registry, metadata helper and transaction
|
|
recovery run unchanged. Directory and flash failures occur only at peripheral
|
|
boundaries. The reusable deadline workload reports a 129 ms/name-scan model,
|
|
not physical timing or a qualification of flash corruption.
|
|
"""
|
|
from pathlib import Path
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
from test_replay_reset_integration import extract_braced
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
PRODUCTION = ROOT / "src/helpers/esp32/BootFileSystem.cpp"
|
|
|
|
FS_HEADER = r'''
|
|
#pragma once
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <string>
|
|
using String = std::string;
|
|
using boolean = bool;
|
|
namespace fs {
|
|
class FileImpl {
|
|
public:
|
|
bool directory = false;
|
|
std::string name;
|
|
virtual ~FileImpl() = default;
|
|
};
|
|
using FileImplPtr = std::shared_ptr<FileImpl>;
|
|
class File {
|
|
public:
|
|
FileImplPtr impl;
|
|
File(FileImplPtr value = {}) : impl(value) {}
|
|
explicit operator bool() const { return bool(impl); }
|
|
bool isDirectory() const { return impl && impl->directory; }
|
|
};
|
|
class FSImpl {
|
|
protected:
|
|
const char* _mountpoint = nullptr;
|
|
public:
|
|
virtual ~FSImpl() = default;
|
|
virtual FileImplPtr open(const char*, const char*, bool) = 0;
|
|
virtual bool exists(const char*) = 0;
|
|
virtual bool rename(const char*, const char*) = 0;
|
|
virtual bool remove(const char*) = 0;
|
|
virtual bool mkdir(const char*) = 0;
|
|
virtual bool rmdir(const char*) = 0;
|
|
void mountpoint(const char* value) { _mountpoint = value; }
|
|
const char* mountpoint() { return _mountpoint; }
|
|
};
|
|
using FSImplPtr = std::shared_ptr<FSImpl>;
|
|
class FS {
|
|
protected:
|
|
FSImplPtr _impl;
|
|
public:
|
|
explicit FS(FSImplPtr impl) : _impl(impl) {}
|
|
File open(const char* path, const char* mode = "r", bool create = false) {
|
|
return File(_impl->open(path, mode, create));
|
|
}
|
|
bool exists(const char* path) { return _impl->exists(path); }
|
|
bool remove(const char* path) { return _impl->remove(path); }
|
|
bool rename(const char* from, const char* to) { return _impl->rename(from, to); }
|
|
bool mkdir(const char* path) { return _impl->mkdir(path); }
|
|
bool rmdir(const char* path) { return _impl->rmdir(path); }
|
|
};
|
|
}
|
|
using fs::File;
|
|
'''
|
|
|
|
DIRENT_HEADER = r'''
|
|
#pragma once
|
|
#include <cstddef>
|
|
struct DIR { size_t cursor = 0; };
|
|
struct dirent { unsigned char d_type = 1; char d_name[256] = {}; };
|
|
#define DT_REG 1
|
|
#define DT_DIR 2
|
|
DIR* opendir(const char*);
|
|
struct dirent* readdir(DIR*);
|
|
int closedir(DIR*);
|
|
'''
|
|
|
|
STAT_HEADER = r'''
|
|
#pragma once
|
|
struct stat { unsigned st_mode = 0; };
|
|
int stat(const char*, struct stat*);
|
|
'''
|
|
|
|
HARNESS = r'''
|
|
#include <cassert>
|
|
#include <cerrno>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <new>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <strings.h>
|
|
#include <FS.h>
|
|
#include <dirent.h>
|
|
#include <sys/stat.h>
|
|
#include <helpers/esp32/BootFileSystem.h>
|
|
#include <helpers/esp32/BootFilePresence.h>
|
|
#include <helpers/FilePresence.h>
|
|
#include <helpers/CommonPrefsRecovery.h>
|
|
|
|
static unsigned new_calls = 0;
|
|
void* operator new(size_t count) {
|
|
++new_calls;
|
|
if (void* address = std::malloc(count)) return address;
|
|
throw std::bad_alloc();
|
|
}
|
|
void operator delete(void* address) noexcept { std::free(address); }
|
|
void operator delete(void* address, size_t) noexcept { std::free(address); }
|
|
|
|
struct Peripherals {
|
|
std::vector<std::string> names;
|
|
unsigned scans = 0, directory_opens = 0, directory_reads = 0, directory_closes = 0;
|
|
unsigned native_exists = 0, native_opens = 0, stats = 0, allocations = 0;
|
|
unsigned mutations = 0, frees = 0, reallocations = 0;
|
|
size_t shrink_bytes = 0;
|
|
bool fail_open = false, fail_close = false, fail_malloc = false, metadata_error = false;
|
|
bool fail_realloc = false;
|
|
bool dirty_success_errno = false, unterminated_name = false;
|
|
int fail_read_at = -1;
|
|
const void* owner = nullptr;
|
|
} peripheral;
|
|
static std::map<void*, size_t> inventory_buffers;
|
|
|
|
void* inventory_malloc(size_t count) {
|
|
++peripheral.allocations;
|
|
assert(count == 8192);
|
|
if (peripheral.fail_malloc) return nullptr;
|
|
void* address = std::malloc(count);
|
|
if (address) inventory_buffers[address] = count;
|
|
return address;
|
|
}
|
|
void* inventory_realloc(void* address, size_t count) {
|
|
++peripheral.reallocations;
|
|
peripheral.shrink_bytes = count;
|
|
assert(inventory_buffers.count(address) && inventory_buffers[address] == 8192);
|
|
assert(count > 0 && count < 8192);
|
|
if (peripheral.fail_realloc) return nullptr;
|
|
const auto found = inventory_buffers.find(address);
|
|
void* compact = std::realloc(address, count);
|
|
if (compact) {
|
|
inventory_buffers.erase(found);
|
|
inventory_buffers[compact] = count;
|
|
}
|
|
return compact;
|
|
}
|
|
void inventory_free(void* address) {
|
|
if (address) {
|
|
assert(inventory_buffers.erase(address) == 1);
|
|
++peripheral.frees;
|
|
}
|
|
std::free(address);
|
|
}
|
|
DIR* opendir(const char* path) {
|
|
++peripheral.directory_opens;
|
|
++peripheral.scans; // One complete SPIFFS lookup-table traversal, when valid.
|
|
assert(std::string(path) == "/spiffs" || std::string(path) == "/spiffs/");
|
|
if (peripheral.fail_open) { errno = EIO; return nullptr; }
|
|
return new DIR;
|
|
}
|
|
struct dirent* readdir(DIR* directory) {
|
|
static struct dirent entry;
|
|
++peripheral.directory_reads;
|
|
if (int(directory->cursor) == peripheral.fail_read_at) { errno = EIO; return nullptr; }
|
|
if (directory->cursor == peripheral.names.size()) return nullptr;
|
|
entry = {};
|
|
const std::string& name = peripheral.names[directory->cursor++];
|
|
assert(name.size() < sizeof(entry.d_name));
|
|
memcpy(entry.d_name, name.c_str(), name.size() + 1);
|
|
if (peripheral.unterminated_name) memset(entry.d_name, 'x', sizeof(entry.d_name));
|
|
if (peripheral.dirty_success_errno) errno = EIO;
|
|
return &entry;
|
|
}
|
|
int closedir(DIR* directory) {
|
|
++peripheral.directory_closes;
|
|
delete directory;
|
|
if (peripheral.fail_close) { errno = EIO; return -1; }
|
|
return 0;
|
|
}
|
|
|
|
class NativeFSImpl : public fs::FSImpl {
|
|
public:
|
|
std::set<std::string> files;
|
|
std::set<std::string> directories;
|
|
fs::FileImplPtr last_open;
|
|
bool fail_mutation = false, unreadable = false;
|
|
NativeFSImpl() { mountpoint("/spiffs"); }
|
|
static void requireInvalidated() {
|
|
++peripheral.mutations;
|
|
assert(!mesh::esp32BootFileKnownAbsent(peripheral.owner, "/never-created"));
|
|
}
|
|
fs::FileImplPtr open(const char* path, const char* mode, bool create) override {
|
|
++peripheral.native_opens;
|
|
const bool write = !mode || (strcmp(mode, "r") && strcmp(mode, "rb")) || create;
|
|
if (write) {
|
|
requireInvalidated();
|
|
if (fail_mutation) return {};
|
|
if (path) files.insert(path);
|
|
}
|
|
if (path && strlen(path) > 31) { errno = ENAMETOOLONG; return {}; }
|
|
const bool present = path && files.count(path);
|
|
peripheral.scans += present ? 3 : 2; // pinned VFS stat + constructor stat (+ fopen).
|
|
if (unreadable && present) return {};
|
|
last_open = std::make_shared<fs::FileImpl>();
|
|
last_open->directory = !present || directories.count(path);
|
|
last_open->name = path ? path : "";
|
|
return last_open; // Missing r-open is a truthy SPIFFS Directory.
|
|
}
|
|
bool exists(const char* path) override {
|
|
++peripheral.native_exists;
|
|
const bool present = path && files.count(path);
|
|
peripheral.scans += present ? 3 : 2;
|
|
return present && !unreadable;
|
|
}
|
|
bool rename(const char* from, const char* to) override {
|
|
requireInvalidated();
|
|
if (fail_mutation || !files.count(from) || files.count(to)) return false;
|
|
files.erase(from); files.insert(to); return true;
|
|
}
|
|
bool remove(const char* path) override {
|
|
requireInvalidated();
|
|
return !fail_mutation && files.erase(path);
|
|
}
|
|
bool mkdir(const char*) override { requireInvalidated(); return !fail_mutation; }
|
|
bool rmdir(const char*) override { requireInvalidated(); return !fail_mutation; }
|
|
};
|
|
static NativeFSImpl* metadata_fs;
|
|
int stat(const char* absolute, struct stat* info) {
|
|
++peripheral.stats; ++peripheral.scans;
|
|
if (peripheral.metadata_error) { errno = EIO; return -1; }
|
|
assert(strncmp(absolute, "/spiffs", 7) == 0);
|
|
if (!metadata_fs->files.count(absolute + 7)) { errno = ENOENT; return -1; }
|
|
info->st_mode = 0100000;
|
|
return 0;
|
|
}
|
|
|
|
// This shell executes the real transaction recovery's probes/rename policy.
|
|
#define FILESYSTEM fs::FS
|
|
namespace mesh {
|
|
class ContactFileTransaction {
|
|
public:
|
|
using PresenceProbe = bool (*)(FILESYSTEM*, const char*, bool&);
|
|
@PROBE@
|
|
@RECOVER@
|
|
};
|
|
}
|
|
struct CommonCLI { bool recoverCommonPrefsFiles(fs::FS*); };
|
|
@COMMON_RECOVER@
|
|
|
|
static void populate(NativeFSImpl& native, const std::vector<std::string>& names) {
|
|
peripheral.names = names;
|
|
for (const std::string& name : names)
|
|
native.files.insert(name[0] == '/' ? name : "/" + name);
|
|
}
|
|
static void requireInactiveFallback(mesh::Esp32BootFileSystem& view) {
|
|
assert(!view.isInventoryActive());
|
|
assert(!mesh::esp32BootFileKnownAbsent(&view, "/never-created"));
|
|
const unsigned calls = peripheral.native_exists;
|
|
assert(!view.exists("/never-created"));
|
|
assert(peripheral.native_exists == calls + 1);
|
|
const unsigned directory_calls = peripheral.directory_opens;
|
|
assert(!view.beginInventory());
|
|
assert(peripheral.directory_opens == directory_calls);
|
|
}
|
|
|
|
static void deadlineWorkload(fs::FS& fs) {
|
|
assert(mesh::ContactFileTransaction::recover(&fs, "/com_prefs"));
|
|
assert(!fs.exists("/com_prefs") && !fs.exists("/node_prefs") && !fs.exists("/prefs.json"));
|
|
assert(mesh::ContactFileTransaction::recover(&fs, "/data_tx"));
|
|
assert(!fs.exists("/data_tx") && !fs.exists("/management") && !fs.exists("/management.bak"));
|
|
assert(mesh::ContactFileTransaction::recover(
|
|
&fs, "/telemetry_history_tx", mesh::filePresence<fs::FS>));
|
|
// Repeated metadata calls follow the empty ACL/replay startup recovery shape.
|
|
for (const char* path : {"/s_contacts", "/s_contacts.bak", "/client_login",
|
|
"/client_login.bak", "/client_login.tmp", "/s_contacts",
|
|
"/s_contacts.bak", "/s_contacts.tmp", "/s_contacts",
|
|
"/telemetry_history_tx"}) {
|
|
bool present = true;
|
|
assert(mesh::filePresence(&fs, path, present) && !present);
|
|
}
|
|
for (const char* path : {"/flood_filters", "/flood_filters.tmp", "/flood_filters.bak",
|
|
"/clock_sync"}) assert(!fs.exists(path));
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
assert(argc == 2);
|
|
const std::string which = argv[1];
|
|
auto native = std::make_shared<NativeFSImpl>();
|
|
metadata_fs = native.get();
|
|
fs::FS original(native);
|
|
const unsigned construction_allocations = new_calls;
|
|
mesh::Esp32BootFileSystem view(original);
|
|
assert(new_calls == construction_allocations); // Embedded alias reuses SDK control block.
|
|
peripheral.owner = &view;
|
|
|
|
if (which == "timing_on" || which == "timing_off") {
|
|
if (which == "timing_on") assert(view.beginInventory());
|
|
deadlineWorkload(view);
|
|
const unsigned elapsed = 1000 + 129 * peripheral.scans;
|
|
std::cout << "{\"elapsed_ms\":" << elapsed << ",\"metadata_lookups\":" << peripheral.scans
|
|
<< ",\"native_exists\":" << peripheral.native_exists
|
|
<< ",\"native_opens\":" << peripheral.native_opens
|
|
<< ",\"snapshot_reads\":" << peripheral.directory_reads << "}\n";
|
|
return 0;
|
|
}
|
|
|
|
if (which == "basic" || which == "owner" || which == "positive_fault" || which == "positive_directory") {
|
|
populate(*native, {"/identity/_main.id", "com_prefs", "com_prefs.tmp",
|
|
"com_prefs.bak", "mqtt_prefs", "s_contacts", "corrupt", "Dir/leaf"});
|
|
assert(view.beginInventory() && view.isInventoryActive());
|
|
size_t expected_bytes = 0;
|
|
for (const std::string& name : peripheral.names)
|
|
expected_bytes += name.size() + (name[0] == '/' ? 1 : 2);
|
|
assert(peripheral.allocations == 1 && peripheral.reallocations == 1);
|
|
assert(peripheral.shrink_bytes == expected_bytes);
|
|
assert(inventory_buffers.size() == 1 && inventory_buffers.begin()->second == expected_bytes);
|
|
assert(!view.exists("/never-created"));
|
|
assert(!view.open("/never-created", "r"));
|
|
assert(!view.open("/never-created", "rb"));
|
|
assert(peripheral.native_exists == 0 && peripheral.native_opens == 0);
|
|
bool present = true;
|
|
assert(mesh::filePresence(&view, "/never-created", present) && !present);
|
|
assert(peripheral.stats == 0);
|
|
if (which == "owner") {
|
|
assert(!mesh::esp32BootFileKnownAbsent(&original, "/never-created"));
|
|
assert(!mesh::esp32BootFileKnownAbsent(nullptr, "/never-created"));
|
|
assert(mesh::filePresence(&original, "/never-created", present) && !present);
|
|
assert(peripheral.stats == 1);
|
|
assert(original.open("/never-created").isDirectory());
|
|
mesh::Esp32BootFileSystem other_view(original);
|
|
const unsigned directory_calls = peripheral.directory_opens;
|
|
assert(!other_view.beginInventory());
|
|
assert(peripheral.directory_opens == directory_calls);
|
|
assert(!mesh::esp32BootFileKnownAbsent(&other_view, "/never-created"));
|
|
assert(mesh::esp32BootFileKnownAbsent(&view, "/never-created"));
|
|
} else if (which == "positive_fault") {
|
|
peripheral.metadata_error = true;
|
|
assert(!mesh::filePresence(&view, "/com_prefs", present));
|
|
native->unreadable = true;
|
|
assert(!view.open("/com_prefs"));
|
|
assert(peripheral.stats == 1 && peripheral.native_opens == 1);
|
|
} else if (which == "positive_directory") {
|
|
native->directories.insert("/corrupt");
|
|
const auto file = view.open("/corrupt");
|
|
assert(file.isDirectory() && file.impl == native->last_open);
|
|
assert(peripheral.native_opens == 1);
|
|
} else {
|
|
for (const char* path : {"/com_prefs", "/com_prefs.tmp", "/com_prefs.bak",
|
|
"/mqtt_prefs", "/s_contacts", "/corrupt"}) {
|
|
assert(view.exists(path));
|
|
auto file = view.open(path);
|
|
assert(file.impl == native->last_open && !file.isDirectory());
|
|
}
|
|
assert(mesh::filePresence(&view, "/com_prefs", present) && present);
|
|
assert(peripheral.stats == 1); // Positive metadata remains a real stat.
|
|
const unsigned before = peripheral.native_opens;
|
|
for (const char* path : {"/", "/identity", "/IDENTITY", "/dir", "/Dir/", "/Dir//leaf",
|
|
"/./missing", "/../missing", "relative"}) {
|
|
assert(view.open(path)); // Delegate to original, preserving SDK directory behavior.
|
|
}
|
|
assert(peripheral.native_opens == before + 9);
|
|
assert(!view.exists("/COM_PREFS")); // Exact file lookup stays case-sensitive.
|
|
const std::string long_path = "/" + std::string(31, 'x');
|
|
const unsigned long_before = peripheral.native_opens;
|
|
errno = 0;
|
|
assert(!view.open(long_path.c_str()) && errno == ENAMETOOLONG);
|
|
assert(peripheral.native_opens == long_before + 1);
|
|
assert(!mesh::esp32BootFileKnownAbsent(&view, long_path.c_str()));
|
|
}
|
|
assert(view.isInventoryActive());
|
|
view.endInventory();
|
|
assert(inventory_buffers.empty());
|
|
requireInactiveFallback(view);
|
|
} else if (which == "rename_backup" || which == "rename_temp") {
|
|
const char* source = which == "rename_backup" ? "/com_prefs.bak" : "/com_prefs.tmp";
|
|
populate(*native, {source + 1});
|
|
assert(view.beginInventory());
|
|
assert(!view.exists("/com_prefs") && view.exists(source));
|
|
if (which == "rename_backup") {
|
|
assert(mesh::ContactFileTransaction::recover(&view, "/com_prefs"));
|
|
assert(view.exists("/com_prefs") && !view.exists(source));
|
|
assert(view.open("/com_prefs").impl == native->last_open);
|
|
} else {
|
|
CommonCLI cli;
|
|
assert(cli.recoverCommonPrefsFiles(&view)); // First-save temp is uncommitted.
|
|
assert(!view.exists("/com_prefs") && !view.exists(source));
|
|
}
|
|
requireInactiveFallback(view);
|
|
} else if (which == "mutation_before_begin" || which == "end_before_begin") {
|
|
if (which == "mutation_before_begin") view.open("/new", "w");
|
|
else view.endInventory();
|
|
assert(!view.beginInventory());
|
|
assert(peripheral.directory_opens == 0 && peripheral.allocations == 0);
|
|
} else if (which.rfind("mutation_", 0) == 0) {
|
|
assert(view.beginInventory());
|
|
native->fail_mutation = which.find("fail") != std::string::npos;
|
|
const std::string operation = which.substr(9);
|
|
if (operation == "rplus") view.open("/new", "r+");
|
|
else if (operation == "rbplus") view.open("/new", "rb+");
|
|
else if (operation == "write") view.open("/new", "w");
|
|
else if (operation == "append") view.open("/new", "a");
|
|
else if (operation == "unknown") view.open("/new", "unexpected");
|
|
else if (operation == "null") view.open("/new", nullptr);
|
|
else if (operation == "create") view.open("/new", "r", true);
|
|
else if (operation == "open_fail") view.open("/new", "w");
|
|
else if (operation == "rename_fail") assert(!view.rename("/absent", "/new"));
|
|
else if (operation == "remove_fail") assert(!view.remove("/absent"));
|
|
else if (operation == "mkdir") view.mkdir("/dir");
|
|
else if (operation == "rmdir") view.rmdir("/dir");
|
|
else assert(false);
|
|
assert(peripheral.mutations == 1);
|
|
requireInactiveFallback(view);
|
|
} else if (which == "end" || which == "global_end") {
|
|
assert(view.beginInventory());
|
|
if (which == "end") view.endInventory();
|
|
else mesh::endEsp32BootFileInventory();
|
|
requireInactiveFallback(view);
|
|
} else if (which == "null_backend") {
|
|
fs::FS null_fs(fs::FSImplPtr{});
|
|
mesh::Esp32BootFileSystem null_view(null_fs);
|
|
assert(!null_view.beginInventory() && !null_view.isInventoryActive());
|
|
assert(peripheral.directory_opens == 0 && peripheral.allocations == 0);
|
|
} else if (which == "other_mount") {
|
|
native->mountpoint("/sd");
|
|
mesh::Esp32BootFileSystem other_view(original);
|
|
assert(!other_view.beginInventory() && !other_view.isInventoryActive());
|
|
assert(peripheral.directory_opens == 0 && peripheral.allocations == 0);
|
|
} else if (which == "dirty_errno") {
|
|
populate(*native, {"present"});
|
|
peripheral.dirty_success_errno = true;
|
|
assert(view.beginInventory()); // Stale errno from a successful read is not an EOF error.
|
|
assert(view.isInventoryActive() && !view.exists("/missing"));
|
|
assert(peripheral.directory_closes == 1);
|
|
} else if (which == "empty") {
|
|
assert(view.beginInventory());
|
|
assert(view.isInventoryActive() && !view.exists("/missing") && !view.open("/missing"));
|
|
assert(peripheral.native_exists == 0 && peripheral.native_opens == 0);
|
|
assert(peripheral.allocations == 1 && peripheral.frees == 1 && peripheral.reallocations == 0);
|
|
assert(inventory_buffers.empty());
|
|
} else {
|
|
populate(*native, {"present"});
|
|
if (which == "open_error") peripheral.fail_open = true;
|
|
else if (which == "close_error") peripheral.fail_close = true;
|
|
else if (which == "read_error_first") peripheral.fail_read_at = 0;
|
|
else if (which == "read_error_partial") peripheral.fail_read_at = 1;
|
|
else if (which == "oom") peripheral.fail_malloc = true;
|
|
else if (which == "shrink_failure") peripheral.fail_realloc = true;
|
|
else if (which == "overflow") {
|
|
peripheral.names.clear();
|
|
for (unsigned i = 0; i < 400; ++i)
|
|
peripheral.names.push_back(std::string(25, 'x') + std::to_string(i));
|
|
} else if (which == "bad_name_empty") peripheral.names = {""};
|
|
else if (which == "bad_name_dot") peripheral.names = {"../identity"};
|
|
else if (which == "bad_name_repeat") peripheral.names = {"identity//key"};
|
|
else if (which == "bad_name_absolute") peripheral.names = {"//identity/key"};
|
|
else if (which == "bad_name_long") peripheral.names = {std::string(31, 'x')};
|
|
else if (which == "bad_name_unterminated") peripheral.unterminated_name = true;
|
|
else assert(false);
|
|
assert(!view.beginInventory());
|
|
assert(inventory_buffers.empty());
|
|
if (which == "shrink_failure") {
|
|
assert(peripheral.reallocations == 1 && peripheral.frees == 1);
|
|
assert(peripheral.shrink_bytes == strlen("/present") + 1);
|
|
}
|
|
requireInactiveFallback(view);
|
|
if (peripheral.directory_opens && !peripheral.fail_open)
|
|
assert(peripheral.directory_closes == 1);
|
|
}
|
|
}
|
|
'''
|
|
|
|
|
|
def compile_boot_inventory_harness(work, mutation=None):
|
|
"""Return a compiled actual-class harness; all generated files live in work."""
|
|
work = Path(work)
|
|
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
|
if compiler is None:
|
|
raise unittest.SkipTest("a host C++17 compiler is required")
|
|
(work / "sys").mkdir(exist_ok=True)
|
|
(work / "FS.h").write_text(FS_HEADER, encoding="utf-8")
|
|
(work / "FSImpl.h").write_text('#include "FS.h"\n', encoding="utf-8")
|
|
(work / "Arduino.h").write_text('#include "FS.h"\n', encoding="utf-8")
|
|
(work / "sdkconfig.h").write_text('#define CONFIG_SPIFFS_OBJ_NAME_LEN 32\n', encoding="utf-8")
|
|
(work / "dirent.h").write_text(DIRENT_HEADER, encoding="utf-8")
|
|
(work / "sys/stat.h").write_text(STAT_HEADER, encoding="utf-8")
|
|
transaction = (ROOT / "src/helpers/ContactFileTransaction.h").read_text(encoding="utf-8")
|
|
probe = extract_braced(transaction, "static bool probe(")
|
|
recover = extract_braced(transaction, "static bool recover(")
|
|
common_recover = extract_braced(
|
|
(ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="utf-8"),
|
|
"bool CommonCLI::recoverCommonPrefsFiles(")
|
|
harness = HARNESS.replace("@PROBE@", probe).replace("@RECOVER@", recover).replace(
|
|
"@COMMON_RECOVER@", common_recover)
|
|
cpp = work / "harness.cpp"
|
|
cpp.write_text(harness, encoding="utf-8")
|
|
# Include real production under allocation-boundary substitution. Standard
|
|
# declarations are read first; no production logic is replaced by a stub.
|
|
production = PRODUCTION.read_text(encoding="utf-8")
|
|
if mutation is not None:
|
|
production = mutation(production)
|
|
wrapped = work / "production.cpp"
|
|
wrapped.write_text(
|
|
'#include <cstdlib>\n#include <cstddef>\n'
|
|
'void* inventory_malloc(size_t);\nvoid* inventory_realloc(void*, size_t);\n'
|
|
'void inventory_free(void*);\n'
|
|
'namespace std { using ::inventory_malloc; using ::inventory_realloc; using ::inventory_free; }\n'
|
|
'#define malloc inventory_malloc\n#define realloc inventory_realloc\n#define free inventory_free\n' + production,
|
|
encoding="utf-8")
|
|
binary = work / "inventory"
|
|
compiled = subprocess.run([
|
|
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-DESP32_PLATFORM=1",
|
|
*(["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
|
|
if sys.platform.startswith("linux") else []),
|
|
f"-I{work}", f"-I{ROOT / 'src'}", f"-I{PRODUCTION.parent}",
|
|
str(cpp), str(wrapped), "-o", str(binary),
|
|
], capture_output=True, text=True, timeout=60)
|
|
if compiled.returncode != 0:
|
|
raise AssertionError(compiled.stdout + compiled.stderr)
|
|
return binary
|
|
|
|
|
|
def run_boot_inventory_timing(disable_inventory=False):
|
|
"""Return modeled readiness and native lookup counts for GUI deadline tests."""
|
|
with tempfile.TemporaryDirectory(prefix="boot-inventory-timing-") as folder:
|
|
binary = compile_boot_inventory_harness(folder)
|
|
case = "timing_off" if disable_inventory else "timing_on"
|
|
result = subprocess.run([str(binary), case], cwd=folder,
|
|
capture_output=True, text=True, timeout=10)
|
|
if result.returncode != 0:
|
|
raise AssertionError(result.stdout + result.stderr)
|
|
return json.loads(result.stdout)
|
|
|
|
|
|
class Esp32BootFileInventoryTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.folder = tempfile.TemporaryDirectory(prefix="boot-inventory-")
|
|
cls.addClassCleanup(cls.folder.cleanup)
|
|
cls.binary = compile_boot_inventory_harness(cls.folder.name)
|
|
|
|
def run_case(self, case):
|
|
result = subprocess.run([str(self.binary), case], cwd=self.folder.name,
|
|
capture_output=True, text=True, timeout=10)
|
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
return result.stdout
|
|
|
|
def test_missing_positive_transaction_and_directory_paths(self):
|
|
for case in ("basic", "empty", "rename_backup", "rename_temp", "positive_fault", "positive_directory"):
|
|
with self.subTest(case=case):
|
|
self.run_case(case)
|
|
|
|
def test_exact_owner_identity_and_permanent_end(self):
|
|
for case in ("owner", "end", "global_end", "null_backend", "other_mount", "end_before_begin"):
|
|
with self.subTest(case=case):
|
|
self.run_case(case)
|
|
|
|
def test_mutation_invalidates_before_delegation_even_failure(self):
|
|
self.run_case("mutation_before_begin")
|
|
for kind in ("rplus", "rbplus", "write", "append", "unknown", "null", "create",
|
|
"open_fail", "rename_fail", "remove_fail", "mkdir", "rmdir"):
|
|
with self.subTest(kind=kind):
|
|
self.run_case("mutation_" + kind)
|
|
|
|
def test_complete_eof_is_required_and_stale_errno_is_cleared(self):
|
|
for case in ("open_error", "read_error_first", "read_error_partial", "close_error", "dirty_errno"):
|
|
with self.subTest(case=case):
|
|
self.run_case(case)
|
|
|
|
def test_oom_capacity_and_malformed_names_fall_back(self):
|
|
for case in ("oom", "shrink_failure", "overflow", "bad_name_empty", "bad_name_dot", "bad_name_repeat",
|
|
"bad_name_absolute", "bad_name_long", "bad_name_unterminated"):
|
|
with self.subTest(case=case):
|
|
self.run_case(case)
|
|
|
|
def test_native_lookup_cost_crosses_unchanged_gui_deadline(self):
|
|
active = json.loads(self.run_case("timing_on"))
|
|
disabled = json.loads(self.run_case("timing_off"))
|
|
self.assertLess(active["elapsed_ms"], 5000)
|
|
self.assertGreaterEqual(disabled["elapsed_ms"], 5000)
|
|
self.assertEqual(active["native_exists"], 0)
|
|
self.assertEqual(active["metadata_lookups"], 1)
|
|
self.assertGreater(disabled["native_exists"], 0)
|
|
|
|
def test_compiled_negative_controls_detect_missing_guards(self):
|
|
controls = (
|
|
("skip_negative_open",
|
|
"else if (owner_.knownAbsent(path)) return fs::FileImplPtr();",
|
|
"else if (false) return fs::FileImplPtr();", "basic", "!view.open"),
|
|
("skip_mutation_invalidation", "owner_.endInventory();", "(void)0;",
|
|
"mutation_write", "!mesh::esp32BootFileKnownAbsent"),
|
|
("accept_partial_scan", "complete = errno == 0;", "complete = true;",
|
|
"read_error_partial", "!view.beginInventory()"),
|
|
("hide_positive_file", "strcmp(name, path) == 0", "false",
|
|
"basic", "view.exists(path)"),
|
|
)
|
|
for name, before, after, case, expected_assertion in controls:
|
|
with self.subTest(control=name):
|
|
def mutate(source, before=before, after=after):
|
|
self.assertIn(before, source)
|
|
return source.replace(before, after)
|
|
|
|
with tempfile.TemporaryDirectory(prefix="boot-inventory-negative-") as folder:
|
|
binary = compile_boot_inventory_harness(folder, mutate)
|
|
result = subprocess.run([str(binary), case], cwd=folder,
|
|
capture_output=True, text=True, timeout=10)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn(expected_assertion, result.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|