Files
littlefs/tests/test_exhaustion.toml
T
Christopher Haster bc9562c64e tests: Accidentally found a couple buffer overruns
- Off-by-one in test_btree_find_general[_sparse]_fuzz

  Because we can only create named btrees via splitting, these always
  start with one entry. If all operations are randomly selected to be
  splits, this can lead to an overflow of the sim buffer (sounds
  unlikely, but relatively easily for small N).

  The fix is to use a `for (lfs3_size_t i = 1; i < N; i++)` loop to
  account for the initial entry. Note we already use this in the
  test_btree_split_* tests.

  An alternative is allocating space for N+1 entries, but this seems
  unintuitive with N usually being associated with the upper bound on
  btree size.

- Off-by-one in our sim rename pattern

  When renaming, we don't bother to update sim_size, because after the
  rename the sim_size size will be unchanged. But this means the
  sim_size is out-of-date during the memmove that reinserts the renamed
  entry. Buffer overflow!

  To fix we just need to use sim_size-1 to account for the temporarily
  deleted entry.

  This is messy C code, so not surprised it went unnoticed, even though
  this pattern ended up in quite a few tests.

Found while running with HEAP=1. This was just intended to test HEAP=1,
but I guess the injected heap hooks result in a more fragile heap? They
increase all allocations by one word, and maybe this reduces alignment
padding? Not exactly sure.

But it's a good argument for maybe adding heap canaries in the future.
Previously we ran Valgrind on all tests, but it's unclear if this will
still be reasonable with the number of tests we have now.
2026-02-19 12:39:20 -06:00

1815 lines
69 KiB
TOML

