#!/usr/bin/env python3 """Execute the actual room-topic transaction with filesystem faults and power cuts.""" from pathlib import Path import os import shutil import struct import subprocess import sys import tempfile import unittest import zlib ROOT = Path(__file__).resolve().parents[1] HARNESS = r''' #include #include #include #include #include #include #include #include #include #include #include #include struct PowerCut {}; class MemoryFS; static MemoryFS* metadata_filesystem = nullptr; class File { MemoryFS* fs = nullptr; std::shared_ptr> bytes; std::string path; size_t cursor = 0; bool writable = false, directory = false, valid = false; public: File() = default; File(MemoryFS*, std::shared_ptr>, const char*, bool, bool); operator bool() const { return valid; } bool isDirectory() const { return directory; } size_t size(); int read(uint8_t*, size_t); size_t write(const uint8_t*, size_t); void flush(); void close(); }; class MemoryFS { public: void _lockFS() {} void _unlockFS() {} MemoryFS* _getFS() { return this; } std::map>> files; std::set faults, directories; std::vector trace; size_t boundary = 0, cut_at = 0; bool short_write = false, short_read = false; std::vector replacement_on_close; unsigned write_opens = 0; void point(const std::string& event) { trace.push_back(event); if (++boundary == cut_at) throw PowerCut{}; } bool begin(const std::string& op) { point("before:" + op); return !faults.count(op); } void end(const std::string& op) { point("after:" + op); } bool exists(const char* path) { const std::string op = std::string("stat:") + path; const bool okay = begin(op); const bool present = files.count(path) || directories.count(path); end(op); return okay && present; } bool remove(const char* path) { const std::string op = std::string("remove:") + path; if (!begin(op)) return false; const bool removed = files.erase(path) + directories.erase(path) != 0; end(op); return removed; } bool rename(const char* from, const char* to) { const std::string op = std::string("rename:") + from + ":" + to; if (!begin(op) || !files.count(from) || files.count(to) || directories.count(to)) return false; files[to] = files[from]; files.erase(from); end(op); return true; } File open(const char* path, const char* mode = "r", bool create = false) { (void)create; const bool write = mode[0] == 'w'; const std::string op = std::string("open:") + (write ? "w:" : "r:") + path; if (!begin(op)) return {}; if (write) { ++write_opens; files[path] = std::make_shared>(); if (faults.count(std::string("directory:") + path)) directories.insert(path); } if (!files.count(path) && !directories.count(path)) { end(op); return {}; } if (!files.count(path)) files[path] = std::make_shared>(); File file(this, files[path], path, write, directories.count(path)); end(op); return file; } File open(const char* path, int mode) { assert(mode == 1); return open(path, "w"); } void put(const char* path, const std::vector& bytes) { files[path] = std::make_shared>(bytes); } std::vector get(const char* path) const { return *files.at(path); } void reset() { trace.clear(); boundary = cut_at = 0; faults.clear(); short_write = short_read = false; write_opens = 0; } }; File::File(MemoryFS* owner, std::shared_ptr> data, const char* name, bool write, bool dir) : fs(owner), bytes(data), path(name), writable(write), directory(dir), valid(true) {} size_t File::size() { const std::string op = "size:" + path; const bool okay = fs->begin(op); const size_t size = okay ? bytes->size() : 0; fs->end(op); return size; } int File::read(uint8_t* output, size_t length) { const std::string op = "read:" + path; if (!valid || directory || !fs->begin(op)) return -1; length = std::min(length, bytes->size() - cursor); if (fs->short_read && length) --length; if (length) memcpy(output, bytes->data() + cursor, length); cursor += length; fs->end(op); return static_cast(length); } size_t File::write(const uint8_t* data, size_t length) { const std::string op = "write:" + path; if (!valid || directory || !fs->begin(op)) return 0; if (fs->short_write && length) --length; bytes->insert(bytes->end(), data, data + length); fs->end(op); return length; } void File::flush() { const std::string op = "flush:" + path; if (!fs->begin(op) && !bytes->empty()) bytes->pop_back(); fs->end(op); } void File::close() { const std::string op = std::string("close:") + (writable ? "w:" : "r:") + path; if (!fs->begin(op) && writable && !bytes->empty()) (*bytes)[0] ^= 1; if (writable && !fs->replacement_on_close.empty()) *bytes = fs->replacement_on_close; valid = false; fs->end(op); } #define FILE_O_WRITE 1 #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) struct lfs_info {}; static constexpr int LFS_ERR_NOENT = -2; static int lfs_stat(MemoryFS* fs, const char* path, lfs_info*) { const std::string op = std::string("stat:") + path; const bool okay = fs->begin(op); const bool present = fs->files.count(path) || fs->directories.count(path); fs->end(op); return !okay ? -5 : present ? 0 : LFS_ERR_NOENT; } #endif #include static const char* P = mesh::ROOM_TOPIC_PRIMARY_PATH; static const char* T = mesh::ROOM_TOPIC_TEMP_PATH; static const char* B = mesh::ROOM_TOPIC_BACKUP_PATH; static const std::vector OLD = { @OLD_VECTOR@ }; static const std::vector NEW = { @NEW_VECTOR@ }; static unsigned scenarios = 0; static bool save(MemoryFS& fs, const char* text) { metadata_filesystem = &fs; return mesh::saveRoomTopic(&fs, text); } static bool load(MemoryFS& fs, char (&out)[152]) { metadata_filesystem = &fs; return mesh::loadRoomTopic(&fs, out); } static void loaded(MemoryFS& fs, const char* expected) { char text[152]; memset(text, 0x7f, sizeof(text)); assert(load(fs, text) && strcmp(text, expected) == 0); } static void checkFailedSave(MemoryFS& fs) { assert(!save(fs, "New topic")); fs.reset(); loaded(fs, "Old topic"); assert(fs.get(P) == OLD); assert(!fs.files.count(T) && !fs.files.count(B)); ++scenarios; } static void crc(std::vector& image) { uint32_t sum = 0xffffffffu; for (size_t i = 0; i < 156; ++i) { sum ^= image[i]; for (unsigned bit = 0; bit < 8; ++bit) sum = (sum >> 1) ^ ((sum & 1) ? 0xedb88320u : 0); } sum ^= 0xffffffffu; for (unsigned i = 0; i < 4; ++i) image[156 + i] = uint8_t(sum >> (8 * i)); } int main() { { MemoryFS fs; loaded(fs, ""); assert(save(fs, "Old topic")); assert(fs.get(P) == OLD); loaded(fs, "Old topic"); assert(save(fs, "New topic")); assert(fs.get(P) == NEW); loaded(fs, "New topic"); assert(save(fs, "")); loaded(fs, ""); assert(fs.files.count(P)); ++scenarios; } { MemoryFS fs; char maximum[152]; memset(maximum, 'x', 151); maximum[151] = 0; assert(save(fs, maximum)); loaded(fs, maximum); const auto before = fs.get(P); fs.reset(); char oversized[153]; memset(oversized, 'x', 152); oversized[152] = 0; assert(!save(fs, oversized) && !save(fs, nullptr) && fs.trace.empty()); assert(fs.get(P) == before); ++scenarios; } { char output[152]; memset(output, 7, sizeof(output)); assert(!mesh::loadRoomTopic(static_cast(nullptr), output)); assert(output[0] == 0 && !mesh::saveRoomTopic(static_cast(nullptr), "x")); ++scenarios; } std::vector> invalid = { {}, {1}, std::vector(159), std::vector(161), OLD }; invalid.back()[159] ^= 1; auto future = OLD; future[3] = 2; crc(future); invalid.push_back(future); auto magic = OLD; magic[0] = 'X'; crc(magic); invalid.push_back(magic); auto unterminated = OLD; std::fill(unterminated.begin() + 4, unterminated.begin() + 156, 'x'); crc(unterminated); invalid.push_back(unterminated); auto padding = OLD; padding[100] = 'x'; crc(padding); invalid.push_back(padding); for (const auto& bytes : invalid) { MemoryFS fs; fs.put(P, bytes); fs.put(B, OLD); fs.put(T, NEW); char text[152]; assert(!load(fs, text) && text[0] == 0); assert(!save(fs, "New topic")); assert(fs.get(P) == bytes && fs.get(B) == OLD && fs.get(T) == NEW); assert(fs.write_opens == 0); ++scenarios; MemoryFS only_backup; only_backup.put(B, bytes); only_backup.put(T, NEW); assert(!load(only_backup, text) && !save(only_backup, "New topic")); assert(!only_backup.files.count(P) && only_backup.get(B) == bytes && only_backup.get(T) == NEW); ++scenarios; } for (size_t position = 0; position < OLD.size(); ++position) { MemoryFS fs; auto corrupted = OLD; corrupted[position] ^= 1; fs.put(P, corrupted); fs.put(B, NEW); fs.put(T, NEW); char text[152]; memset(text, 0x7f, sizeof(text)); assert(!load(fs, text)); assert(std::all_of(text, text + sizeof(text), [](char value) { return value == 0; })); assert(!save(fs, "New topic") && fs.get(P) == corrupted); assert(fs.get(B) == NEW && fs.get(T) == NEW && fs.write_opens == 0); ++scenarios; } { MemoryFS fs; fs.put(P, NEW); fs.put(B, OLD); fs.put(T, OLD); loaded(fs, "New topic"); assert(!fs.files.count(B) && !fs.files.count(T)); ++scenarios; } { MemoryFS fs; fs.put(B, OLD); fs.put(T, NEW); loaded(fs, "Old topic"); assert(fs.get(P) == OLD && !fs.files.count(T)); ++scenarios; } { MemoryFS fs; fs.put(T, NEW); loaded(fs, ""); assert(!fs.files.count(P) && !fs.files.count(T)); ++scenarios; } { MemoryFS fs; fs.put(B, OLD); fs.faults.insert(std::string("rename:") + B + ":" + P); char text[152]; assert(!load(fs, text) && !save(fs, "New topic")); assert(fs.get(B) == OLD && !fs.files.count(P)); ++scenarios; } for (const std::string& fault : {std::string("open:w:") + T, std::string("write:") + T, std::string("flush:") + T, std::string("close:w:") + T, std::string("open:r:") + T, std::string("read:") + T, std::string("size:") + T, std::string("rename:") + P + ":" + B, std::string("rename:") + T + ":" + P, std::string("directory:") + T}) { MemoryFS fs; fs.put(P, OLD); fs.faults.insert(fault); checkFailedSave(fs); } { MemoryFS fs; fs.put(P, OLD); fs.short_write = true; checkFailedSave(fs); } { MemoryFS fs; fs.put(P, OLD); fs.short_read = true; checkFailedSave(fs); } { MemoryFS fs; fs.put(P, OLD); fs.replacement_on_close = OLD; // A valid but stale image must fail exact readback, not pass on CRC alone. checkFailedSave(fs); } { MemoryFS fs; fs.put(P, OLD); fs.faults.insert(std::string("rename:") + T + ":" + P); fs.faults.insert(std::string("rename:") + B + ":" + P); fs.faults.insert(std::string("remove:") + T); assert(!save(fs, "New topic")); assert(!fs.files.count(P) && fs.get(B) == OLD && fs.get(T) == NEW); fs.reset(); loaded(fs, "Old topic"); assert(fs.get(P) == OLD); ++scenarios; } { MemoryFS fs; fs.faults.insert(std::string("rename:") + T + ":" + P); fs.faults.insert(std::string("remove:") + T); assert(!save(fs, "New topic")); fs.reset(); loaded(fs, ""); assert(!fs.files.count(P)); ++scenarios; } for (const char* artifact : {B, T}) { MemoryFS fs; fs.put(P, NEW); fs.put(artifact, OLD); fs.faults.insert(std::string("remove:") + artifact); loaded(fs, "New topic"); const unsigned writes = fs.write_opens; assert(!save(fs, "Old topic") && fs.write_opens == writes && fs.get(P) == NEW); fs.reset(); assert(save(fs, "Old topic")); loaded(fs, "Old topic"); ++scenarios; } { MemoryFS fs; fs.put(P, OLD); fs.faults.insert(std::string("remove:") + B); assert(save(fs, "New topic") && fs.get(P) == NEW && fs.get(B) == OLD); loaded(fs, "New topic"); assert(!save(fs, "Old topic")); fs.reset(); loaded(fs, "New topic"); ++scenarios; } for (const char* path : {P, B}) { MemoryFS fs; fs.put(path, OLD); fs.faults.insert(std::string("open:r:") + path); char text[152]; assert(!load(fs, text) && !save(fs, "New topic")); assert(fs.get(path) == OLD && fs.write_opens == 0); ++scenarios; MemoryFS directory; directory.put(path, OLD); directory.directories.insert(path); assert(!load(directory, text) && !save(directory, "New topic")); assert(directory.get(path) == OLD); ++scenarios; } #if defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) for (bool present : {false, true}) { MemoryFS fs; if (present) fs.put(P, OLD); fs.faults.insert(std::string("stat:") + P); char text[152]; assert(!load(fs, text) && !save(fs, "New topic") && fs.write_opens == 0); assert(fs.files.count(P) == size_t(present)); ++scenarios; } { MemoryFS fs; fs.put(P, OLD); fs.put(B, NEW); fs.faults.insert(std::string("stat:") + B); loaded(fs, "Old topic"); assert(!save(fs, "New topic") && fs.get(P) == OLD); ++scenarios; } #endif // Cut power before and after every filesystem boundary, including all read // verification operations. Reboot must select exactly old or committed new. for (bool existing : {false, true}) { MemoryFS baseline; if (existing) baseline.put(P, OLD); assert(save(baseline, "New topic")); const size_t boundaries = baseline.boundary; for (size_t cut = 1; cut <= boundaries; ++cut) { MemoryFS fs; if (existing) fs.put(P, OLD); fs.cut_at = cut; try { save(fs, "New topic"); } catch (const PowerCut&) {} const bool committed = fs.files.count(P) && fs.get(P) == NEW; fs.reset(); loaded(fs, committed ? "New topic" : (existing ? "Old topic" : "")); assert(!fs.files.count(T) && !fs.files.count(B)); if (committed || existing) assert(fs.get(P) == (committed ? NEW : OLD)); else assert(!fs.files.count(P)); ++scenarios; } } printf("PASS: %u room topic persistence and power-cut scenarios\n", scenarios); } ''' STAT_MOCK = r''' #pragma once struct stat {}; inline int stat(const char* absolute, struct stat*) { if (!metadata_filesystem || strncmp(absolute, "/spiffs", 7) != 0) { errno = EIO; return -1; } const char* path = absolute + 7; const std::string op = std::string("stat:") + path; const bool okay = metadata_filesystem->begin(op); const bool present = metadata_filesystem->files.count(path) || metadata_filesystem->directories.count(path); metadata_filesystem->end(op); if (!okay) { errno = EIO; return -1; } if (!present) { errno = ENOENT; return -1; } return 0; } ''' def image(text): payload = b"RTP\x01" + text.encode().ljust(152, b"\x00") return payload + struct.pack("