feat(lofs): exclusive /lofs mount with LoLog

Single LoLog slot on QSPI or nRF flash2; sd/lofs/extra/internal order.
Host lolog tests; drop qspi/spare/reserve mount names.
This commit is contained in:
Ben Allfree committed 2026-10-07 16:53:38 -07:00
1 parent 1243b2bc1b
commit 22c8567c4f
34 files changed
+3117 -106

No files matched your search

+3
View File
@@ -26,3 +26,6 @@ build_src_filter =
+<LoBBSConfig.h>
+<LoBBSStackGuard.cpp>
+<LoBBSStackGuard.h>
+<lofs/lolog/**>
+<lofs/volumes/LoFSRawDevice.cpp>
+<lofs/volumes/LoFSRawDevice.h>
+19
View File
@@ -0,0 +1,19 @@
# LoLog host tests
Run from repo root:
```bash
cd ci/host && pio test -e host -f test_lolog_host
```
Coverage (Unity):
| Area | Tests |
|------|--------|
| Boot / mount | Empty format, erased chip, pure DATA replay, reboot |
| File ops | Nested mkdir, overwrite, rename, rmdir, prefix names, 2.5 KB EXTENT file, writeAt, stat |
| Crash / ACID | Fault on every prog byte (large commit), every prog call, torn tail + append, CRC poke, partial mkdir group |
| Index / clean | Index flush remount, bulk flush, tomb shadow, delete when full, cleaner churn, headroom |
| Fuzz | 30 rounds random ops + crash injection vs ref model |
Fault injection uses `LoLogRamDevice` (`setFaultAfterProgBytes`, `setFaultOnProgCall`, `poke`).
@@ -0,0 +1,63 @@
#pragma once
#include "lofs/lolog/LoLog.h"
#include "lofs/lolog/LoLogRamDevice.h"
#include <cstring>
#include <string>
#include <unity.h>
#include <vector>
inline bool lologWriteFile(LoLog &log, const char *path, const uint8_t *data, size_t len)
{
LoLog::OpenHandle *h = log.open(path, true, true);
if (!h)
return false;
size_t off = 0;
while (off < len) {
size_t n = log.writeHandle(h, data + off, len - off);
if (n == 0)
break;
off += n;
}
log.flushHandle(h);
log.closeHandle(h);
return off == len;
}
inline bool lologReadFile(LoLog &log, const char *path, std::vector<uint8_t> &out)
{
LoLog::OpenHandle *h = log.open(path, false, false);
if (!h)
return false;
out.clear();
uint8_t buf[256];
for (;;) {
size_t n = log.readHandle(h, buf, sizeof(buf));
if (n == 0)
break;
out.insert(out.end(), buf, buf + n);
}
log.closeHandle(h);
return true;
}
inline int lologCountDir(LoLog &log, const char *dirPath)
{
LoLog::OpenHandle *dir = log.openDir(dirPath);
if (!dir)
return -1;
int n = 0;
while (LoLog::OpenHandle *e = log.openNextInDir(dir)) {
log.closeHandle(e);
n++;
}
log.closeHandle(dir);
return n;
}
/** Fresh chip: all 0xFF, mount without explicit format. */
inline bool lologMountErasedChip(LoLogRamDevice &dev, LoLog &log)
{
dev.reset();
return log.mount();
}
+84
View File
@@ -0,0 +1,84 @@
#pragma once
#include "lolog_test_helpers.hpp"
#include <map>
#include <set>
#include <string>
#include <vector>
/** In-memory reference tree for fuzz / crash oracle (paths without leading slash). */
class RefFs {
public:
bool mkdir(const std::string &path)
{
if (path.empty() || files.count(path))
return false;
dirs.insert(path);
return true;
}
bool write(const std::string &path, const std::vector<uint8_t> &data)
{
if (path.empty())
return false;
files[path] = data;
return true;
}
bool unlink(const std::string &path) { return files.erase(path) > 0; }
bool rename(const std::string &from, const std::string &to)
{
auto it = files.find(from);
if (it == files.end())
return false;
if (files.count(to))
return false;
files[to] = it->second;
files.erase(it);
return true;
}
bool existsFile(const std::string &path) const { return files.count(path) > 0; }
const std::map<std::string, std::vector<uint8_t>> &allFiles() const { return files; }
void clear()
{
files.clear();
dirs.clear();
}
private:
std::map<std::string, std::vector<uint8_t>> files;
std::set<std::string> dirs;
};
inline bool lologMatchesRef(LoLog &log, const RefFs &ref)
{
for (const auto &kv : ref.allFiles()) {
if (!log.exists(kv.first.c_str()))
return false;
std::vector<uint8_t> got;
if (!lologReadFile(log, kv.first.c_str(), got))
return false;
if (got != kv.second)
return false;
}
for (const auto &kv : ref.allFiles()) {
if (log.exists(kv.first.c_str()) && !ref.existsFile(kv.first))
return false;
}
std::vector<std::string> logPaths;
for (int i = 0; i < 512; i++) {
char p[32];
snprintf(p, sizeof(p), "fuzz_%d.dat", i);
if (log.exists(p))
logPaths.push_back(p);
}
for (const auto &p : logPaths) {
if (!ref.existsFile(p))
return false;
}
return true;
}
@@ -0,0 +1,41 @@
#include "lolog_test_helpers.hpp"
#include <unity.h>
extern LoLogRamDevice gDev;
extern LoLog gLog;
void test_mount_empty_after_format(void)
{
TEST_ASSERT_FALSE(gLog.exists("anything"));
TEST_ASSERT_TRUE(gLog.isDirectory(""));
}
void test_mount_erased_chip_without_format(void)
{
LoLogRamDevice dev(16);
LoLog log(dev);
TEST_ASSERT_TRUE(lologMountErasedChip(dev, log));
}
void test_pure_replay_no_index_runs(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("replay"));
std::vector<uint8_t> payload = {'a', 'b', 'c'};
TEST_ASSERT_TRUE(lologWriteFile(gLog, "replay/r1.ls", payload.data(), payload.size()));
LoLog g2(gDev);
TEST_ASSERT_TRUE(g2.mount());
std::vector<uint8_t> readback;
TEST_ASSERT_TRUE(lologReadFile(g2, "replay/r1.ls", readback));
TEST_ASSERT_EQUAL_UINT32(payload.size(), readback.size());
}
void test_mount_after_reboot_simulation(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("boot"));
TEST_ASSERT_TRUE(lologWriteFile(gLog, "boot/persist.ls", (const uint8_t *)"ok", 2));
LoLog after(gDev);
TEST_ASSERT_TRUE(after.mount());
TEST_ASSERT_TRUE(after.exists("boot/persist.ls"));
}
+235
View File
@@ -0,0 +1,235 @@
#include "lolog_test_helpers.hpp"
#include "lofs/lolog/LoLogConfig.h"
#include <unity.h>
extern LoLogRamDevice gDev;
extern LoLog gLog;
static uint32_t progCallsForLargeCommit(LoLogRamDevice &dev)
{
dev.reset();
LoLog log(dev);
log.format();
log.mkdir("gc");
LoLog::OpenHandle *h = log.open("gc/big.bin", true, true);
if (!h)
return 0;
dev.clearProgCallCount();
std::vector<uint8_t> payload(2500, 0x5A);
log.writeHandle(h, payload.data(), payload.size());
log.flushHandle(h);
log.closeHandle(h);
return dev.progCallCount();
}
static bool groupVisibleAfterRemount(LoLogRamDevice &dev, const char *path, size_t expectLen)
{
LoLog log(dev);
if (!log.mount())
return false;
if (!log.exists(path))
return expectLen == 0;
std::vector<uint8_t> got;
if (!lologReadFile(log, path, got))
return false;
return got.size() == expectLen;
}
void test_commit_group_atomic_each_prog_call(void)
{
const uint32_t calls = progCallsForLargeCommit(gDev);
TEST_ASSERT_GREATER_THAN(1, calls);
for (uint32_t fault = 1; fault <= calls; fault++) {
gDev.reset();
LoLog log(gDev);
log.format();
TEST_ASSERT_TRUE(log.mkdir("gc"));
gDev.setFaultOnProgCall(fault);
LoLog::OpenHandle *h = log.open("gc/big.bin", true, true);
if (h) {
std::vector<uint8_t> payload(2500, 0x5A);
log.writeHandle(h, payload.data(), payload.size());
log.flushHandle(h);
log.closeHandle(h);
}
gDev.setFaultOnProgCall(0);
const bool full = groupVisibleAfterRemount(gDev, "gc/big.bin", 2500);
if (!full) {
LoLog absent(gDev);
absent.mount();
if (absent.exists("gc/big.bin")) {
std::vector<uint8_t> got;
lologReadFile(absent, "gc/big.bin", got);
TEST_ASSERT_EQUAL_UINT32(0, got.size());
}
} else {
LoLog check(gDev);
check.mount();
std::vector<uint8_t> got;
lologReadFile(check, "gc/big.bin", got);
for (uint8_t b : got)
TEST_ASSERT_EQUAL_UINT8(0x5A, b);
}
}
}
void test_commit_group_partial_never_applied(void)
{
gDev.reset();
LoLog log(gDev);
log.format();
TEST_ASSERT_TRUE(log.mkdir("p"));
gDev.setFaultOnProgCall(1);
TEST_ASSERT_FALSE(log.mkdir("p/sub"));
gDev.setFaultOnProgCall(0);
LoLog rem(gDev);
rem.mount();
TEST_ASSERT_FALSE(rem.exists("p/sub"));
}
void test_commit_group_each_prog_byte(void)
{
LoLogRamDevice measure(32);
measure.reset();
LoLog mlog(measure);
mlog.format();
mlog.mkdir("by");
measure.clearProgBytes();
std::vector<uint8_t> payload(2500, 0x11);
LoLog::OpenHandle *mh = mlog.open("by/big.bin", true, true);
TEST_ASSERT_NOT_NULL(mh);
mlog.writeHandle(mh, payload.data(), payload.size());
mlog.flushHandle(mh);
mlog.closeHandle(mh);
const uint64_t totalBytes = measure.progBytes();
TEST_ASSERT_GREATER_THAN(8, totalBytes);
for (uint64_t cut = 1; cut <= totalBytes; cut++) {
measure.reset();
LoLog l(measure);
l.format();
l.mkdir("by");
measure.setFaultAfterProgBytes((uint32_t)cut);
LoLog::OpenHandle *lh = l.open("by/big.bin", true, true);
if (lh) {
l.writeHandle(lh, payload.data(), payload.size());
l.flushHandle(lh);
l.closeHandle(lh);
}
measure.setFaultAfterProgBytes(0);
const bool full = groupVisibleAfterRemount(measure, "by/big.bin", 2500);
if (full) {
LoLog check(measure);
check.mount();
std::vector<uint8_t> got;
lologReadFile(check, "by/big.bin", got);
TEST_ASSERT_EQUAL_UINT32(2500, got.size());
} else {
LoLog absent(measure);
absent.mount();
if (absent.exists("by/big.bin")) {
std::vector<uint8_t> got;
lologReadFile(absent, "by/big.bin", got);
TEST_ASSERT_EQUAL_UINT32(0, got.size());
}
}
}
}
void test_torn_tail_then_append(void)
{
gDev.reset();
LoLog log(gDev);
log.format();
TEST_ASSERT_TRUE(log.mkdir("torn"));
TEST_ASSERT_TRUE(lologWriteFile(log, "torn/a.ls", (const uint8_t *)"AAAA", 4));
gDev.setFaultAfterProgBytes(12);
LoLog::OpenHandle *h = log.open("torn/b.ls", true, true);
if (h) {
log.writeHandle(h, (const uint8_t *)"BBBBBBBB", 8);
log.flushHandle(h);
log.closeHandle(h);
}
gDev.setFaultAfterProgBytes(0);
{
LoLog mid(gDev);
TEST_ASSERT_TRUE(mid.mount());
TEST_ASSERT_TRUE(mid.exists("torn/a.ls"));
if (mid.exists("torn/b.ls")) {
std::vector<uint8_t> shell;
lologReadFile(mid, "torn/b.ls", shell);
TEST_ASSERT_EQUAL_UINT32(0, shell.size());
}
}
LoLog fin(gDev);
TEST_ASSERT_TRUE(fin.mount());
TEST_ASSERT_TRUE(lologWriteFile(fin, "torn/c.ls", (const uint8_t *)"CCCC", 4));
std::vector<uint8_t> finRead;
TEST_ASSERT_TRUE(lologReadFile(fin, "torn/c.ls", finRead));
TEST_ASSERT_EQUAL_UINT32(4, finRead.size());
TEST_ASSERT_TRUE(fin.exists("torn/a.ls"));
}
static void corruptSecondEntryCrc(LoLogRamDevice &dev)
{
for (uint32_t s = 0; s < dev.sectorCount(); s++) {
uint32_t off = lolog::kHeaderSize;
int ent = 0;
while (off + 8 <= lolog::kSectorSize) {
uint8_t hdr[8];
if (!dev.read(s * lolog::kSectorSize + off, hdr, 8))
break;
uint16_t plen = (uint16_t)hdr[0] | ((uint16_t)hdr[1] << 8);
if (plen == lolog::kEntryEnd)
break;
const uint32_t padded = (8u + plen + 3u) & ~3u;
if (hdr[2] == lolog::kEntFile) {
ent++;
if (ent == 2 && plen > 0) {
dev.poke(s * lolog::kSectorSize + off + 4 + plen, 0x00);
return;
}
}
off += padded;
}
}
}
void test_torn_entry_crc_poke(void)
{
gDev.reset();
LoLog log(gDev);
log.format();
TEST_ASSERT_TRUE(log.mkdir("poke"));
TEST_ASSERT_TRUE(lologWriteFile(log, "poke/ok.ls", (const uint8_t *)"good", 4));
TEST_ASSERT_TRUE(lologWriteFile(log, "poke/next.ls", (const uint8_t *)"bad", 3));
corruptSecondEntryCrc(gDev);
LoLog rem(gDev);
TEST_ASSERT_TRUE(rem.mount());
TEST_ASSERT_TRUE(rem.exists("poke/ok.ls"));
TEST_ASSERT_FALSE(rem.exists("poke/next.ls"));
TEST_ASSERT_TRUE(lologWriteFile(rem, "poke/after.ls", (const uint8_t *)"yes", 3));
TEST_ASSERT_TRUE(rem.exists("poke/after.ls"));
}
void test_fault_single_byte_discards_group(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("g"));
gDev.setFaultAfterProgBytes(1);
LoLog::OpenHandle *h = gLog.open("g/a.ls", true, true);
if (h) {
gLog.writeHandle(h, (const uint8_t *)"x", 1);
gLog.flushHandle(h);
gLog.closeHandle(h);
}
gDev.setFaultAfterProgBytes(0);
LoLog g2(gDev);
TEST_ASSERT_TRUE(g2.mount());
TEST_ASSERT_FALSE(g2.exists("g/a.ls"));
}
@@ -0,0 +1,104 @@
#include "lolog_test_helpers.hpp"
#include <string>
#include <unity.h>
extern LoLogRamDevice gDev;
extern LoLog gLog;
void test_mkdir_nested_and_overwrite(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("a/b/c"));
TEST_ASSERT_TRUE(gLog.isDirectory("a"));
TEST_ASSERT_TRUE(gLog.isDirectory("a/b"));
TEST_ASSERT_TRUE(gLog.isDirectory("a/b/c"));
const uint8_t v1[] = "one";
const uint8_t v2[] = "two-longer";
TEST_ASSERT_TRUE(lologWriteFile(gLog, "a/b/c/f.ls", v1, sizeof(v1)));
TEST_ASSERT_TRUE(lologWriteFile(gLog, "a/b/c/f.ls", v2, sizeof(v2)));
std::vector<uint8_t> got;
TEST_ASSERT_TRUE(lologReadFile(gLog, "a/b/c/f.ls", got));
TEST_ASSERT_EQUAL_UINT32(sizeof(v2), got.size());
}
void test_rename_file(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("rn"));
TEST_ASSERT_TRUE(lologWriteFile(gLog, "rn/old.ls", (const uint8_t *)"data", 4));
TEST_ASSERT_TRUE(gLog.rename("rn/old.ls", "rn/new.ls"));
TEST_ASSERT_FALSE(gLog.exists("rn/old.ls"));
TEST_ASSERT_TRUE(gLog.exists("rn/new.ls"));
}
void test_rmdir_nonempty_fails_nonempty_ok_recursive(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("rd"));
TEST_ASSERT_TRUE(lologWriteFile(gLog, "rd/x.ls", (const uint8_t *)"1", 1));
TEST_ASSERT_FALSE(gLog.rmdir("rd", false));
TEST_ASSERT_TRUE(gLog.rmdir("rd", true));
TEST_ASSERT_FALSE(gLog.exists("rd/x.ls"));
}
void test_prefix_collision_names(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("pfx"));
const std::string p1 = "pfx/abcdefgh_11111111.ls";
const std::string p2 = "pfx/abcdefgh_22222222.ls";
TEST_ASSERT_TRUE(lologWriteFile(gLog, p1.c_str(), (const uint8_t *)"A", 1));
TEST_ASSERT_TRUE(lologWriteFile(gLog, p2.c_str(), (const uint8_t *)"B", 1));
std::vector<uint8_t> a, b;
TEST_ASSERT_TRUE(lologReadFile(gLog, "pfx/abcdefgh_11111111.ls", a));
TEST_ASSERT_TRUE(lologReadFile(gLog, "pfx/abcdefgh_22222222.ls", b));
TEST_ASSERT_EQUAL_UINT8(0x41, a[0]);
TEST_ASSERT_EQUAL_UINT8(0x42, b[0]);
}
void test_large_file_inline_and_extent(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("big"));
std::vector<uint8_t> payload(2500);
for (size_t i = 0; i < payload.size(); i++)
payload[i] = (uint8_t)(i & 0xFF);
TEST_ASSERT_TRUE(lologWriteFile(gLog, "big/upload.bin", payload.data(), payload.size()));
LoLog g2(gDev);
TEST_ASSERT_TRUE(g2.mount());
std::vector<uint8_t> got;
TEST_ASSERT_TRUE(lologReadFile(g2, "big/upload.bin", got));
TEST_ASSERT_EQUAL_UINT32(payload.size(), got.size());
TEST_ASSERT_EQUAL_UINT8(0, got[0]);
TEST_ASSERT_EQUAL_UINT8(0xC3, got[2500 - 1]);
}
void test_large_file_write_at_path(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("up"));
LoLog::OpenHandle *h = gLog.open("up/chunk.bin", true, true);
TEST_ASSERT_NOT_NULL(h);
gLog.closeHandle(h);
uint8_t chunk[600];
for (size_t i = 0; i < sizeof(chunk); i++)
chunk[i] = (uint8_t)i;
TEST_ASSERT_TRUE(gLog.writeAtPath("up/chunk.bin", 0, chunk, sizeof(chunk)));
std::vector<uint8_t> got;
TEST_ASSERT_TRUE(lologReadFile(gLog, "up/chunk.bin", got));
TEST_ASSERT_EQUAL_UINT32(sizeof(chunk), got.size());
TEST_ASSERT_EQUAL_UINT8(0, got[0]);
TEST_ASSERT_EQUAL_UINT8(255, got[255]);
}
void test_stat_and_is_directory(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("st"));
TEST_ASSERT_TRUE(lologWriteFile(gLog, "st/f.ls", (const uint8_t *)"x", 1));
uint32_t sz = 0;
bool isDir = true;
TEST_ASSERT_TRUE(gLog.statPath("st/f.ls", &sz, &isDir));
TEST_ASSERT_FALSE(isDir);
TEST_ASSERT_EQUAL_UINT32(1, sz);
TEST_ASSERT_TRUE(gLog.statPath("st", &sz, &isDir));
TEST_ASSERT_TRUE(isDir);
}
@@ -0,0 +1,99 @@
#include "lolog_test_helpers.hpp"
#include "ref_fs.hpp"
#include <unity.h>
extern LoLogRamDevice gDev;
static uint32_t lfsRand(uint32_t &state)
{
state = state * 1664525u + 1013904223u;
return state;
}
static void applyRefOp(RefFs &ref, LoLog &log, uint32_t op, uint32_t &rng)
{
char path[48];
char path2[48];
switch (op % 5) {
case 0: {
snprintf(path, sizeof(path), "fuzz/d%d", (int)(lfsRand(rng) % 20));
if (log.mkdir(path))
ref.mkdir(path);
break;
}
case 1: {
snprintf(path, sizeof(path), "fuzz/f%d.dat", (int)(lfsRand(rng) % 40));
std::vector<uint8_t> data(8 + (lfsRand(rng) % 120));
for (size_t i = 0; i < data.size(); i++)
data[i] = (uint8_t)lfsRand(rng);
if (lologWriteFile(log, path, data.data(), data.size()))
ref.write(path, data);
break;
}
case 2: {
snprintf(path, sizeof(path), "fuzz/f%d.dat", (int)(lfsRand(rng) % 40));
if (log.remove(path))
ref.unlink(path);
break;
}
case 3: {
snprintf(path, sizeof(path), "fuzz/f%d.dat", (int)(lfsRand(rng) % 40));
snprintf(path2, sizeof(path2), "fuzz/t%d.dat", (int)(lfsRand(rng) % 40));
if (ref.existsFile(path) && !ref.existsFile(path2) && log.rename(path, path2))
ref.rename(path, path2);
break;
}
default:
log.maintain(5000);
break;
}
}
void test_random_ops_with_crash_injection(void)
{
RefFs ref;
uint32_t rng = 0xC0FFEE42u;
for (int round = 0; round < 30; round++) {
gDev.reset();
LoLog log(gDev);
log.format();
log.mkdir("fuzz");
ref.clear();
ref.mkdir("fuzz");
const int opsBeforeCrash = 3 + (int)(lfsRand(rng) % 12);
for (int i = 0; i < opsBeforeCrash; i++)
applyRefOp(ref, log, lfsRand(rng), rng);
gDev.setFaultOnProgCall(1 + (lfsRand(rng) % 8));
applyRefOp(ref, log, lfsRand(rng), rng);
gDev.setFaultOnProgCall(0);
LoLog rem(gDev);
TEST_ASSERT_TRUE(rem.mount());
for (const auto &kv : ref.allFiles()) {
if (!rem.exists(kv.first.c_str()))
continue;
std::vector<uint8_t> got;
if (!lologReadFile(rem, kv.first.c_str(), got))
continue;
if (got.size() != kv.second.size())
continue;
if (!got.empty())
TEST_ASSERT_EQUAL_UINT8_ARRAY(kv.second.data(), got.data(), got.size());
}
}
}
void test_directory_list_count(void)
{
gDev.reset();
LoLog log(gDev);
log.format();
TEST_ASSERT_TRUE(log.mkdir("d"));
TEST_ASSERT_TRUE(lologWriteFile(log, "d/one.ls", (const uint8_t *)"1", 1));
TEST_ASSERT_TRUE(lologWriteFile(log, "d/two.ls", (const uint8_t *)"2", 1));
TEST_ASSERT_EQUAL(2, lologCountDir(log, "d"));
}
@@ -0,0 +1,110 @@
#include "lolog_test_helpers.hpp"
#include "lofs/lolog/LoLogConfig.h"
#include <unity.h>
extern LoLogRamDevice gDev;
extern LoLog gLog;
void test_index_flush_pure_reboot(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("ix"));
TEST_ASSERT_TRUE(lologWriteFile(gLog, "ix/a.ls", (const uint8_t *)"z", 1));
gLog.maintain(60000);
LoLog g2(gDev);
TEST_ASSERT_TRUE(g2.mount());
TEST_ASSERT_TRUE(g2.exists("ix/a.ls"));
}
void test_index_multiple_flushes_and_lookup(void)
{
for (int i = 0; i < 90; i++) {
char dir[16];
snprintf(dir, sizeof(dir), "bulk%d", i);
TEST_ASSERT_TRUE(gLog.mkdir(dir));
char path[32];
snprintf(path, sizeof(path), "%s/r.ls", dir);
TEST_ASSERT_TRUE(lologWriteFile(gLog, path, (const uint8_t *)"x", 1));
gLog.maintain(60000);
}
LoLog g2(gDev);
TEST_ASSERT_TRUE(g2.mount());
TEST_ASSERT_TRUE(g2.exists("bulk42/r.ls"));
TEST_ASSERT_TRUE(g2.exists("bulk0/r.ls"));
}
void test_tombstone_shadow_after_remount(void)
{
TEST_ASSERT_TRUE(gLog.mkdir("ts"));
TEST_ASSERT_TRUE(lologWriteFile(gLog, "ts/gone.ls", (const uint8_t *)"1", 1));
TEST_ASSERT_TRUE(gLog.remove("ts/gone.ls"));
gLog.maintain(60000);
LoLog g2(gDev);
TEST_ASSERT_TRUE(g2.mount());
TEST_ASSERT_FALSE(g2.exists("ts/gone.ls"));
}
void test_delete_when_near_full(void)
{
LoLogRamDevice tiny(16);
LoLog log(tiny);
log.format();
TEST_ASSERT_TRUE(lologWriteFile(log, "f0.ls", (const uint8_t *)"x", 1));
int created = 1;
for (int i = 1; i < 500; i++) {
char path[24];
snprintf(path, sizeof(path), "f%d.ls", i);
if (!lologWriteFile(log, path, (const uint8_t *)"x", 1))
break;
created++;
}
TEST_ASSERT_GREATER_THAN(3, created);
TEST_ASSERT_TRUE(log.remove("f0.ls"));
char extra[24];
snprintf(extra, sizeof(extra), "f%d.ls", created + 10);
TEST_ASSERT_TRUE(lologWriteFile(log, extra, (const uint8_t *)"n", 1));
}
void test_cleaner_recycles_sectors(void)
{
LoLogRamDevice dev(12);
LoLog log(dev);
log.format();
for (int i = 0; i < 80; i++) {
char path[20];
snprintf(path, sizeof(path), "c%d.ls", i);
if (!lologWriteFile(log, path, (const uint8_t *)"xy", 2))
break;
}
for (int i = 0; i < 40; i++) {
char path[20];
snprintf(path, sizeof(path), "c%d.ls", i);
log.remove(path);
}
for (int m = 0; m < 60; m++)
log.maintain(60000);
uint32_t usedAfter = (uint32_t)log.usedBytes();
(void)usedAfter;
TEST_ASSERT_TRUE(log.exists("c79.ls"));
std::vector<uint8_t> tail;
TEST_ASSERT_TRUE(lologReadFile(log, "c79.ls", tail));
TEST_ASSERT_EQUAL_UINT8(0x78, tail[0]);
}
void test_headroom_blocks_before_starve(void)
{
LoLogRamDevice tiny(6);
LoLog log(tiny);
log.format();
for (int i = 0; i < 200; i++) {
char path[32];
snprintf(path, sizeof(path), "f%d.ls", i);
LoLog::OpenHandle *h = log.open(path, true, true);
if (!h)
break;
log.closeHandle(h);
}
LoLog::OpenHandle *last = log.open("overflow.ls", true, true);
TEST_ASSERT_NULL(last);
}
@@ -0,0 +1,85 @@
#include "lofs/lolog/LoLogRamDevice.h"
#include "lofs/lolog/LoLog.h"
#include <unity.h>
LoLogRamDevice gDev(32);
LoLog gLog(gDev);
void setUp(void)
{
gDev.reset();
gLog.format();
}
void tearDown(void) {}
#define DECL(name) void name(void)
DECL(test_mount_empty_after_format);
DECL(test_mount_erased_chip_without_format);
DECL(test_pure_replay_no_index_runs);
DECL(test_mount_after_reboot_simulation);
DECL(test_mkdir_nested_and_overwrite);
DECL(test_rename_file);
DECL(test_rmdir_nonempty_fails_nonempty_ok_recursive);
DECL(test_prefix_collision_names);
DECL(test_large_file_inline_and_extent);
DECL(test_large_file_write_at_path);
DECL(test_stat_and_is_directory);
DECL(test_commit_group_atomic_each_prog_call);
DECL(test_commit_group_each_prog_byte);
DECL(test_torn_tail_then_append);
DECL(test_torn_entry_crc_poke);
DECL(test_fault_single_byte_discards_group);
DECL(test_commit_group_partial_never_applied);
DECL(test_index_flush_pure_reboot);
DECL(test_index_multiple_flushes_and_lookup);
DECL(test_tombstone_shadow_after_remount);
DECL(test_delete_when_near_full);
DECL(test_cleaner_recycles_sectors);
DECL(test_headroom_blocks_before_starve);
DECL(test_random_ops_with_crash_injection);
DECL(test_directory_list_count);
int main(int argc, char **argv)
{
(void)argc;
(void)argv;
UNITY_BEGIN();
RUN_TEST(test_mount_empty_after_format);
RUN_TEST(test_mount_erased_chip_without_format);
RUN_TEST(test_pure_replay_no_index_runs);
RUN_TEST(test_mount_after_reboot_simulation);
RUN_TEST(test_mkdir_nested_and_overwrite);
RUN_TEST(test_rename_file);
RUN_TEST(test_rmdir_nonempty_fails_nonempty_ok_recursive);
RUN_TEST(test_prefix_collision_names);
RUN_TEST(test_large_file_inline_and_extent);
RUN_TEST(test_large_file_write_at_path);
RUN_TEST(test_stat_and_is_directory);
RUN_TEST(test_commit_group_atomic_each_prog_call);
RUN_TEST(test_commit_group_each_prog_byte);
RUN_TEST(test_torn_tail_then_append);
RUN_TEST(test_torn_entry_crc_poke);
RUN_TEST(test_fault_single_byte_discards_group);
RUN_TEST(test_commit_group_partial_never_applied);
RUN_TEST(test_index_flush_pure_reboot);
RUN_TEST(test_index_multiple_flushes_and_lookup);
RUN_TEST(test_tombstone_shadow_after_remount);
RUN_TEST(test_delete_when_near_full);
RUN_TEST(test_cleaner_recycles_sectors);
RUN_TEST(test_headroom_blocks_before_starve);
RUN_TEST(test_random_ops_with_crash_injection);
RUN_TEST(test_directory_list_count);
return UNITY_END();
}