# Test running a filesystem to exhaustion and its effects on wear-leveling
after = [
'test_dirs',
'test_files',
'test_fwrite',
'test_stickynotes',
'test_alloc',
'test_badblocks',
'test_relocations',
]
# High-level wear-leveling litmus tests
#
# littlefs implements the weaker form of wear-leveling: dynamic
# wear-leveling. This means we can't guarantee evenly distributed wear,
# but we can at least guarantee the lifetime of storage scales with the
# size of storage.
#
# This gives us something concrete we can test, that doubling the size of
# storage roughly doubles the lifetime of the storage.
# test dir wear-leveling
[cases.test_exhaustion_spam_dir_fuzz]
defines.ERASE_CYCLES = 10
defines.BLOCK_RECYCLES = 4
defines.BADBLOCK_BEHAVIOR = [
'LFS3_EMUBD_BADBLOCK_PROGERROR',
'LFS3_EMUBD_BADBLOCK_ERASEERROR',
'LFS3_EMUBD_BADBLOCK_READERROR',
'LFS3_EMUBD_BADBLOCK_PROGNOOP',
'LFS3_EMUBD_BADBLOCK_ERASENOOP',
]
# we need prog checking to detect read errors
defines.CKPROGS = 'BADBLOCK_BEHAVIOR >= LFS3_EMUBD_BADBLOCK_READERROR'
defines.N = [1, 2, 4, 8, 16, 32, 64, 128, 256]
defines.SEED = 42
fuzz = 'SEED'
if = 'LFS3_IFDEF_CKPROGS(true, !CKPROGS)'
code = '''
// run our test twice, once with 1/2 the storage, once with 2/2 the
// storage, and compare how many operations we were able to perform
// before filesystem death
uint32_t run_bc[2] = {BLOCK_COUNT/2, BLOCK_COUNT};
uint32_t run_ops[2] = {0, 0};
for (int run = 0; run < 2; run++) {
// clear any wear from the previous run
for (lfs3_block_t i = 0; i < BLOCK_COUNT; i++) {
lfs3_emubd_setwear(CFG, i, 0) => 0;
}
// configure the filesystem size
struct lfs3_cfg cfg = *CFG;
cfg.block_count = run_bc[run];
// run the test
lfs3_t lfs3;
lfs3_format(&lfs3,
LFS3_F_RDWR
| ((CKPROGS) ? LFS3_IFDEF_CKPROGS(LFS3_F_CKPROGS, -1) : 0),
&cfg) => 0;
lfs3_mount(&lfs3,
LFS3_M_RDWR
| ((CKPROGS) ? LFS3_IFDEF_CKPROGS(LFS3_M_CKPROGS, -1) : 0),
&cfg) => 0;
// set up a simulation to compare against
lfs3_size_t *sim = malloc(N*sizeof(lfs3_size_t));
lfs3_size_t sim_size = 0;
uint32_t prng = SEED;
for (;; run_ops[run]++) {
// choose a pseudo-random op, either mkdir, remove, or rename
uint8_t op = TEST_PRNG(&prng) % 3;
if (op == 0 || sim_size == 0) {
// choose a pseudo-random number, truncate to 3 hexadecimals
lfs3_size_t x = TEST_PRNG(&prng) % N;
// insert into our sim
for (lfs3_size_t j = 0;; j++) {
if (j >= sim_size || sim[j] >= x) {
// already seen?
if (j < sim_size && sim[j] == x) {
// do nothing
} else {
// insert
memmove(&sim[j+1], &sim[j],
(sim_size-j)*sizeof(lfs3_size_t));
sim_size += 1;
sim[j] = x;
}
break;
}
}
// create a directory here
char name[256];
sprintf(name, "dir%03x", x);
int err = lfs3_mkdir(&lfs3, name);
assert(!err || err == LFS3_ERR_EXIST || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
goto dead;
}
} else if (op == 1) {
// choose a pseudo-random entry to delete
lfs3_size_t j = TEST_PRNG(&prng) % sim_size;
lfs3_size_t x = sim[j];
// delete from our sim
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs3_size_t));
sim_size -= 1;
// remove this directory
char name[256];
sprintf(name, "dir%03x", x);
int err = lfs3_remove(&lfs3, name);
assert(!err || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
goto dead;
}
} else {
// choose a pseudo-random entry to rename, and a pseudo-random
// number to rename to
lfs3_size_t j = TEST_PRNG(&prng) % sim_size;
lfs3_size_t x = sim[j];
lfs3_size_t y = TEST_PRNG(&prng) % N;
for (lfs3_size_t k = 0;; k++) {
if (k >= sim_size || sim[k] >= y) {
// already seen and not a noop?
if (k < sim_size && sim[k] == y && x != y) {
// just delete the original entry
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs3_size_t));
sim_size -= 1;
} else {
// first delete
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs3_size_t));
if (k > j) {
k -= 1;
}
// then insert
memmove(&sim[k+1], &sim[k],
(sim_size-1-k)*sizeof(lfs3_size_t));
sim[k] = y;
}
break;
}
}
// rename this directory
char old_name[256];
sprintf(old_name, "dir%03x", x);
char new_name[256];
sprintf(new_name, "dir%03x", y);
int err = lfs3_rename(&lfs3, old_name, new_name);
assert(!err || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
goto dead;
}
}
// check our simulation every power-of-2 ops
if (lfs3_popc(run_ops[run]) == 1) {
// test that our directories match our simulation
for (lfs3_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "dir%03x", sim[j]);
struct lfs3_info info;
lfs3_stat(&lfs3, name, &info) => 0;
char name2[256];
sprintf(name2, "dir%03x", sim[j]);
assert(strcmp(info.name, name2) == 0);
assert(info.type == LFS3_TYPE_DIR);
assert(info.size == 0);
}
lfs3_dir_t dir;
lfs3_dir_open(&lfs3, &dir, "/") => 0;
struct lfs3_info info;
lfs3_dir_read(&lfs3, &dir, &info) => 0;
assert(strcmp(info.name, ".") == 0);
assert(info.type == LFS3_TYPE_DIR);
assert(info.size == 0);
lfs3_dir_read(&lfs3, &dir, &info) => 0;
assert(strcmp(info.name, "..") == 0);
assert(info.type == LFS3_TYPE_DIR);
assert(info.size == 0);
for (lfs3_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "dir%03x", sim[j]);
lfs3_dir_read(&lfs3, &dir, &info) => 0;
assert(strcmp(info.name, name) == 0);
assert(info.type == LFS3_TYPE_DIR);
assert(info.size == 0);
}
lfs3_dir_read(&lfs3, &dir, &info) => LFS3_ERR_NOENT;
lfs3_dir_close(&lfs3, &dir) => 0;
}
}
dead:;
// clean up sim/lfs3
free(sim);
lfs3_unmount(&lfs3) => 0;
// print how many ops
printf("run %d, %dx%d, %d ec: %d ops\n",
run,
(int)BLOCK_SIZE,
run_bc[run],
(int)ERASE_CYCLES,
run_ops[run]);
}
// check that we increased the liftime by ~2x, with ~10% error
printf("lifetime: %d -> %d (x%.2f)\n",
run_ops[0],
run_ops[1],
(double)run_ops[1] / (double)run_ops[0]);
assert(run_ops[1]*110/100 > 2*run_ops[0]);
'''
# test file wear-leveling
[cases.test_exhaustion_spam_file_fuzz]
defines.ERASE_CYCLES = 10
defines.BLOCK_RECYCLES = 4
defines.BADBLOCK_BEHAVIOR = [
'LFS3_EMUBD_BADBLOCK_PROGERROR',
'LFS3_EMUBD_BADBLOCK_ERASEERROR',
'LFS3_EMUBD_BADBLOCK_READERROR',
'LFS3_EMUBD_BADBLOCK_PROGNOOP',
'LFS3_EMUBD_BADBLOCK_ERASENOOP',
]
# we need prog checking to detect read errors
defines.CKPROGS = 'BADBLOCK_BEHAVIOR >= LFS3_EMUBD_BADBLOCK_READERROR'
defines.N = [1, 2, 4, 8, 16, 32, 64]
defines.SIZE = [
'0',
'FCACHE_SIZE/2',
'2*FCACHE_SIZE',
'BLOCK_SIZE/2',
'BLOCK_SIZE',
'2*BLOCK_SIZE',
'4*BLOCK_SIZE',
]
defines.SEED = 42
fuzz = 'SEED'
if = [
'LFS3_IFDEF_CKPROGS(true, !CKPROGS)',
'(SIZE*N)/BLOCK_SIZE <= 16',
]
code = '''
// run our test twice, once with 1/2 the storage, once with 2/2 the
// storage, and compare how many operations we were able to perform
// before filesystem death
uint32_t run_bc[2] = {BLOCK_COUNT/2, BLOCK_COUNT};
uint32_t run_ops[2] = {0, 0};
for (int run = 0; run < 2; run++) {
// clear any wear from the previous run
for (lfs3_block_t i = 0; i < BLOCK_COUNT; i++) {
lfs3_emubd_setwear(CFG, i, 0) => 0;
}
// configure the filesystem size
struct lfs3_cfg cfg = *CFG;
cfg.block_count = run_bc[run];
// run the test
lfs3_t lfs3;
lfs3_format(&lfs3,
LFS3_F_RDWR
| ((CKPROGS) ? LFS3_IFDEF_CKPROGS(LFS3_F_CKPROGS, -1) : 0),
&cfg) => 0;
lfs3_mount(&lfs3,
LFS3_M_RDWR
| ((CKPROGS) ? LFS3_IFDEF_CKPROGS(LFS3_M_CKPROGS, -1) : 0),
&cfg) => 0;
// set up a simulation to compare against
lfs3_size_t *sim = malloc(N*sizeof(lfs3_size_t));
uint32_t *sim_prngs = malloc(N*sizeof(uint32_t));
lfs3_size_t sim_size = 0;
uint32_t prng = SEED;
for (;; run_ops[run]++) {
// choose which operation to do
uint8_t op = TEST_PRNG(&prng) % 3;
// creating a new file?
if (op == 0 || sim_size == 0) {
// choose a pseudo-random number
lfs3_size_t x = TEST_PRNG(&prng) % N;
// associate each file with a prng that generates its contents
uint32_t wprng = TEST_PRNG(&prng);
// insert into our sim
for (lfs3_size_t j = 0;; j++) {
if (j >= sim_size || sim[j] >= x) {
// already seen?
if (j < sim_size && sim[j] == x) {
// new prng
sim_prngs[j] = wprng;
} else {
// insert
memmove(&sim[j+1], &sim[j],
(sim_size-j)*sizeof(lfs3_size_t));
memmove(&sim_prngs[j+1], &sim_prngs[j],
(sim_size-j)*sizeof(uint32_t));
sim_size += 1;
sim[j] = x;
sim_prngs[j] = wprng;
}
break;
}
}
// create a file here
char name[256];
sprintf(name, "amethyst%03x", x);
uint8_t wbuf[SIZE];
for (lfs3_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&wprng) % 26);
}
lfs3_file_t file;
int err = lfs3_file_open(&lfs3, &file, name,
LFS3_O_WRONLY | LFS3_O_CREAT | LFS3_O_TRUNC);
assert(!err || err == LFS3_ERR_NOSPC);
if (err) {
goto dead;
}
lfs3_ssize_t d = lfs3_file_write(&lfs3, &file, wbuf, SIZE);
assert(d == SIZE || d == LFS3_ERR_NOSPC);
if (d == LFS3_ERR_NOSPC) {
lfs3_file_close(&lfs3, &file) => 0;
goto dead;
}
err = lfs3_file_close(&lfs3, &file);
assert(!err || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
goto dead;
}
// deleting a file?
} else if (op == 1) {
// choose a random file to delete
lfs3_size_t j = TEST_PRNG(&prng) % sim_size;
lfs3_size_t x = sim[j];
// delete from our sim
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs3_size_t));
memmove(&sim_prngs[j], &sim_prngs[j+1],
(sim_size-(j+1))*sizeof(uint32_t));
sim_size -= 1;
// delete this file
char name[256];
sprintf(name, "amethyst%03x", x);
int err = lfs3_remove(&lfs3, name);
assert(!err || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
goto dead;
}
// renaming a file?
} else {
// choose a random file to rename, and a random number to
// rename to
lfs3_size_t j = TEST_PRNG(&prng) % sim_size;
lfs3_size_t x = sim[j];
lfs3_size_t y = TEST_PRNG(&prng) % N;
uint32_t wprng = sim_prngs[j];
// update our sim
for (lfs3_size_t k = 0;; k++) {
if (k >= sim_size || sim[k] >= y) {
// renaming and replacing
if (k < sim_size && sim[k] == y && x != y) {
// delete the original entry
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs3_size_t));
memmove(&sim_prngs[j], &sim_prngs[j+1],
(sim_size-(j+1))*sizeof(uint32_t));
sim_size -= 1;
if (k > j) {
k -= 1;
}
// update the prng
sim_prngs[k] = wprng;
// just renaming
} else {
// first delete
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs3_size_t));
memmove(&sim_prngs[j], &sim_prngs[j+1],
(sim_size-(j+1))*sizeof(uint32_t));
if (k > j) {
k -= 1;
}
// then insert
memmove(&sim[k+1], &sim[k],
(sim_size-1-k)*sizeof(lfs3_size_t));
memmove(&sim_prngs[k+1], &sim_prngs[k],
(sim_size-1-k)*sizeof(uint32_t));
sim[k] = y;
sim_prngs[k] = wprng;
}
break;
}
}
// rename this file
char old_name[256];
sprintf(old_name, "amethyst%03x", x);
char new_name[256];
sprintf(new_name, "amethyst%03x", y);
int err = lfs3_rename(&lfs3, old_name, new_name);
assert(!err || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
goto dead;
}
}
// check our simulation every power-of-2 ops
if (lfs3_popc(run_ops[run]) == 1) {
// check that our files match our simulation
for (lfs3_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "amethyst%03x", sim[j]);
struct lfs3_info info;
lfs3_stat(&lfs3, name, &info) => 0;
assert(strcmp(info.name, name) == 0);
assert(info.type == LFS3_TYPE_REG);
assert(info.size == SIZE);
}
lfs3_dir_t dir;
lfs3_dir_open(&lfs3, &dir, "/") => 0;
struct lfs3_info info;
lfs3_dir_read(&lfs3, &dir, &info) => 0;
assert(strcmp(info.name, ".") == 0);
assert(info.type == LFS3_TYPE_DIR);
assert(info.size == 0);
lfs3_dir_read(&lfs3, &dir, &info) => 0;
assert(strcmp(info.name, "..") == 0);
assert(info.type == LFS3_TYPE_DIR);
assert(info.size == 0);
for (lfs3_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "amethyst%03x", sim[j]);
lfs3_dir_read(&lfs3, &dir, &info) => 0;
assert(strcmp(info.name, name) == 0);
assert(info.type == LFS3_TYPE_REG);
assert(info.size == SIZE);
}
lfs3_dir_read(&lfs3, &dir, &info) => LFS3_ERR_NOENT;
lfs3_dir_close(&lfs3, &dir) => 0;
// check the file contents
for (lfs3_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "amethyst%03x", sim[j]);
lfs3_file_t file;
lfs3_file_open(&lfs3, &file, name, LFS3_O_RDONLY) => 0;
uint32_t wprng = sim_prngs[j];
uint8_t wbuf[SIZE];
for (lfs3_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&wprng) % 26);
}
uint8_t rbuf[SIZE];
lfs3_file_read(&lfs3, &file, rbuf, SIZE) => SIZE;
assert(memcmp(rbuf, wbuf, SIZE) == 0);
lfs3_file_close(&lfs3, &file) => 0;
}
}
}
dead:;
// clean up sim/lfs3
free(sim);
free(sim_prngs);
lfs3_unmount(&lfs3) => 0;
// print how many ops
printf("run %d, %dx%d, %d ec: %d ops\n",
run,
(int)BLOCK_SIZE,
run_bc[run],
(int)ERASE_CYCLES,
run_ops[run]);
}
// check that we increased the liftime by ~2x, with ~10% error
printf("lifetime: %d -> %d (x%.2f)\n",
run_ops[0],
run_ops[1],
(double)run_ops[1] / (double)run_ops[0]);
assert(run_ops[1]*110/100 > 2*run_ops[0]);
'''
# with more complex file writes
[cases.test_exhaustion_spam_fwrite_fuzz]
defines.ERASE_CYCLES = 10
defines.BLOCK_RECYCLES = 4
defines.BADBLOCK_BEHAVIOR = [
'LFS3_EMUBD_BADBLOCK_PROGERROR',
'LFS3_EMUBD_BADBLOCK_ERASEERROR',
'LFS3_EMUBD_BADBLOCK_READERROR',
'LFS3_EMUBD_BADBLOCK_PROGNOOP',
'LFS3_EMUBD_BADBLOCK_ERASENOOP',
]
# we need prog checking to detect read errors
defines.CKPROGS = 'BADBLOCK_BEHAVIOR >= LFS3_EMUBD_BADBLOCK_READERROR'
defines.SIZE = [
'FCACHE_SIZE/2',
'2*FCACHE_SIZE',
'BLOCK_SIZE/2',
'BLOCK_SIZE',
'2*BLOCK_SIZE',
'4*BLOCK_SIZE',
]
# chunk is more an upper limit here
defines.CHUNK = 64
# INIT=0 => no init
# INIT=1 => fill with data
# INIT=2 => truncate to size
defines.INIT = [0, 1, 2]
defines.SYNC = [false, true]
defines.SEED = 42
fuzz = 'SEED'
if = [
'LFS3_IFDEF_CKPROGS(true, !CKPROGS)',
'CHUNK <= SIZE',
# this just saves testing time
'SIZE <= 4*1024*FRAGMENT_SIZE',
]
code = '''
// run our test twice, once with 1/2 the storage, once with 2/2 the
// storage, and compare how many operations we were able to perform
// before filesystem death
uint32_t run_bc[2] = {BLOCK_COUNT/2, BLOCK_COUNT};
uint32_t run_ops[2] = {0, 0};
for (int run = 0; run < 2; run++) {
// clear any wear from the previous run
for (lfs3_block_t i = 0; i < BLOCK_COUNT; i++) {
lfs3_emubd_setwear(CFG, i, 0) => 0;
}
// configure the filesystem size
struct lfs3_cfg cfg = *CFG;
cfg.block_count = run_bc[run];
// run the test
lfs3_t lfs3;
lfs3_format(&lfs3,
LFS3_F_RDWR
| ((CKPROGS) ? LFS3_IFDEF_CKPROGS(LFS3_F_CKPROGS, -1) : 0),
&cfg) => 0;
lfs3_mount(&lfs3,
LFS3_M_RDWR
| ((CKPROGS) ? LFS3_IFDEF_CKPROGS(LFS3_M_CKPROGS, -1) : 0),
&cfg) => 0;
// create a file
lfs3_file_t file;
lfs3_file_open(&lfs3, &file, "hello",
LFS3_O_WRONLY | LFS3_O_CREAT | LFS3_O_EXCL) => 0;
// simulate our file in ram
uint8_t sim[SIZE];
lfs3_off_t size;
uint32_t prng = SEED;
if (INIT == 0) {
memset(sim, 0, SIZE);
size = 0;
} else if (INIT == 1) {
for (lfs3_size_t i = 0; i < SIZE; i++) {
sim[i] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfs3_file_write(&lfs3, &file, sim, SIZE) => SIZE;
size = SIZE;
} else {
memset(sim, 0, SIZE);
lfs3_file_truncate(&lfs3, &file, SIZE) => 0;
size = SIZE;
}
// sync?
if (SYNC) {
lfs3_file_sync(&lfs3, &file) => 0;
}
for (;; run_ops[run]++) {
// choose a random location
lfs3_off_t off = TEST_PRNG(&prng) % SIZE;
// and a random size, up to the chunk size
lfs3_size_t chunk = lfs3_min(
(TEST_PRNG(&prng) % (CHUNK+1-1)) + 1,
SIZE - off);
// update sim
for (lfs3_size_t j = 0; j < chunk; j++) {
sim[off+j] = 'a' + (TEST_PRNG(&prng) % 26);
}
size = lfs3_max(size, off+chunk);
// update file
lfs3_file_seek(&lfs3, &file, off, LFS3_SEEK_SET) => off;
lfs3_ssize_t d = lfs3_file_write(&lfs3, &file, &sim[off], chunk);
assert(d == (lfs3_ssize_t)chunk || d == LFS3_ERR_NOSPC);
if (d == LFS3_ERR_NOSPC) {
goto dead;
}
// sync?
if (SYNC) {
int err = lfs3_file_sync(&lfs3, &file);
assert(!err || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
goto dead;
}
}
// check our simulation every power-of-2 ops
if (lfs3_popc(run_ops[run]) == 1 && SYNC) {
// check our file with stat
struct lfs3_info info;
lfs3_stat(&lfs3, "hello", &info) => 0;
assert(strcmp(info.name, "hello") == 0);
assert(info.type == LFS3_TYPE_REG);
assert(info.size == size);
// and with dir read
lfs3_dir_t dir;
lfs3_dir_open(&lfs3, &dir, "/") => 0;
lfs3_dir_read(&lfs3, &dir, &info) => 0;
assert(strcmp(info.name, ".") == 0);
assert(info.type == LFS3_TYPE_DIR);
assert(info.size == 0);
lfs3_dir_read(&lfs3, &dir, &info) => 0;
assert(strcmp(info.name, "..") == 0);
assert(info.type == LFS3_TYPE_DIR);
assert(info.size == 0);
lfs3_dir_read(&lfs3, &dir, &info) => 0;
assert(strcmp(info.name, "hello") == 0);
assert(info.type == LFS3_TYPE_REG);
assert(info.size == size);
lfs3_dir_read(&lfs3, &dir, &info) => LFS3_ERR_NOENT;
lfs3_dir_close(&lfs3, &dir) => 0;
// try reading our file
lfs3_file_t file_;
lfs3_file_open(&lfs3, &file_, "hello", LFS3_O_RDONLY) => 0;
// is size correct?
lfs3_file_size(&lfs3, &file_) => size;
// try reading
uint8_t rbuf[2*SIZE];
memset(rbuf, 0xaa, 2*SIZE);
lfs3_file_read(&lfs3, &file_, rbuf, 2*SIZE) => size;
// does our file match our simulation?
assert(memcmp(rbuf, sim, size) == 0);
lfs3_file_close(&lfs3, &file_) => 0;
}
}
dead:;
// clean up sim/lfs3
lfs3_file_close(&lfs3, &file) => 0;
lfs3_unmount(&lfs3) => 0;
// print how many ops
printf("run %d, %dx%d, %d ec: %d ops\n",
run,
(int)BLOCK_SIZE,
run_bc[run],
(int)ERASE_CYCLES,
run_ops[run]);
}
// check that we increased the liftime by ~2x, with ~10% error
printf("lifetime: %d -> %d (x%.2f)\n",
run_ops[0],
run_ops[1],
(double)run_ops[1] / (double)run_ops[0]);
assert(run_ops[1]*110/100 > 2*run_ops[0]);
'''
# with uncreats, zombies, etc
[cases.test_exhaustion_spam_uz_fuzz]
defines.ERASE_CYCLES = 10
defines.BLOCK_RECYCLES = 4
defines.BADBLOCK_BEHAVIOR = [
'LFS3_EMUBD_BADBLOCK_PROGERROR',
'LFS3_EMUBD_BADBLOCK_ERASEERROR',
'LFS3_EMUBD_BADBLOCK_READERROR',
'LFS3_EMUBD_BADBLOCK_PROGNOOP',
'LFS3_EMUBD_BADBLOCK_ERASENOOP',
]
# we need prog checking to detect read errors
defines.CKPROGS = 'BADBLOCK_BEHAVIOR >= LFS3_EMUBD_BADBLOCK_READERROR'
# you probably need to flush if you expect errors
defines.FLUSH = true
defines.N = [1, 2, 4, 8, 16, 32, 64]
defines.SIZE = [
'0',
'FCACHE_SIZE/2',
'2*FCACHE_SIZE',
'BLOCK_SIZE/2',
'BLOCK_SIZE',
'2*BLOCK_SIZE',
'4*BLOCK_SIZE',
]
defines.SEED = 42
fuzz = 'SEED'
if = [
'LFS3_IFDEF_CKPROGS(true, !CKPROGS)',
'(SIZE*N)/BLOCK_SIZE <= 16',
]
code = '''
// run our test twice, once with 1/2 the storage, once with 2/2 the
// storage, and compare how many operations we were able to perform
// before filesystem death
uint32_t run_bc[2] = {BLOCK_COUNT/2, BLOCK_COUNT};
uint32_t run_ops[2] = {0, 0};
for (int run = 0; run < 2; run++) {
// clear any wear from the previous run
for (lfs3_block_t i = 0; i < BLOCK_COUNT; i++) {
lfs3_emubd_setwear(CFG, i, 0) => 0;
}
// configure the filesystem size
struct lfs3_cfg cfg = *CFG;
cfg.block_count = run_bc[run];
// run the test
lfs3_t lfs3;
lfs3_format(&lfs3,
LFS3_F_RDWR
| ((CKPROGS) ? LFS3_IFDEF_CKPROGS(LFS3_F_CKPROGS, -1) : 0),
&cfg) => 0;
lfs3_mount(&lfs3,
LFS3_M_RDWR
| ((CKPROGS) ? LFS3_IFDEF_CKPROGS(LFS3_M_CKPROGS, -1) : 0),
&cfg) => 0;
// set up a simulation to compare against
lfs3_size_t *sim = malloc(N*sizeof(lfs3_size_t));
uint32_t *sim_prngs = malloc(N*sizeof(uint32_t));
bool *sim_isstickys = malloc(N*sizeof(bool));
lfs3_size_t sim_size = 0;
typedef struct sim_file {
lfs3_size_t x;
bool sticky;
bool zombie;
uint32_t prng;
lfs3_file_t file;
} sim_file_t;
sim_file_t **sim_files = malloc(N*sizeof(sim_file_t*));
lfs3_size_t sim_file_count = 0;
uint32_t prng = SEED;
for (;; run_ops[run]++) {
nonsense:;
// choose which operation to do
uint8_t op = TEST_PRNG(&prng) % 5;
// open a new file?
if (op == 0) {
if (sim_file_count >= N) {
goto nonsense;
}
// choose a pseudo-random number
lfs3_size_t x = TEST_PRNG(&prng) % N;
// already exists?
bool exist = false;
uint32_t wprng = 0;
bool sticky = true;
for (lfs3_size_t j = 0; j < sim_size; j++) {
if (sim[j] == x) {
exist = true;
wprng = sim_prngs[j];
sticky = sim_isstickys[j];
break;
}
}
// choose a random seed if we don't exist
if (!exist) {
wprng = TEST_PRNG(&prng);
sticky = true;
}
lfs3_size_t j = sim_file_count;
sim_files[j] = malloc(sizeof(sim_file_t));
// open the actual file
char name[256];
sprintf(name, "batman%03x", x);
int err = lfs3_file_open(&lfs3, &sim_files[j]->file, name,
LFS3_O_RDWR
| LFS3_O_CREAT
| ((FLUSH) ? LFS3_O_FLUSH : 0));
assert(!err || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
free(sim_files[j]);
goto dead;
}
// write some initial data if we don't exist
if (!exist || sticky) {
uint8_t wbuf[SIZE];
uint32_t wprng_ = wprng;
for (lfs3_size_t k = 0; k < SIZE; k++) {
wbuf[k] = 'a' + (TEST_PRNG(&wprng_) % 26);
}
lfs3_ssize_t d = lfs3_file_write(&lfs3, &sim_files[j]->file,
wbuf, SIZE);
assert(d == SIZE || d == LFS3_ERR_NOSPC);
if (d == LFS3_ERR_NOSPC) {
lfs3_file_close(&lfs3, &sim_files[j]->file) => 0;
free(sim_files[j]);
goto dead;
}
}
// open in our sim
sim_files[j]->x = x;
sim_files[j]->sticky = sticky;
sim_files[j]->zombie = false;
sim_files[j]->prng = wprng;
sim_file_count++;
// insert into our sim
for (lfs3_size_t k = 0;; k++) {
if (k >= sim_size || sim[k] >= x) {
// already seen?
if (k < sim_size && sim[k] == x) {
// new prng
sim_prngs[k] = wprng;
} else {
// insert
memmove(&sim[k+1], &sim[k],
(sim_size-k)*sizeof(lfs3_size_t));
memmove(&sim_prngs[k+1], &sim_prngs[k],
(sim_size-k)*sizeof(uint32_t));
memmove(&sim_isstickys[k+1], &sim_isstickys[k],
(sim_size-k)*sizeof(bool));
sim_size += 1;
sim[k] = x;
sim_prngs[k] = wprng;
sim_isstickys[k] = sticky;
}
break;
}
}
// write/rewrite a file?
} else if (op == 1) {
if (sim_file_count == 0) {
goto nonsense;
}
// choose a random file handle
lfs3_size_t j = TEST_PRNG(&prng) % sim_file_count;
lfs3_size_t x = sim_files[j]->x;
// choose a random seed
uint32_t wprng = TEST_PRNG(&prng);
// write to the file
lfs3_file_rewind(&lfs3, &sim_files[j]->file) => 0;
uint8_t wbuf[SIZE];
uint32_t wprng_ = wprng;
for (lfs3_size_t k = 0; k < SIZE; k++) {
wbuf[k] = 'a' + (TEST_PRNG(&wprng_) % 26);
}
lfs3_ssize_t d = lfs3_file_write(&lfs3, &sim_files[j]->file,
wbuf, SIZE);
assert(d == SIZE || d == LFS3_ERR_NOSPC);
if (d == LFS3_ERR_NOSPC) {
goto dead;
}
int err = lfs3_file_sync(&lfs3, &sim_files[j]->file);
assert(err == 0 || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
goto dead;
}
// update sim
sim_files[j]->prng = wprng;
if (!sim_files[j]->zombie) {
// update in our sim
for (lfs3_size_t k = 0;; k++) {
if (sim[k] == x) {
// new prng
sim_prngs[k] = wprng;
// no longer sticky
sim_isstickys[k] = false;
break;
}
}
// update related sim files
for (lfs3_size_t k = 0; k < sim_file_count; k++) {
if (sim_files[k]->x == x && !sim_files[k]->zombie) {
// new prng
sim_files[k]->prng = wprng;
// no longer sticky
sim_files[k]->sticky = false;
}
}
}
// close a file?
} else if (op == 2) {
if (sim_file_count == 0) {
goto nonsense;
}
// choose a random file handle
lfs3_size_t j = TEST_PRNG(&prng) % sim_file_count;
lfs3_size_t x = sim_files[j]->x;
bool sticky = sim_files[j]->sticky;
bool zombie = sim_files[j]->zombie;
// this doesn't really test anything, but if we don't close
// files eventually everything will end up zombies
// close the file without affected disk
lfs3_file_desync(&lfs3, &sim_files[j]->file) => 0;
lfs3_file_close(&lfs3, &sim_files[j]->file) => 0;
// clobber closed files to try to catch lingering references
memset(&sim_files[j]->file, 0xcc, sizeof(lfs3_file_t));
// remove from list
free(sim_files[j]);
sim_files[j] = sim_files[sim_file_count-1];
sim_file_count -= 1;
// update our sim
if (sticky && !zombie) {
// orphaned?
bool orphan = true;
for (lfs3_size_t k = 0; k < sim_file_count; k++) {
if (sim_files[k]->x == x && !sim_files[k]->zombie) {
orphan = false;
}
}
// if we were never synced, delete from sim
if (orphan) {
for (lfs3_size_t k = 0;; k++) {
if (sim[k] == x) {
memmove(&sim[k], &sim[k+1],
(sim_size-(k+1))*sizeof(lfs3_size_t));
memmove(&sim_prngs[k], &sim_prngs[k+1],
(sim_size-(k+1))*sizeof(uint32_t));
memmove(&sim_isstickys[k], &sim_isstickys[k+1],
(sim_size-(k+1))*sizeof(bool));
sim_size -= 1;
break;
}
}
}
}
// remove a file?
} else if (op == 3) {
if (sim_size == 0) {
goto nonsense;
}
// choose a random file to delete
lfs3_size_t j = TEST_PRNG(&prng) % sim_size;
lfs3_size_t x = sim[j];
// delete this file
char name[256];
sprintf(name, "batman%03x", x);
int err = lfs3_remove(&lfs3, name);
assert(!err || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
goto dead;
}
// delete from our sim
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs3_size_t));
memmove(&sim_prngs[j], &sim_prngs[j+1],
(sim_size-(j+1))*sizeof(uint32_t));
memmove(&sim_isstickys[j], &sim_isstickys[j+1],
(sim_size-(j+1))*sizeof(bool));
sim_size -= 1;
// mark any related sim files as zombied
for (lfs3_size_t k = 0; k < sim_file_count; k++) {
if (sim_files[k]->x == x) {
sim_files[k]->zombie = true;
}
}
// rename a file?
} else if (op == 4) {
if (sim_size == 0) {
goto nonsense;
}
// choose a random file to rename, and a random number to
// rename to
lfs3_size_t j = TEST_PRNG(&prng) % sim_size;
lfs3_size_t x = sim[j];
lfs3_size_t y = TEST_PRNG(&prng) % N;
uint32_t wprng = sim_prngs[j];
bool sticky = sim_isstickys[j];
// rename this file
char old_name[256];
sprintf(old_name, "batman%03x", x);
char new_name[256];
sprintf(new_name, "batman%03x", y);
int err = lfs3_rename(&lfs3, old_name, new_name);
assert(!err || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
goto dead;
}
// update our sim
for (lfs3_size_t k = 0;; k++) {
if (k >= sim_size || sim[k] >= y) {
// renaming and replacing
if (k < sim_size && sim[k] == y && x != y) {
// delete the original entry
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs3_size_t));
memmove(&sim_prngs[j], &sim_prngs[j+1],
(sim_size-(j+1))*sizeof(uint32_t));
memmove(&sim_isstickys[j], &sim_isstickys[j+1],
(sim_size-(j+1))*sizeof(bool));
sim_size -= 1;
if (k > j) {
k -= 1;
}
// update the prng/sticky
sim_prngs[k] = wprng;
sim_isstickys[k] = sticky;
// just renaming
} else {
// first delete
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs3_size_t));
memmove(&sim_prngs[j], &sim_prngs[j+1],
(sim_size-(j+1))*sizeof(uint32_t));
memmove(&sim_isstickys[j], &sim_isstickys[j+1],
(sim_size-(j+1))*sizeof(bool));
if (k > j) {
k -= 1;
}
// then insert
memmove(&sim[k+1], &sim[k],
(sim_size-1-k)*sizeof(lfs3_size_t));
memmove(&sim_prngs[k+1], &sim_prngs[k],
(sim_size-1-k)*sizeof(uint32_t));
memmove(&sim_isstickys[k+1], &sim_isstickys[k],
(sim_size-1-k)*sizeof(bool));
sim[k] = y;
sim_prngs[k] = wprng;
sim_isstickys[k] = sticky;
}
break;
}
}
// update any related sim files
for (lfs3_size_t k = 0; k < sim_file_count; k++) {
// move source files
if (sim_files[k]->x == x) {
sim_files[k]->x = y;
// mark target files as zombied
} else if (sim_files[k]->x == y) {
sim_files[k]->zombie = true;
}
}
}
// check our simulation every power-of-2 ops
if (lfs3_popc(run_ops[run]) == 1) {
// check that disk matches our simulation
for (lfs3_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "batman%03x", sim[j]);
struct lfs3_info info;
lfs3_stat(&lfs3, name, &info) => 0;
assert(strcmp(info.name, name) == 0);
if (sim_isstickys[j]) {
assert(info.type == LFS3_TYPE_STICKYNOTE);
assert(info.size == 0);
} else {
assert(info.type == LFS3_TYPE_REG);
assert(info.size == SIZE);
}
}
lfs3_dir_t dir;
lfs3_dir_open(&lfs3, &dir, "/") => 0;
struct lfs3_info info;
lfs3_dir_read(&lfs3, &dir, &info) => 0;
assert(strcmp(info.name, ".") == 0);
assert(info.type == LFS3_TYPE_DIR);
assert(info.size == 0);
lfs3_dir_read(&lfs3, &dir, &info) => 0;
assert(strcmp(info.name, "..") == 0);
assert(info.type == LFS3_TYPE_DIR);
assert(info.size == 0);
for (lfs3_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "batman%03x", sim[j]);
lfs3_dir_read(&lfs3, &dir, &info) => 0;
assert(strcmp(info.name, name) == 0);
if (sim_isstickys[j]) {
assert(info.type == LFS3_TYPE_STICKYNOTE);
assert(info.size == 0);
} else {
assert(info.type == LFS3_TYPE_REG);
assert(info.size == SIZE);
}
}
lfs3_dir_read(&lfs3, &dir, &info) => LFS3_ERR_NOENT;
lfs3_dir_close(&lfs3, &dir) => 0;
for (lfs3_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "batman%03x", sim[j]);
lfs3_file_t file;
lfs3_file_open(&lfs3, &file, name, LFS3_O_RDONLY) => 0;
uint32_t wprng = sim_prngs[j];
uint8_t wbuf[SIZE];
for (lfs3_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&wprng) % 26);
}
uint8_t rbuf[SIZE];
if (sim_isstickys[j]) {
lfs3_file_read(&lfs3, &file, rbuf, SIZE) => 0;
} else {
lfs3_file_read(&lfs3, &file, rbuf, SIZE) => SIZE;
assert(memcmp(rbuf, wbuf, SIZE) == 0);
}
lfs3_file_close(&lfs3, &file) => 0;
}
// check that our file handles match our simulation
for (lfs3_size_t j = 0; j < sim_file_count; j++) {
uint32_t wprng = sim_files[j]->prng;
uint8_t wbuf[SIZE];
for (lfs3_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&wprng) % 26);
}
lfs3_file_rewind(&lfs3, &sim_files[j]->file) => 0;
uint8_t rbuf[SIZE];
lfs3_file_read(&lfs3, &sim_files[j]->file, rbuf, SIZE)
=> SIZE;
assert(memcmp(rbuf, wbuf, SIZE) == 0);
}
}
}
dead:;
// clean up sim/lfs3
free(sim);
free(sim_prngs);
free(sim_isstickys);
for (lfs3_size_t j = 0; j < sim_file_count; j++) {
lfs3_file_desync(&lfs3, &sim_files[j]->file) => 0;
lfs3_file_close(&lfs3, &sim_files[j]->file) => 0;
free(sim_files[j]);
}
free(sim_files);
lfs3_unmount(&lfs3) => 0;
// print how many ops
printf("run %d, %dx%d, %d ec: %d ops\n",
run,
(int)BLOCK_SIZE,
run_bc[run],
(int)ERASE_CYCLES,
run_ops[run]);
}
// check that we increased the liftime by ~2x, with ~10% error
printf("lifetime: %d -> %d (x%.2f)\n",
run_ops[0],
run_ops[1],
(double)run_ops[1] / (double)run_ops[0]);
assert(run_ops[1]*110/100 > 2*run_ops[0]);
'''
# with uncreats, zombies, dirs, etc
[cases.test_exhaustion_spam_uzd_fuzz]
defines.ERASE_CYCLES = 10
defines.BLOCK_RECYCLES = 4
defines.BADBLOCK_BEHAVIOR = [
'LFS3_EMUBD_BADBLOCK_PROGERROR',
'LFS3_EMUBD_BADBLOCK_ERASEERROR',
'LFS3_EMUBD_BADBLOCK_READERROR',
'LFS3_EMUBD_BADBLOCK_PROGNOOP',
'LFS3_EMUBD_BADBLOCK_ERASENOOP',
]
# we need prog checking to detect read errors
defines.CKPROGS = 'BADBLOCK_BEHAVIOR >= LFS3_EMUBD_BADBLOCK_READERROR'
# you probably need to flush if you expect errors
defines.FLUSH = true
defines.N = [1, 2, 4, 8, 16, 32, 64]
defines.SIZE = [
'0',
'FCACHE_SIZE/2',
'2*FCACHE_SIZE',
'BLOCK_SIZE/2',
'BLOCK_SIZE',
'2*BLOCK_SIZE',
'4*BLOCK_SIZE',
]
defines.SEED = 42
fuzz = 'SEED'
if = [
'LFS3_IFDEF_CKPROGS(true, !CKPROGS)',
'(SIZE*N)/BLOCK_SIZE <= 16',
]
code = '''
// run our test twice, once with 1/2 the storage, once with 2/2 the
// storage, and compare how many operations we were able to perform
// before filesystem death
uint32_t run_bc[2] = {BLOCK_COUNT/2, BLOCK_COUNT};
uint32_t run_ops[2] = {0, 0};
for (int run = 0; run < 2; run++) {
// clear any wear from the previous run
for (lfs3_block_t i = 0; i < BLOCK_COUNT; i++) {
lfs3_emubd_setwear(CFG, i, 0) => 0;
}
// configure the filesystem size
struct lfs3_cfg cfg = *CFG;
cfg.block_count = run_bc[run];
// run the test
lfs3_t lfs3;
lfs3_format(&lfs3,
LFS3_F_RDWR
| ((CKPROGS) ? LFS3_IFDEF_CKPROGS(LFS3_F_CKPROGS, -1) : 0),
&cfg) => 0;
lfs3_mount(&lfs3,
LFS3_M_RDWR
| ((CKPROGS) ? LFS3_IFDEF_CKPROGS(LFS3_M_CKPROGS, -1) : 0),
&cfg) => 0;
// set up a simulation to compare against
lfs3_size_t *sim = malloc(N*sizeof(lfs3_size_t));
uint32_t *sim_prngs = malloc(N*sizeof(uint32_t));
bool *sim_isstickys = malloc(N*sizeof(bool));
bool *sim_isdirs = malloc(N*sizeof(bool));
lfs3_size_t sim_size = 0;
typedef struct sim_file {
lfs3_size_t x;
bool sticky;
bool zombie;
uint32_t prng;
lfs3_file_t file;
} sim_file_t;
sim_file_t **sim_files = malloc(N*sizeof(sim_file_t*));
lfs3_size_t sim_file_count = 0;
uint32_t prng = SEED;
for (;; run_ops[run]++) {
nonsense:;
// choose which operation to do
uint8_t op = TEST_PRNG(&prng) % 8;
// open a new file?
if (op == 0) {
if (sim_file_count >= N) {
goto nonsense;
}
// choose a pseudo-random number
lfs3_size_t x = TEST_PRNG(&prng) % N;
// already exists?
bool exist = true;
uint32_t wprng = 0;
bool sticky = true;
for (lfs3_size_t j = 0; j < sim_size; j++) {
if (sim[j] == x) {
if (sim_isdirs[j]) {
goto nonsense;
}
exist = true;
wprng = sim_prngs[j];
sticky = sim_isstickys[j];
break;
}
}
// choose a random seed if we don't exist
if (!exist) {
wprng = TEST_PRNG(&prng);
sticky = true;
}
lfs3_size_t j = sim_file_count;
sim_files[j] = malloc(sizeof(sim_file_t));
// open the actual file
char name[256];
sprintf(name, "batman%03x", x);
int err = lfs3_file_open(&lfs3, &sim_files[j]->file, name,
LFS3_O_RDWR
| LFS3_O_CREAT
| ((FLUSH) ? LFS3_O_FLUSH : 0));
assert(!err || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
free(sim_files[j]);
goto dead;
}
// write some initial data if we don't exist
if (!exist || sticky) {
uint8_t wbuf[SIZE];
uint32_t wprng_ = wprng;
for (lfs3_size_t k = 0; k < SIZE; k++) {
wbuf[k] = 'a' + (TEST_PRNG(&wprng_) % 26);
}
lfs3_ssize_t d = lfs3_file_write(&lfs3, &sim_files[j]->file,
wbuf, SIZE);
assert(d == SIZE || d == LFS3_ERR_NOSPC);
if (d == LFS3_ERR_NOSPC) {
lfs3_file_close(&lfs3, &sim_files[j]->file) => 0;
free(sim_files[j]);
goto dead;
}
}
// open in our sim
sim_files[j]->x = x;
sim_files[j]->sticky = sticky;
sim_files[j]->zombie = false;
sim_files[j]->prng = wprng;
sim_file_count++;
// insert into our sim
for (lfs3_size_t k = 0;; k++) {
if (k >= sim_size || sim[k] >= x) {
// already seen?
if (k < sim_size && sim[k] == x) {
// new prng
sim_prngs[k] = wprng;
} else {
// insert
memmove(&sim[k+1], &sim[k],
(sim_size-k)*sizeof(lfs3_size_t));
memmove(&sim_prngs[k+1], &sim_prngs[k],
(sim_size-k)*sizeof(uint32_t));
memmove(&sim_isstickys[k+1], &sim_isstickys[k],
(sim_size-k)*sizeof(bool));
memmove(&sim_isdirs[k+1], &sim_isdirs[k],
(sim_size-k)*sizeof(bool));
sim_size += 1;
sim[k] = x;
sim_prngs[k] = wprng;
sim_isstickys[k] = sticky;
sim_isdirs[k] = false;
}
break;
}
}
// write/rewrite a file?
} else if (op == 1) {
if (sim_file_count == 0) {
goto nonsense;
}
// choose a random file handle
lfs3_size_t j = TEST_PRNG(&prng) % sim_file_count;
lfs3_size_t x = sim_files[j]->x;
// choose a random seed
uint32_t wprng = TEST_PRNG(&prng);
// write to the file
lfs3_file_rewind(&lfs3, &sim_files[j]->file) => 0;
uint8_t wbuf[SIZE];
uint32_t wprng_ = wprng;
for (lfs3_size_t k = 0; k < SIZE; k++) {
wbuf[k] = 'a' + (TEST_PRNG(&wprng_) % 26);
}
lfs3_ssize_t d = lfs3_file_write(&lfs3, &sim_files[j]->file,
wbuf, SIZE);
assert(d == SIZE || d == LFS3_ERR_NOSPC);
if (d == LFS3_ERR_NOSPC) {
goto dead;
}
int err = lfs3_file_sync(&lfs3, &sim_files[j]->file);
assert(err == 0 || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
goto dead;
}
// update sim
sim_files[j]->prng = wprng;
if (!sim_files[j]->zombie) {
// update in our sim
for (lfs3_size_t k = 0;; k++) {
if (k >= sim_size || sim[k] >= x) {
// new prng
sim_prngs[k] = wprng;
// no longer sticky
sim_isstickys[k] = false;
break;
}
}
// update related sim files
for (lfs3_size_t k = 0; k < sim_file_count; k++) {
if (sim_files[k]->x == x && !sim_files[k]->zombie) {
// new prng
sim_files[k]->prng = wprng;
// no longer sticky
sim_files[k]->sticky = false;
}
}
}
// close a file?
} else if (op == 2) {
if (sim_file_count == 0) {
goto nonsense;
}
// choose a random file handle
lfs3_size_t j = TEST_PRNG(&prng) % sim_file_count;
lfs3_size_t x = sim_files[j]->x;
lfs3_size_t sticky = sim_files[j]->sticky;
lfs3_size_t zombie = sim_files[j]->zombie;
// this doesn't really test anything, but if we don't close
// files eventually everything will end up zombies
// close the file without affected disk
lfs3_file_desync(&lfs3, &sim_files[j]->file) => 0;
lfs3_file_close(&lfs3, &sim_files[j]->file) => 0;
// clobber closed files to try to catch lingering references
memset(&sim_files[j]->file, 0xcc, sizeof(lfs3_file_t));
// remove from list
free(sim_files[j]);
sim_files[j] = sim_files[sim_file_count-1];
sim_file_count -= 1;
// update our sim
if (sticky && !zombie) {
// orphaned?
bool orphan = true;
for (lfs3_size_t k = 0; k < sim_file_count; k++) {
if (sim_files[k]->x == x && !sim_files[k]->zombie) {
orphan = false;
}
}
// if we were never synced, delete from sim
if (orphan) {
for (lfs3_size_t k = 0;; k++) {
if (sim[k] == x) {
memmove(&sim[k], &sim[k+1],
(sim_size-(k+1))*sizeof(lfs3_size_t));
memmove(&sim_prngs[k], &sim_prngs[k+1],
(sim_size-(k+1))*sizeof(uint32_t));
memmove(&sim_isstickys[k], &sim_isstickys[k+1],
(sim_size-(k+1))*sizeof(bool));
memmove(&sim_isdirs[k], &sim_isdirs[k+1],
(sim_size-(k+1))*sizeof(bool));
sim_size -= 1;
break;
}
}
}
}
// remove a file?
} else if (op == 3) {
if (sim_size == 0) {
goto nonsense;
}
// choose a random file to delete
lfs3_size_t j = TEST_PRNG(&prng) % sim_size;
lfs3_size_t x = sim[j];
// delete this file
char name[256];
sprintf(name, "batman%03x", x);
int err = lfs3_remove(&lfs3, name);
assert(!err || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
goto dead;
}
// delete from our sim
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs3_size_t));
memmove(&sim_prngs[j], &sim_prngs[j+1],
(sim_size-(j+1))*sizeof(uint32_t));
memmove(&sim_isstickys[j], &sim_isstickys[j+1],
(sim_size-(j+1))*sizeof(bool));
memmove(&sim_isdirs[j], &sim_isdirs[j+1],
(sim_size-(j+1))*sizeof(bool));
sim_size -= 1;
// mark any related sim files as zombied
for (lfs3_size_t k = 0; k < sim_file_count; k++) {
if (sim_files[k]->x == x) {
sim_files[k]->zombie = true;
}
}
// rename a file?
} else if (op == 4) {
if (sim_size == 0) {
goto nonsense;
}
// choose a random file to rename, and a random number to
// rename to
lfs3_size_t j = TEST_PRNG(&prng) % sim_size;
lfs3_size_t x = sim[j];
lfs3_size_t y = TEST_PRNG(&prng) % N;
uint32_t wprng = sim_prngs[j];
bool sticky = sim_isstickys[j];
bool dir = sim_isdirs[j];
for (lfs3_size_t k = 0;; k++) {
if (k >= sim_size || sim[k] >= y) {
// renaming and replacing
if (k < sim_size && sim[k] == y && x != y) {
// type mismatch?
if (sim_isdirs[k] != dir) {
goto nonsense;
}
}
break;
}
}
// rename this file
char old_name[256];
sprintf(old_name, "batman%03x", x);
char new_name[256];
sprintf(new_name, "batman%03x", y);
int err = lfs3_rename(&lfs3, old_name, new_name);
assert(!err || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
goto dead;
}
// update our sim
for (lfs3_size_t k = 0;; k++) {
if (k >= sim_size || sim[k] >= y) {
// renaming and replacing
if (k < sim_size && sim[k] == y && x != y) {
// delete the original entry
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs3_size_t));
memmove(&sim_prngs[j], &sim_prngs[j+1],
(sim_size-(j+1))*sizeof(uint32_t));
memmove(&sim_isstickys[j], &sim_isstickys[j+1],
(sim_size-(j+1))*sizeof(bool));
memmove(&sim_isdirs[j], &sim_isdirs[j+1],
(sim_size-(j+1))*sizeof(bool));
sim_size -= 1;
if (k > j) {
k -= 1;
}
// update the prng/sticky/dir
sim_prngs[k] = wprng;
sim_isstickys[k] = sticky;
sim_isdirs[k] = dir;
// just renaming
} else {
// first delete
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs3_size_t));
memmove(&sim_prngs[j], &sim_prngs[j+1],
(sim_size-(j+1))*sizeof(uint32_t));
memmove(&sim_isstickys[j], &sim_isstickys[j+1],
(sim_size-(j+1))*sizeof(bool));
memmove(&sim_isdirs[j], &sim_isdirs[j+1],
(sim_size-(j+1))*sizeof(bool));
if (k > j) {
k -= 1;
}
// then insert
memmove(&sim[k+1], &sim[k],
(sim_size-1-k)*sizeof(lfs3_size_t));
memmove(&sim_prngs[k+1], &sim_prngs[k],
(sim_size-1-k)*sizeof(uint32_t));
memmove(&sim_isstickys[k+1], &sim_isstickys[k],
(sim_size-1-k)*sizeof(bool));
memmove(&sim_isdirs[k+1], &sim_isdirs[k],
(sim_size-1-k)*sizeof(bool));
sim[k] = y;
sim_prngs[k] = wprng;
sim_isstickys[k] = sticky;
sim_isdirs[k] = dir;
}
break;
}
}
// update any related sim files
for (lfs3_size_t k = 0; k < sim_file_count; k++) {
// move source files
if (sim_files[k]->x == x) {
sim_files[k]->x = y;
// mark target files as zombied
} else if (sim_files[k]->x == y) {
sim_files[k]->zombie = true;
}
}
// toss a directory into the mix
} else if (op == 5) {
// choose a pseudo-random number
lfs3_size_t x = TEST_PRNG(&prng) % N;
for (lfs3_size_t k = 0;; k++) {
if (k >= sim_size || sim[k] >= x) {
// already seen?
if (k < sim_size && sim[k] == x) {
goto nonsense;
}
break;
}
}
// make the directory
char name[256];
sprintf(name, "batman%03x", x);
int err = lfs3_mkdir(&lfs3, name);
assert(!err || err == LFS3_ERR_NOSPC);
if (err == LFS3_ERR_NOSPC) {
goto dead;
}
// insert into our sim
for (lfs3_size_t k = 0;; k++) {
if (k >= sim_size || sim[k] >= x) {
// insert
memmove(&sim[k+1], &sim[k],
(sim_size-k)*sizeof(lfs3_size_t));
memmove(&sim_prngs[k+1], &sim_prngs[k],
(sim_size-k)*sizeof(uint32_t));
memmove(&sim_isstickys[k+1], &sim_isstickys[k],
(sim_size-k)*sizeof(bool));
memmove(&sim_isdirs[k+1], &sim_isdirs[k],
(sim_size-k)*sizeof(bool));
sim_size += 1;
sim[k] = x;
sim_prngs[k] = 0;
sim_isdirs[k] = true;
break;
}
}
// mark any related sim files as zombied
for (lfs3_size_t k = 0; k < sim_file_count; k++) {
if (sim_files[k]->x == x) {
sim_files[k]->zombie = true;
}
}
}
// check our simulation every power-of-2 ops
if (lfs3_popc(run_ops[run]) == 1) {
// check that disk matches our simulation
for (lfs3_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "batman%03x", sim[j]);
struct lfs3_info info;
lfs3_stat(&lfs3, name, &info) => 0;
assert(strcmp(info.name, name) == 0);
if (sim_isdirs[j]) {
assert(info.type == LFS3_TYPE_DIR);
assert(info.size == 0);
} else if (sim_isstickys[j]) {
assert(info.type == LFS3_TYPE_STICKYNOTE);
assert(info.size == 0);
} else {
assert(info.type == LFS3_TYPE_REG);
assert(info.size == SIZE);
}
}
lfs3_dir_t dir;
lfs3_dir_open(&lfs3, &dir, "/") => 0;
struct lfs3_info info;
lfs3_dir_read(&lfs3, &dir, &info) => 0;
assert(strcmp(info.name, ".") == 0);
assert(info.type == LFS3_TYPE_DIR);
assert(info.size == 0);
lfs3_dir_read(&lfs3, &dir, &info) => 0;
assert(strcmp(info.name, "..") == 0);
assert(info.type == LFS3_TYPE_DIR);
assert(info.size == 0);
for (lfs3_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "batman%03x", sim[j]);
lfs3_dir_read(&lfs3, &dir, &info) => 0;
assert(strcmp(info.name, name) == 0);
if (sim_isdirs[j]) {
assert(info.type == LFS3_TYPE_DIR);
assert(info.size == 0);
} else if (sim_isstickys[j]) {
assert(info.type == LFS3_TYPE_STICKYNOTE);
assert(info.size == 0);
} else {
assert(info.type == LFS3_TYPE_REG);
assert(info.size == SIZE);
}
}
lfs3_dir_read(&lfs3, &dir, &info) => LFS3_ERR_NOENT;
lfs3_dir_close(&lfs3, &dir) => 0;
for (lfs3_size_t j = 0; j < sim_size; j++) {
if (sim_isdirs[j]) {
char name[256];
sprintf(name, "batman%03x", sim[j]);
lfs3_file_t file;
lfs3_file_open(&lfs3, &file, name, LFS3_O_RDONLY)
=> LFS3_ERR_ISDIR;
} else {
char name[256];
sprintf(name, "batman%03x", sim[j]);
lfs3_file_t file;
lfs3_file_open(&lfs3, &file, name, LFS3_O_RDONLY) => 0;
uint32_t wprng = sim_prngs[j];
uint8_t wbuf[SIZE];
for (lfs3_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&wprng) % 26);
}
uint8_t rbuf[SIZE];
if (sim_isstickys[j]) {
lfs3_file_read(&lfs3, &file, rbuf, SIZE) => 0;
} else {
lfs3_file_read(&lfs3, &file, rbuf, SIZE) => SIZE;
assert(memcmp(rbuf, wbuf, SIZE) == 0);
}
lfs3_file_close(&lfs3, &file) => 0;
}
}
// check that our file handles match our simulation
for (lfs3_size_t j = 0; j < sim_file_count; j++) {
uint32_t wprng = sim_files[j]->prng;
uint8_t wbuf[SIZE];
for (lfs3_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&wprng) % 26);
}
lfs3_file_rewind(&lfs3, &sim_files[j]->file) => 0;
uint8_t rbuf[SIZE];
lfs3_file_read(&lfs3, &sim_files[j]->file, rbuf, SIZE)
=> SIZE;
assert(memcmp(rbuf, wbuf, SIZE) == 0);
}
}
}
dead:;
// clean up sim/lfs3
free(sim);
free(sim_prngs);
free(sim_isstickys);
free(sim_isdirs);
for (lfs3_size_t j = 0; j < sim_file_count; j++) {
lfs3_file_desync(&lfs3, &sim_files[j]->file) => 0;
lfs3_file_close(&lfs3, &sim_files[j]->file) => 0;
free(sim_files[j]);
}
free(sim_files);
lfs3_unmount(&lfs3) => 0;
// print how many ops
printf("run %d, %dx%d, %d ec: %d ops\n",
run,
(int)BLOCK_SIZE,
run_bc[run],
(int)ERASE_CYCLES,
run_ops[run]);
}
// check that we increased the liftime by ~2x, with ~10% error
printf("lifetime: %d -> %d (x%.2f)\n",
run_ops[0],
run_ops[1],
(double)run_ops[1] / (double)run_ops[0]);
assert(run_ops[1]*110/100 > 2*run_ops[0]);
'''