#include #include #include #include #include #include #include #include #include #include // Model the pinned FILE* buffering boundary, not NOR page operations or time. // The production transaction and CRC implementation are compiled below. class FileSystem; struct Stream { FileSystem* fs; std::string path; bool writing; size_t buffer_size = 4096, position = 0, backend_position = 0, cursor = 0; std::vector buffer; }; class File { std::shared_ptr stream; public: File() = default; File(FileSystem* fs, const char* path, bool writing) : stream(std::make_shared(Stream{fs, path, writing})) {} explicit operator bool() const { return bool(stream); } bool setBufferSize(size_t); size_t size() const; size_t read(uint8_t*, size_t); size_t write(const uint8_t*, size_t); void flush(); void close() { flush(); stream.reset(); } }; class FileSystem { public: using Files = std::map>; Files files; unsigned reads = 0, writes = 0, backend_reads = 0, backend_writes = 0; unsigned config_calls = 0, closes = 0, renames = 0; unsigned fail_config = 0, short_read_call = 0, fail_rename = 0; size_t largest_read = 0, largest_backend_read = 0, max_write = SIZE_MAX; size_t backend_read_bytes = 0, reader_buffer_size = 0; bool fail_open_read = false, fail_open_write = false; std::vector rename_snapshots; bool exists(const char* path) { return files.count(path) != 0; } bool remove(const char* path) { return files.erase(path) != 0; } bool rename(const char* from, const char* to) { ++renames; if (renames == fail_rename || !exists(from) || exists(to)) return false; files[to] = files.at(from); files.erase(from); rename_snapshots.push_back(files); return true; } File open(const char* path, const char* mode, bool = false) { const bool writing = *mode == 'w'; if (writing ? fail_open_write : fail_open_read || !exists(path)) return File(); if (writing) files[path].clear(); return File(this, path, writing); } }; bool File::setBufferSize(size_t size) { if (!stream) return false; if (++stream->fs->config_calls == stream->fs->fail_config) return false; assert(stream->buffer.empty() && stream->position == 0); stream->buffer_size = size; if (!stream->writing) stream->fs->reader_buffer_size = size; return true; } size_t File::size() const { return stream ? stream->fs->files.at(stream->path).size() : 0; } void File::flush() { if (!stream || !stream->writing || stream->buffer.empty()) return; auto& fs = *stream->fs; ++fs.backend_writes; auto& target = fs.files.at(stream->path); target.insert(target.end(), stream->buffer.begin(), stream->buffer.end()); stream->buffer.clear(); } size_t File::write(const uint8_t* data, size_t length) { if (!stream || !stream->writing) return 0; auto& fs = *stream->fs; ++fs.writes; const size_t accepted = std::min(length, fs.max_write); size_t copied = 0; while (copied < accepted) { const size_t n = std::min(accepted - copied, stream->buffer_size - stream->buffer.size()); stream->buffer.insert(stream->buffer.end(), data + copied, data + copied + n); copied += n; if (stream->buffer.size() == stream->buffer_size) flush(); } return accepted; } size_t File::read(uint8_t* data, size_t length) { if (!stream || stream->writing) return 0; auto& fs = *stream->fs; ++fs.reads; fs.largest_read = std::max(fs.largest_read, length); if (fs.reads == fs.short_read_call) --length; const auto& source = fs.files.at(stream->path); size_t copied = 0; while (copied < length) { if (stream->cursor == stream->buffer.size()) { const size_t available = source.size() - std::min(source.size(), stream->backend_position); const size_t n = std::min(stream->buffer_size, available); if (!n) break; ++fs.backend_reads; fs.backend_read_bytes += n; fs.largest_backend_read = std::max(fs.largest_backend_read, n); stream->buffer.assign(source.begin() + stream->backend_position, source.begin() + stream->backend_position + n); stream->backend_position += n; stream->cursor = 0; } const size_t n = std::min(length - copied, stream->buffer.size() - stream->cursor); memcpy(data + copied, stream->buffer.data() + stream->cursor, n); stream->cursor += n; copied += n; } stream->position += copied; return copied; } #define FILESYSTEM FileSystem unsigned fixture_commit_calls = 0; #include "transaction_under_test.h" using Writer = mesh::ContactFileTransaction; using Progress = Writer::CommitProgress; std::vector image(size_t bytes) { std::vector result(bytes); for (size_t i = 0; i < bytes; ++i) result[i] = (i * 37 + i / 251) & 255; return result; } void writeImage(Writer& writer, const std::vector& bytes) { for (size_t offset = 0; offset < bytes.size();) { const size_t count = std::min(size_t(152), bytes.size() - offset); assert(writer.write(bytes.data() + offset, count) == count); offset += count; } } unsigned effectiveBudget(unsigned requested) { #if defined(ESP32_PLATFORM) return std::max(1U, std::min(8U, requested)); #else (void)requested; return 1; #endif } struct Measurement { unsigned passes, logical_reads, backend_reads, max_pass_reads, max_pass_refills; size_t max_pass_backend_bytes, reader_buffer_size; }; Measurement verify(size_t bytes, unsigned budget, bool use_default = false) { FileSystem fs; const auto old = image(21), next = image(bytes); fs.files["/contacts3"] = old; Writer writer(&fs, "/contacts3"); assert(writer); writeImage(writer, next); unsigned passes = 0, max_reads = 0, max_refills = 0, data_passes = 0; size_t max_backend_bytes = 0; #if defined(ESP32_PLATFORM) // Selection is made once in Open, independently of the later read loop. const size_t selected = !use_default && budget >= 8 ? 512 : 128; #endif Progress result = Progress::Pending; while (result == Progress::Pending) { const unsigned before_reads = fs.reads, before_backend = fs.backend_reads; const size_t before_backend_bytes = fs.backend_read_bytes; const bool was_ready = writer.readyToPublish(); result = use_default ? writer.serviceCommit() : writer.serviceCommit(true, budget); assert(++passes < 10000); const unsigned reads = fs.reads - before_reads, refills = fs.backend_reads - before_backend; max_reads = std::max(max_reads, reads); max_refills = std::max(max_refills, refills); const size_t backend_bytes = fs.backend_read_bytes - before_backend_bytes; max_backend_bytes = std::max(max_backend_bytes, backend_bytes); const unsigned bound = use_default ? 1 : effectiveBudget(budget); assert(reads <= bound); #if defined(ESP32_PLATFORM) assert(refills <= (64 * bound + selected - 1) / selected); assert(backend_bytes <= 512); // FILE refill admissions, not NOR operations or time #endif if (reads) { ++data_passes; assert(!writer.readyToPublish()); // close/CRC comparison is a later pass } if (result == Progress::Pending) assert(fs.files.at("/contacts3") == old); else assert(was_ready); // publication never shares a CRC read pass } assert(result == Progress::Succeeded); const unsigned bound = use_default ? 1 : effectiveBudget(budget); assert(data_passes == (bytes + 64 * bound - 1) / (64 * bound)); assert(passes == data_passes + 4); assert(fs.files.at("/contacts3") == next); assert(fs.reads == (bytes + 63) / 64); assert(fs.largest_read <= 64); #if defined(ESP32_PLATFORM) assert(fs.reader_buffer_size == selected); assert(fs.largest_backend_read <= selected); assert(fs.backend_reads == (bytes + selected - 1) / selected); #endif return {passes, fs.reads, fs.backend_reads, max_reads, max_refills, max_backend_bytes, fs.reader_buffer_size}; } void limitsAndTails() { for (size_t bytes : {size_t(0), size_t(1), size_t(63), size_t(64), size_t(65), size_t(127), size_t(128), size_t(129), size_t(511), size_t(512), size_t(513), size_t(152 * 347)}) { verify(bytes, 1, true); for (unsigned budget : {0U, 1U, 2U, 8U, 9U, UINT32_MAX}) verify(bytes, budget); } FileSystem fs; Writer writer(&fs, "/sync"); writeImage(writer, image(347 * 152)); fixture_commit_calls = 0; assert(writer.commit()); assert(fixture_commit_calls == 829); // actual commit() drains default-one passes #if defined(ESP32_PLATFORM) assert(fs.reader_buffer_size == 128 && fs.backend_reads == 413); #endif } void readFailureAndCorruption() { const auto old = image(19), next = image(1536); for (unsigned failed_read = 1; failed_read <= 16; ++failed_read) { FileSystem fs; fs.files["/contacts3"] = old; Writer writer(&fs, "/contacts3"); writeImage(writer, next); fs.short_read_call = failed_read; Progress result = Progress::Pending; for (unsigned i = 0; result == Progress::Pending && i < 100; ++i) result = writer.serviceCommit(true, 8); assert(result == Progress::Failed); assert(fs.files.at("/contacts3") == old && !fs.exists("/contacts3.tmp")); } for (unsigned corruption : {0U, 63U, 64U, 127U, 511U, 512U, 1535U}) { FileSystem fs; fs.files["/contacts3"] = old; Writer writer(&fs, "/contacts3"); writeImage(writer, next); assert(writer.serviceCommit(true, 8) == Progress::Pending); // flush fs.files.at("/contacts3.tmp")[corruption] ^= 1; Progress result = Progress::Pending; while (result == Progress::Pending) result = writer.serviceCommit(true, 8); assert(result == Progress::Failed && fs.files.at("/contacts3") == old); } } void cancellationAndCrash() { const auto old = image(19), next = image(1536); for (unsigned stop = 0; stop < 8; ++stop) { FileSystem fs; fs.files["/contacts3"] = old; Writer writer(&fs, "/contacts3"); writeImage(writer, next); for (unsigned i = 0; i < stop; ++i) { const auto result = writer.serviceCommit(true, 8); if (result == Progress::Succeeded) break; } if (fs.files.at("/contacts3") == old) { const unsigned reads = fs.reads; assert(writer.serviceCommit(false, 8) == Progress::Failed); assert(fs.reads == reads && !fs.exists("/contacts3.tmp")); } // Simulate boot from the durable map before any destructor can run. FileSystem reboot; reboot.files = fs.files; assert(Writer::recover(&reboot, "/contacts3")); assert(reboot.files.at("/contacts3") == old || reboot.files.at("/contacts3") == next); } for (unsigned failure : {0U, 1U, 2U, 3U}) { FileSystem fs; fs.files["/contacts3"] = old; Writer writer(&fs, "/contacts3"); writeImage(writer, next); fs.fail_rename = failure; Progress result = Progress::Pending; while (result == Progress::Pending) result = writer.serviceCommit(true, 8); assert(result == (failure == 1 || failure == 2 ? Progress::Failed : Progress::Succeeded)); assert(fs.files.at("/contacts3") == (result == Progress::Succeeded ? next : old)); for (const auto& snapshot : fs.rename_snapshots) { FileSystem reboot; reboot.files = snapshot; assert(Writer::recover(&reboot, "/contacts3")); assert(reboot.files.at("/contacts3") == old || reboot.files.at("/contacts3") == next); } } } void setupFailures() { for (unsigned fail_config : {1U, 2U}) { FileSystem fs; fs.files["/contacts3"] = image(19); fs.fail_config = fail_config; Writer writer(&fs, "/contacts3"); if (fail_config == 1) { assert(!writer); continue; } writeImage(writer, image(512)); assert(writer.serviceCommit(true, 8) == Progress::Pending); assert(writer.serviceCommit(true, 8) == Progress::Failed); assert(fs.reads == 0 && fs.files.at("/contacts3") == image(19)); } FileSystem fs; fs.files["/contacts3"] = image(19); Writer writer(&fs, "/contacts3"); writeImage(writer, image(512)); fs.fail_open_read = true; assert(writer.serviceCommit(true, 8) == Progress::Pending); assert(writer.serviceCommit(true, 8) == Progress::Failed); assert(fs.reads == 0 && fs.files.at("/contacts3") == image(19)); } int main() { limitsAndTails(); readFailureAndCorruption(); cancellationAndCrash(); #if defined(ESP32_PLATFORM) setupFailures(); #endif const auto one = verify(347 * 152, 1), eight = verify(347 * 152, 8); printf("{\"one_passes\":%u,\"eight_passes\":%u,\"logical_reads\":%u," "\"backend_reads\":%u,\"max_pass_reads\":%u,\"max_pass_refills\":%u," "\"max_pass_backend_bytes\":%zu,\"reader_buffer_size\":%zu," "\"one_backend_reads\":%u,\"one_reader_buffer_size\":%zu}\n", one.passes, eight.passes, eight.logical_reads, eight.backend_reads, eight.max_pass_reads, eight.max_pass_refills, eight.max_pass_backend_bytes, eight.reader_buffer_size, one.backend_reads, one.reader_buffer_size); }