Files
littlefs/tests/test_exhaustion.toml
T
Christopher Haster c648f96dc5 Added check_progs for immediate prog validation
This configuration option enables the previous behavior of reading back
every prog to check that the data was written correctly.

Unfortunately, this brings a bit of baggage, thanks to our cache
interactions being more complicated now:

- We really want to reuse the rcache for prog validation, despite the
  cache performance implications. Unfortunately, we simply can't, thanks
  to the new bd utility functions tying up the rcache. lfsr_bd_cpy, for
  example, does not expect rcache to be invalidated between a read and
  prog, and if it is, things break (I may or may not have found this by
  experience).

  These bd utilities are valuable, so we really need some other way to
  validate our progs.

- Since we can't rely on the rcache, this leaves checksumming as the
  only option for validating progs. Checksumming isn't perfect, as there
  is a decent chance of false negatives, but to be honest it's probably
  good enough for anything that's not malicious.

- This also adds the new constraint that we need to be able to read back
  any prog into the pcache, which implies read_size <= prog_size. This
  constraint didn't exist when we could clobber our rcache, but this is
  not worth throwing away the new bd utilities. Not to mention
  clobbering our rcache could hurt cache performance.

  Why not make read_size <= prog_size conditional on check_progs?

  The main reason is convenience. One very compelling use case for
  check_progs is to help debug unknown filesystem/integration failures,
  buf if you can't enable check_progs without changing the filesystem
  configuration, you can't really rely on check_progs for debugging.

  This helps future proof what we expect from block devices, in case
  future error detection/correction mechanisms can benefit from our
  prog_size always being readable.

Code changes were not that significant, however there was a surprising
stack cost. This seems to be because lfsr_bd_read__ can now be called
from multiple places, causing it to no longer be inlined in
lfsr_bd_read_, costing a bit of stack for the additional function call:

  before: 33566           2624
  after:  33682 (+0.3%)   2640 (+0.6%)
2024-05-29 23:09:41 -05:00

1909 lines
70 KiB
TOML

# Test running a filesystem to exhaustion and its effects on wear-leveling
after = [
'test_dirs',
'test_files',
'test_forphans',
'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_dir_fuzz]
defines.ERASE_CYCLES = 10
defines.BLOCK_RECYCLES = 4
defines.BADBLOCK_BEHAVIOR = [
'LFS_EMUBD_BADBLOCK_PROGERROR',
'LFS_EMUBD_BADBLOCK_ERASEERROR',
'LFS_EMUBD_BADBLOCK_READERROR',
'LFS_EMUBD_BADBLOCK_PROGNOOP',
'LFS_EMUBD_BADBLOCK_ERASENOOP',
]
# we need prog checking to detect read errors
defines.CHECK_PROGS = 'BADBLOCK_BEHAVIOR >= LFS_EMUBD_BADBLOCK_READERROR'
defines.N = [1, 2, 4, 8, 16, 32, 64, 128, 256]
defines.SEED = 42
fuzz = 'SEED'
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 (lfs_block_t i = 0; i < BLOCK_COUNT; i++) {
lfs_emubd_setwear(CFG, i, 0) => 0;
}
// configure the filesystem size
struct lfs_config cfg = *CFG;
cfg.block_count = run_bc[run];
// run the test
lfs_t lfs;
lfsr_format(&lfs, &cfg) => 0;
lfsr_mount(&lfs, &cfg) => 0;
// set up a simulation to compare against
lfs_size_t *sim = malloc(N*sizeof(lfs_size_t));
lfs_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
lfs_size_t x = TEST_PRNG(&prng) % N;
// insert into our sim
for (lfs_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(lfs_size_t));
sim_size += 1;
sim[j] = x;
}
break;
}
}
// create a directory here
char name[256];
sprintf(name, "dir%03x", x);
int err = lfsr_mkdir(&lfs, name);
assert(!err || err == LFS_ERR_EXIST || err == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
} else if (op == 1) {
// choose a pseudo-random entry to delete
lfs_size_t j = TEST_PRNG(&prng) % sim_size;
lfs_size_t x = sim[j];
// delete from our sim
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs_size_t));
sim_size -= 1;
// remove this directory
char name[256];
sprintf(name, "dir%03x", x);
int err = lfsr_remove(&lfs, name);
assert(!err || err == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
} else {
// choose a pseudo-random entry to rename, and a pseudo-random
// number to rename to
lfs_size_t j = TEST_PRNG(&prng) % sim_size;
lfs_size_t x = sim[j];
lfs_size_t y = TEST_PRNG(&prng) % N;
for (lfs_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(lfs_size_t));
sim_size -= 1;
} else {
// first delete
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs_size_t));
if (k > j) {
k -= 1;
}
// then insert
memmove(&sim[k+1], &sim[k],
(sim_size-k)*sizeof(lfs_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 = lfsr_rename(&lfs, old_name, new_name);
assert(!err || err == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
}
// check our simulation every power-of-2 ops
if (lfs_popc(run_ops[run]) == 1) {
// test that our directories match our simulation
for (lfs_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "dir%03x", sim[j]);
struct lfs_info info;
lfsr_stat(&lfs, name, &info) => 0;
char name2[256];
sprintf(name2, "dir%03x", sim[j]);
assert(strcmp(info.name, name2) == 0);
assert(info.type == LFS_TYPE_DIR);
assert(info.size == 0);
}
lfsr_dir_t dir;
lfsr_dir_open(&lfs, &dir, "/") => 0;
struct lfs_info info;
lfsr_dir_read(&lfs, &dir, &info) => 0;
assert(strcmp(info.name, ".") == 0);
assert(info.type == LFS_TYPE_DIR);
assert(info.size == 0);
lfsr_dir_read(&lfs, &dir, &info) => 0;
assert(strcmp(info.name, "..") == 0);
assert(info.type == LFS_TYPE_DIR);
assert(info.size == 0);
for (lfs_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "dir%03x", sim[j]);
lfsr_dir_read(&lfs, &dir, &info) => 0;
assert(strcmp(info.name, name) == 0);
assert(info.type == LFS_TYPE_DIR);
assert(info.size == 0);
}
lfsr_dir_read(&lfs, &dir, &info) => LFS_ERR_NOENT;
lfsr_dir_close(&lfs, &dir) => 0;
}
}
dead:;
// clean up sim/lfs
free(sim);
lfsr_unmount(&lfs) => 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_file_fuzz]
defines.ERASE_CYCLES = 10
defines.BLOCK_RECYCLES = 4
defines.BADBLOCK_BEHAVIOR = [
'LFS_EMUBD_BADBLOCK_PROGERROR',
'LFS_EMUBD_BADBLOCK_ERASEERROR',
'LFS_EMUBD_BADBLOCK_READERROR',
'LFS_EMUBD_BADBLOCK_PROGNOOP',
'LFS_EMUBD_BADBLOCK_ERASENOOP',
]
# we need prog checking to detect read errors
defines.CHECK_PROGS = 'BADBLOCK_BEHAVIOR >= LFS_EMUBD_BADBLOCK_READERROR'
defines.N = [1, 2, 4, 8, 16, 32, 64]
defines.SIZE = [
'0',
'FBUFFER_SIZE/2',
'2*FBUFFER_SIZE',
'BLOCK_SIZE/2',
'BLOCK_SIZE',
'2*BLOCK_SIZE',
'4*BLOCK_SIZE',
]
defines.SEED = 42
fuzz = 'SEED'
if = '(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 (lfs_block_t i = 0; i < BLOCK_COUNT; i++) {
lfs_emubd_setwear(CFG, i, 0) => 0;
}
// configure the filesystem size
struct lfs_config cfg = *CFG;
cfg.block_count = run_bc[run];
// run the test
lfs_t lfs;
lfsr_format(&lfs, &cfg) => 0;
lfsr_mount(&lfs, &cfg) => 0;
// set up a simulation to compare against
lfs_size_t *sim = malloc(N*sizeof(lfs_size_t));
uint32_t *sim_prngs = malloc(N*sizeof(uint32_t));
lfs_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
lfs_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 (lfs_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(lfs_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 (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&wprng) % 26);
}
lfsr_file_t file;
int err = lfsr_file_open(&lfs, &file, name,
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC);
assert(!err || err == LFS_ERR_NOSPC);
if (err) {
goto dead;
}
lfs_ssize_t d = lfsr_file_write(&lfs, &file, wbuf, SIZE);
assert(d == SIZE || d == LFS_ERR_NOSPC);
if (d == LFS_ERR_NOSPC) {
goto dead;
}
err = lfsr_file_close(&lfs, &file);
assert(!err || err == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
// deleting a file?
} else if (op == 1) {
// choose a random file to delete
lfs_size_t j = TEST_PRNG(&prng) % sim_size;
lfs_size_t x = sim[j];
// delete from our sim
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs_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 = lfsr_remove(&lfs, name);
assert(!err || err == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
// renaming a file?
} else {
// choose a random file to rename, and a random number to
// rename to
lfs_size_t j = TEST_PRNG(&prng) % sim_size;
lfs_size_t x = sim[j];
lfs_size_t y = TEST_PRNG(&prng) % N;
uint32_t wprng = sim_prngs[j];
// update our sim
for (lfs_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(lfs_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(lfs_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-k)*sizeof(lfs_size_t));
memmove(&sim_prngs[k+1], &sim_prngs[k],
(sim_size-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 = lfsr_rename(&lfs, old_name, new_name);
assert(!err || err == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
}
// check our simulation every power-of-2 ops
if (lfs_popc(run_ops[run]) == 1) {
// check that our files match our simulation
for (lfs_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "amethyst%03x", sim[j]);
struct lfs_info info;
lfsr_stat(&lfs, name, &info) => 0;
assert(strcmp(info.name, name) == 0);
assert(info.type == LFS_TYPE_REG);
assert(info.size == SIZE);
}
lfsr_dir_t dir;
lfsr_dir_open(&lfs, &dir, "/") => 0;
struct lfs_info info;
lfsr_dir_read(&lfs, &dir, &info) => 0;
assert(strcmp(info.name, ".") == 0);
assert(info.type == LFS_TYPE_DIR);
assert(info.size == 0);
lfsr_dir_read(&lfs, &dir, &info) => 0;
assert(strcmp(info.name, "..") == 0);
assert(info.type == LFS_TYPE_DIR);
assert(info.size == 0);
for (lfs_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "amethyst%03x", sim[j]);
lfsr_dir_read(&lfs, &dir, &info) => 0;
assert(strcmp(info.name, name) == 0);
assert(info.type == LFS_TYPE_REG);
assert(info.size == SIZE);
}
lfsr_dir_read(&lfs, &dir, &info) => LFS_ERR_NOENT;
lfsr_dir_close(&lfs, &dir) => 0;
// check the file contents
for (lfs_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "amethyst%03x", sim[j]);
lfsr_file_t file;
lfsr_file_open(&lfs, &file, name, LFS_O_RDONLY) => 0;
uint32_t wprng = sim_prngs[j];
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&wprng) % 26);
}
uint8_t rbuf[SIZE];
lfsr_file_read(&lfs, &file, rbuf, SIZE) => SIZE;
assert(memcmp(rbuf, wbuf, SIZE) == 0);
lfsr_file_close(&lfs, &file) => 0;
}
}
}
dead:;
// clean up sim/lfs
free(sim);
free(sim_prngs);
lfsr_unmount(&lfs) => 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]);
'''
# just more things that could go wrong
[cases.test_exhaustion_orphanzombie_fuzz]
defines.ERASE_CYCLES = 10
defines.BLOCK_RECYCLES = 4
defines.BADBLOCK_BEHAVIOR = [
'LFS_EMUBD_BADBLOCK_PROGERROR',
'LFS_EMUBD_BADBLOCK_ERASEERROR',
'LFS_EMUBD_BADBLOCK_READERROR',
'LFS_EMUBD_BADBLOCK_PROGNOOP',
'LFS_EMUBD_BADBLOCK_ERASENOOP',
]
# we need prog checking to detect read errors
defines.CHECK_PROGS = 'BADBLOCK_BEHAVIOR >= LFS_EMUBD_BADBLOCK_READERROR'
defines.N = [1, 2, 4, 8, 16, 32, 64]
defines.SIZE = [
'0',
'FBUFFER_SIZE/2',
'2*FBUFFER_SIZE',
'BLOCK_SIZE/2',
'BLOCK_SIZE',
'2*BLOCK_SIZE',
'4*BLOCK_SIZE',
]
defines.SEED = 42
fuzz = 'SEED'
if = '(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 (lfs_block_t i = 0; i < BLOCK_COUNT; i++) {
lfs_emubd_setwear(CFG, i, 0) => 0;
}
// configure the filesystem size
struct lfs_config cfg = *CFG;
cfg.block_count = run_bc[run];
// run the test
lfs_t lfs;
lfsr_format(&lfs, &cfg) => 0;
lfsr_mount(&lfs, &cfg) => 0;
// set up a simulation to compare against
lfs_size_t *sim = malloc(N*sizeof(lfs_size_t));
uint32_t *sim_prngs = malloc(N*sizeof(uint32_t));
lfs_size_t sim_size = 0;
typedef struct sim_file {
lfs_size_t x;
bool orphan;
bool zombie;
uint32_t prng;
lfsr_file_t file;
} sim_file_t;
sim_file_t **sim_files = malloc(N*sizeof(sim_file_t*));
lfs_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
lfs_size_t x = TEST_PRNG(&prng) % N;
// already exists?
bool orphan = true;
uint32_t wprng = 0;
for (lfs_size_t j = 0; j < sim_size; j++) {
if (sim[j] == x) {
orphan = false;
wprng = sim_prngs[j];
break;
}
}
// choose a random seed if we don't exist
if (orphan) {
wprng = TEST_PRNG(&prng);
}
// open in our sim
lfs_size_t j = sim_file_count;
sim_files[j] = malloc(sizeof(sim_file_t));
sim_files[j]->x = x;
sim_files[j]->orphan = orphan;
sim_files[j]->zombie = false;
sim_files[j]->prng = wprng;
// open the actual file
char name[256];
sprintf(name, "batman%03x", x);
int err = lfsr_file_open(&lfs, &sim_files[j]->file, name,
LFS_O_RDWR | LFS_O_CREAT);
assert(!err || err == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
sim_file_count++;
// write some initial data if we don't exist
if (orphan) {
uint8_t wbuf[SIZE];
for (lfs_size_t k = 0; k < SIZE; k++) {
wbuf[k] = 'a' + (TEST_PRNG(&wprng) % 26);
}
lfs_ssize_t d = lfsr_file_write(&lfs, &sim_files[j]->file,
wbuf, SIZE);
assert(d == SIZE || d == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
}
// write/rewrite a file?
} else if (op == 1) {
if (sim_file_count == 0) {
goto nonsense;
}
// choose a random file handle
lfs_size_t j = TEST_PRNG(&prng) % sim_file_count;
lfs_size_t x = sim_files[j]->x;
// choose a random seed
uint32_t wprng = TEST_PRNG(&prng);
// update sim
sim_files[j]->prng = wprng;
if (!sim_files[j]->zombie) {
// insert into our sim
for (lfs_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(lfs_size_t));
memmove(&sim_prngs[k+1], &sim_prngs[k],
(sim_size-k)*sizeof(uint32_t));
sim_size += 1;
sim[k] = x;
sim_prngs[k] = wprng;
}
break;
}
}
// update related sim files
for (lfs_size_t k = 0; k < sim_file_count; k++) {
if (sim_files[k]->x == x && !sim_files[k]->zombie) {
sim_files[k]->orphan = false;
sim_files[k]->prng = wprng;
}
}
}
// write to the file
lfsr_file_rewind(&lfs, &sim_files[j]->file) => 0;
uint8_t wbuf[SIZE];
for (lfs_size_t k = 0; k < SIZE; k++) {
wbuf[k] = 'a' + (TEST_PRNG(&wprng) % 26);
}
lfs_ssize_t d = lfsr_file_write(&lfs, &sim_files[j]->file,
wbuf, SIZE);
assert(d == SIZE || d == LFS_ERR_NOSPC);
if (d == LFS_ERR_NOSPC) {
goto dead;
}
int err = lfsr_file_sync(&lfs, &sim_files[j]->file);
assert(err == ((!sim_files[j]->zombie) ? 0 : LFS_ERR_NOENT)
|| err == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
// close a file?
} else if (op == 2) {
if (sim_file_count == 0) {
goto nonsense;
}
// choose a random file handle
lfs_size_t j = TEST_PRNG(&prng) % sim_file_count;
// 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
lfsr_file_desync(&lfs, &sim_files[j]->file) => 0;
lfsr_file_close(&lfs, &sim_files[j]->file) => 0;
// remove from list
free(sim_files[j]);
sim_files[j] = sim_files[sim_file_count-1];
sim_file_count -= 1;
// remove a file?
} else if (op == 3) {
if (sim_size == 0) {
goto nonsense;
}
// choose a random file to delete
lfs_size_t j = TEST_PRNG(&prng) % sim_size;
lfs_size_t x = sim[j];
// delete from our sim
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs_size_t));
memmove(&sim_prngs[j], &sim_prngs[j+1],
(sim_size-(j+1))*sizeof(uint32_t));
sim_size -= 1;
// mark any related sim files as zombied
for (lfs_size_t k = 0; k < sim_file_count; k++) {
if (sim_files[k]->x == x) {
sim_files[k]->zombie = true;
}
}
// delete this file
char name[256];
sprintf(name, "batman%03x", x);
int err = lfsr_remove(&lfs, name);
assert(!err || err == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
// 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
lfs_size_t j = TEST_PRNG(&prng) % sim_size;
lfs_size_t x = sim[j];
lfs_size_t y = TEST_PRNG(&prng) % N;
uint32_t wprng = sim_prngs[j];
// update our sim
for (lfs_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(lfs_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(lfs_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-k)*sizeof(lfs_size_t));
memmove(&sim_prngs[k+1], &sim_prngs[k],
(sim_size-k)*sizeof(uint32_t));
sim[k] = y;
sim_prngs[k] = wprng;
}
break;
}
}
// update any related sim files
for (lfs_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;
}
}
// 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 = lfsr_rename(&lfs, old_name, new_name);
assert(!err || err == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
}
// check our simulation every power-of-2 ops
if (lfs_popc(run_ops[run]) == 1) {
// check that disk matches our simulation
for (lfs_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "batman%03x", sim[j]);
struct lfs_info info;
lfsr_stat(&lfs, name, &info) => 0;
assert(strcmp(info.name, name) == 0);
assert(info.type == LFS_TYPE_REG);
assert(info.size == SIZE);
}
lfsr_dir_t dir;
lfsr_dir_open(&lfs, &dir, "/") => 0;
struct lfs_info info;
lfsr_dir_read(&lfs, &dir, &info) => 0;
assert(strcmp(info.name, ".") == 0);
assert(info.type == LFS_TYPE_DIR);
assert(info.size == 0);
lfsr_dir_read(&lfs, &dir, &info) => 0;
assert(strcmp(info.name, "..") == 0);
assert(info.type == LFS_TYPE_DIR);
assert(info.size == 0);
for (lfs_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "batman%03x", sim[j]);
lfsr_dir_read(&lfs, &dir, &info) => 0;
assert(strcmp(info.name, name) == 0);
assert(info.type == LFS_TYPE_REG);
assert(info.size == SIZE);
}
lfsr_dir_read(&lfs, &dir, &info) => LFS_ERR_NOENT;
lfsr_dir_close(&lfs, &dir) => 0;
for (lfs_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "batman%03x", sim[j]);
lfsr_file_t file;
lfsr_file_open(&lfs, &file, name, LFS_O_RDONLY) => 0;
uint32_t wprng = sim_prngs[j];
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&wprng) % 26);
}
uint8_t rbuf[SIZE];
lfsr_file_read(&lfs, &file, rbuf, SIZE) => SIZE;
assert(memcmp(rbuf, wbuf, SIZE) == 0);
lfsr_file_close(&lfs, &file) => 0;
}
// check that our file handles match our simulation
for (lfs_size_t j = 0; j < sim_file_count; j++) {
uint32_t wprng = sim_files[j]->prng;
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&wprng) % 26);
}
lfsr_file_rewind(&lfs, &sim_files[j]->file) => 0;
uint8_t rbuf[SIZE];
lfsr_file_read(&lfs, &sim_files[j]->file,
rbuf, SIZE) => SIZE;
assert(memcmp(rbuf, wbuf, SIZE) == 0);
}
}
}
dead:;
// clean up sim/lfs
free(sim);
free(sim_prngs);
for (lfs_size_t j = 0; j < sim_file_count; j++) {
lfsr_file_desync(&lfs, &sim_files[j]->file) => 0;
lfsr_file_close(&lfs, &sim_files[j]->file) => 0;
free(sim_files[j]);
}
free(sim_files);
lfsr_unmount(&lfs) => 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]);
'''
# just more things that could go wrong
[cases.test_exhaustion_orphanzombiedir_fuzz]
defines.ERASE_CYCLES = 10
defines.BLOCK_RECYCLES = 4
defines.BADBLOCK_BEHAVIOR = [
'LFS_EMUBD_BADBLOCK_PROGERROR',
'LFS_EMUBD_BADBLOCK_ERASEERROR',
'LFS_EMUBD_BADBLOCK_READERROR',
'LFS_EMUBD_BADBLOCK_PROGNOOP',
'LFS_EMUBD_BADBLOCK_ERASENOOP',
]
# we need prog checking to detect read errors
defines.CHECK_PROGS = 'BADBLOCK_BEHAVIOR >= LFS_EMUBD_BADBLOCK_READERROR'
defines.N = [1, 2, 4, 8, 16, 32, 64]
defines.SIZE = [
'0',
'FBUFFER_SIZE/2',
'2*FBUFFER_SIZE',
'BLOCK_SIZE/2',
'BLOCK_SIZE',
'2*BLOCK_SIZE',
'4*BLOCK_SIZE',
]
defines.SEED = 42
fuzz = 'SEED'
if = '(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 (lfs_block_t i = 0; i < BLOCK_COUNT; i++) {
lfs_emubd_setwear(CFG, i, 0) => 0;
}
// configure the filesystem size
struct lfs_config cfg = *CFG;
cfg.block_count = run_bc[run];
// run the test
lfs_t lfs;
lfsr_format(&lfs, &cfg) => 0;
lfsr_mount(&lfs, &cfg) => 0;
// set up a simulation to compare against
lfs_size_t *sim = malloc(N*sizeof(lfs_size_t));
uint32_t *sim_prngs = malloc(N*sizeof(uint32_t));
bool *sim_isdirs = malloc(N*sizeof(bool));
lfs_size_t sim_size = 0;
typedef struct sim_file {
lfs_size_t x;
bool orphan;
bool zombie;
uint32_t prng;
lfsr_file_t file;
} sim_file_t;
sim_file_t **sim_files = malloc(N*sizeof(sim_file_t*));
lfs_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
lfs_size_t x = TEST_PRNG(&prng) % N;
// already exists?
bool orphan = true;
uint32_t wprng = 0;
for (lfs_size_t j = 0; j < sim_size; j++) {
if (sim[j] == x) {
if (sim_isdirs[j]) {
goto nonsense;
}
orphan = false;
wprng = sim_prngs[j];
break;
}
}
// choose a random seed if we don't exist
if (orphan) {
wprng = TEST_PRNG(&prng);
}
// open in our sim
lfs_size_t j = sim_file_count;
sim_files[j] = malloc(sizeof(sim_file_t));
sim_files[j]->x = x;
sim_files[j]->orphan = orphan;
sim_files[j]->zombie = false;
sim_files[j]->prng = wprng;
// open the actual file
char name[256];
sprintf(name, "batman%03x", x);
int err = lfsr_file_open(&lfs, &sim_files[j]->file, name,
LFS_O_RDWR | LFS_O_CREAT);
assert(!err || err == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
sim_file_count++;
// write some initial data if we don't exist
if (orphan) {
uint8_t wbuf[SIZE];
for (lfs_size_t k = 0; k < SIZE; k++) {
wbuf[k] = 'a' + (TEST_PRNG(&wprng) % 26);
}
lfs_ssize_t d = lfsr_file_write(&lfs, &sim_files[j]->file,
wbuf, SIZE);
assert(d == SIZE || d == LFS_ERR_NOSPC);
if (d == LFS_ERR_NOSPC) {
goto dead;
}
}
// write/rewrite a file?
} else if (op == 1) {
if (sim_file_count == 0) {
goto nonsense;
}
// choose a random file handle
lfs_size_t j = TEST_PRNG(&prng) % sim_file_count;
lfs_size_t x = sim_files[j]->x;
// choose a random seed
uint32_t wprng = TEST_PRNG(&prng);
// update sim
sim_files[j]->prng = wprng;
if (!sim_files[j]->zombie) {
// insert into our sim
for (lfs_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(lfs_size_t));
memmove(&sim_prngs[k+1], &sim_prngs[k],
(sim_size-k)*sizeof(uint32_t));
memmove(&sim_isdirs[k+1], &sim_isdirs[k],
(sim_size-k)*sizeof(bool));
sim_size += 1;
sim[k] = x;
sim_prngs[k] = wprng;
sim_isdirs[k] = false;
}
break;
}
}
// update related sim files
for (lfs_size_t k = 0; k < sim_file_count; k++) {
if (sim_files[k]->x == x && !sim_files[k]->zombie) {
sim_files[k]->orphan = false;
sim_files[k]->prng = wprng;
}
}
}
// write to the file
lfsr_file_rewind(&lfs, &sim_files[j]->file) => 0;
uint8_t wbuf[SIZE];
for (lfs_size_t k = 0; k < SIZE; k++) {
wbuf[k] = 'a' + (TEST_PRNG(&wprng) % 26);
}
lfs_ssize_t d = lfsr_file_write(&lfs, &sim_files[j]->file,
wbuf, SIZE);
assert(d == SIZE || d == LFS_ERR_NOSPC);
if (d == LFS_ERR_NOSPC) {
goto dead;
}
int err = lfsr_file_sync(&lfs, &sim_files[j]->file);
assert(err == ((!sim_files[j]->zombie) ? 0 : LFS_ERR_NOENT)
|| err == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
// close a file?
} else if (op == 2) {
if (sim_file_count == 0) {
goto nonsense;
}
// choose a random file handle
lfs_size_t j = TEST_PRNG(&prng) % sim_file_count;
// 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
lfsr_file_desync(&lfs, &sim_files[j]->file) => 0;
lfsr_file_close(&lfs, &sim_files[j]->file) => 0;
// remove from list
free(sim_files[j]);
sim_files[j] = sim_files[sim_file_count-1];
sim_file_count -= 1;
// remove a file?
} else if (op == 3) {
if (sim_size == 0) {
goto nonsense;
}
// choose a random file to delete
lfs_size_t j = TEST_PRNG(&prng) % sim_size;
lfs_size_t x = sim[j];
// delete from our sim
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs_size_t));
memmove(&sim_prngs[j], &sim_prngs[j+1],
(sim_size-(j+1))*sizeof(uint32_t));
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 (lfs_size_t k = 0; k < sim_file_count; k++) {
if (sim_files[k]->x == x) {
sim_files[k]->zombie = true;
}
}
// delete this file
char name[256];
sprintf(name, "batman%03x", x);
int err = lfsr_remove(&lfs, name);
assert(!err || err == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
// 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
lfs_size_t j = TEST_PRNG(&prng) % sim_size;
lfs_size_t x = sim[j];
lfs_size_t y = TEST_PRNG(&prng) % N;
uint32_t wprng = sim_prngs[j];
bool isdir = sim_isdirs[j];
// update our sim
for (lfs_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] != isdir) {
goto nonsense;
}
// delete the original entry
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs_size_t));
memmove(&sim_prngs[j], &sim_prngs[j+1],
(sim_size-(j+1))*sizeof(uint32_t));
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
sim_prngs[k] = wprng;
// just renaming
} else {
// first delete
memmove(&sim[j], &sim[j+1],
(sim_size-(j+1))*sizeof(lfs_size_t));
memmove(&sim_prngs[j], &sim_prngs[j+1],
(sim_size-(j+1))*sizeof(uint32_t));
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-k)*sizeof(lfs_size_t));
memmove(&sim_prngs[k+1], &sim_prngs[k],
(sim_size-k)*sizeof(uint32_t));
memmove(&sim_isdirs[k+1], &sim_isdirs[k],
(sim_size-k)*sizeof(bool));
sim[k] = y;
sim_prngs[k] = wprng;
sim_isdirs[k] = isdir;
}
break;
}
}
// update any related sim files
for (lfs_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;
}
}
// 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 = lfsr_rename(&lfs, old_name, new_name);
assert(!err || err == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
// toss a directory into the mix
} else if (op == 5) {
// choose a pseudo-random number
lfs_size_t x = TEST_PRNG(&prng) % N;
// insert into our sim, use negative numbers for dirs
for (lfs_size_t k = 0;; k++) {
if (k >= sim_size || sim[k] >= x) {
// already seen?
if (k < sim_size && sim[k] == x) {
goto nonsense;
} else {
// insert
memmove(&sim[k+1], &sim[k],
(sim_size-k)*sizeof(lfs_size_t));
memmove(&sim_prngs[k+1], &sim_prngs[k],
(sim_size-k)*sizeof(uint32_t));
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 (lfs_size_t k = 0; k < sim_file_count; k++) {
if (sim_files[k]->x == x) {
sim_files[k]->zombie = true;
}
}
// make the directory
char name[256];
sprintf(name, "batman%03x", x);
int err = lfsr_mkdir(&lfs, name);
assert(!err || err == LFS_ERR_NOSPC);
if (err == LFS_ERR_NOSPC) {
goto dead;
}
}
// check our simulation every power-of-2 ops
if (lfs_popc(run_ops[run]) == 1) {
// check that disk matches our simulation
for (lfs_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "batman%03x", sim[j]);
struct lfs_info info;
lfsr_stat(&lfs, name, &info) => 0;
assert(strcmp(info.name, name) == 0);
if (sim_isdirs[j]) {
assert(info.type == LFS_TYPE_DIR);
} else {
assert(info.type == LFS_TYPE_REG);
assert(info.size == SIZE);
}
}
lfsr_dir_t dir;
lfsr_dir_open(&lfs, &dir, "/") => 0;
struct lfs_info info;
lfsr_dir_read(&lfs, &dir, &info) => 0;
assert(strcmp(info.name, ".") == 0);
assert(info.type == LFS_TYPE_DIR);
assert(info.size == 0);
lfsr_dir_read(&lfs, &dir, &info) => 0;
assert(strcmp(info.name, "..") == 0);
assert(info.type == LFS_TYPE_DIR);
assert(info.size == 0);
for (lfs_size_t j = 0; j < sim_size; j++) {
char name[256];
sprintf(name, "batman%03x", sim[j]);
lfsr_dir_read(&lfs, &dir, &info) => 0;
assert(strcmp(info.name, name) == 0);
if (sim_isdirs[j]) {
assert(info.type == LFS_TYPE_DIR);
} else {
assert(info.type == LFS_TYPE_REG);
assert(info.size == SIZE);
}
}
lfsr_dir_read(&lfs, &dir, &info) => LFS_ERR_NOENT;
lfsr_dir_close(&lfs, &dir) => 0;
for (lfs_size_t j = 0; j < sim_size; j++) {
if (sim_isdirs[j]) {
char name[256];
sprintf(name, "batman%03x", sim[j]);
lfsr_file_t file;
lfsr_file_open(&lfs, &file, name, LFS_O_RDONLY)
=> LFS_ERR_ISDIR;
} else {
char name[256];
sprintf(name, "batman%03x", sim[j]);
lfsr_file_t file;
lfsr_file_open(&lfs, &file, name, LFS_O_RDONLY) => 0;
uint32_t wprng = sim_prngs[j];
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&wprng) % 26);
}
uint8_t rbuf[SIZE];
lfsr_file_read(&lfs, &file, rbuf, SIZE) => SIZE;
assert(memcmp(rbuf, wbuf, SIZE) == 0);
lfsr_file_close(&lfs, &file) => 0;
}
}
// check that our file handles match our simulation
for (lfs_size_t j = 0; j < sim_file_count; j++) {
uint32_t wprng = sim_files[j]->prng;
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&wprng) % 26);
}
lfsr_file_rewind(&lfs, &sim_files[j]->file) => 0;
uint8_t rbuf[SIZE];
lfsr_file_read(&lfs, &sim_files[j]->file, rbuf, SIZE)
=> SIZE;
assert(memcmp(rbuf, wbuf, SIZE) == 0);
}
}
}
dead:;
// clean up sim/lfs
free(sim);
free(sim_prngs);
for (lfs_size_t j = 0; j < sim_file_count; j++) {
lfsr_file_desync(&lfs, &sim_files[j]->file) => 0;
lfsr_file_close(&lfs, &sim_files[j]->file) => 0;
free(sim_files[j]);
}
free(sim_files);
lfsr_unmount(&lfs) => 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 running a filesystem to exhaustion
#[cases.test_exhaustion_normal]
#defines.ERASE_CYCLES = 10
#defines.BLOCK_COUNT = 256 # small bd so test runs faster
#defines.BLOCK_CYCLES = 'ERASE_CYCLES / 2'
#defines.BADBLOCK_BEHAVIOR = [
# 'LFS_EMUBD_BADBLOCK_PROGERROR',
# 'LFS_EMUBD_BADBLOCK_ERASEERROR',
# 'LFS_EMUBD_BADBLOCK_READERROR',
# 'LFS_EMUBD_BADBLOCK_PROGNOOP',
# 'LFS_EMUBD_BADBLOCK_ERASENOOP',
#]
#defines.FILES = 10
#code = '''
# lfs_t lfs;
# lfs_format(&lfs, cfg) => 0;
# lfs_mount(&lfs, cfg) => 0;
# lfs_mkdir(&lfs, "roadrunner") => 0;
# lfs_unmount(&lfs) => 0;
#
# uint32_t cycle = 0;
# while (true) {
# lfs_mount(&lfs, cfg) => 0;
# for (uint32_t i = 0; i < FILES; i++) {
# // chose name, roughly random seed, and random 2^n size
# char path[1024];
# sprintf(path, "roadrunner/test%d", i);
# uint32_t prng = cycle * i;
# lfs_size_t size = 1 << ((TEST_PRNG(&prng) % 10)+2);
#
# lfs_file_t file;
# lfs_file_open(&lfs, &file, path,
# LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC) => 0;
#
# for (lfs_size_t j = 0; j < size; j++) {
# char c = 'a' + (TEST_PRNG(&prng) % 26);
# lfs_ssize_t res = lfs_file_write(&lfs, &file, &c, 1);
# assert(res == 1 || res == LFS_ERR_NOSPC);
# if (res == LFS_ERR_NOSPC) {
# int err = lfs_file_close(&lfs, &file);
# assert(err == 0 || err == LFS_ERR_NOSPC);
# lfs_unmount(&lfs) => 0;
# goto exhausted;
# }
# }
#
# int err = lfs_file_close(&lfs, &file);
# assert(err == 0 || err == LFS_ERR_NOSPC);
# if (err == LFS_ERR_NOSPC) {
# lfs_unmount(&lfs) => 0;
# goto exhausted;
# }
# }
#
# for (uint32_t i = 0; i < FILES; i++) {
# // check for errors
# char path[1024];
# sprintf(path, "roadrunner/test%d", i);
# uint32_t prng = cycle * i;
# lfs_size_t size = 1 << ((TEST_PRNG(&prng) % 10)+2);
#
# lfs_file_t file;
# lfs_file_open(&lfs, &file, path, LFS_O_RDONLY) => 0;
# for (lfs_size_t j = 0; j < size; j++) {
# char c = 'a' + (TEST_PRNG(&prng) % 26);
# char r;
# lfs_file_read(&lfs, &file, &r, 1) => 1;
# assert(r == c);
# }
#
# lfs_file_close(&lfs, &file) => 0;
# }
# lfs_unmount(&lfs) => 0;
#
# cycle += 1;
# }
#
#exhausted:
# // should still be readable
# lfs_mount(&lfs, cfg) => 0;
# for (uint32_t i = 0; i < FILES; i++) {
# // check for errors
# char path[1024];
# sprintf(path, "roadrunner/test%d", i);
# struct lfs_info info;
# lfs_stat(&lfs, path, &info) => 0;
# }
# lfs_unmount(&lfs) => 0;
#
# LFS_WARN("completed %d cycles", cycle);
#'''
#
## test running a filesystem to exhaustion
## which also requires expanding superblocks
#[cases.test_exhaustion_superblocks]
#defines.ERASE_CYCLES = 10
#defines.BLOCK_COUNT = 256 # small bd so test runs faster
#defines.BLOCK_CYCLES = 'ERASE_CYCLES / 2'
#defines.BADBLOCK_BEHAVIOR = [
# 'LFS_EMUBD_BADBLOCK_PROGERROR',
# 'LFS_EMUBD_BADBLOCK_ERASEERROR',
# 'LFS_EMUBD_BADBLOCK_READERROR',
# 'LFS_EMUBD_BADBLOCK_PROGNOOP',
# 'LFS_EMUBD_BADBLOCK_ERASENOOP',
#]
#defines.FILES = 10
#code = '''
# lfs_t lfs;
# lfs_format(&lfs, cfg) => 0;
#
# uint32_t cycle = 0;
# while (true) {
# lfs_mount(&lfs, cfg) => 0;
# for (uint32_t i = 0; i < FILES; i++) {
# // chose name, roughly random seed, and random 2^n size
# char path[1024];
# sprintf(path, "test%d", i);
# uint32_t prng = cycle * i;
# lfs_size_t size = 1 << ((TEST_PRNG(&prng) % 10)+2);
#
# lfs_file_t file;
# lfs_file_open(&lfs, &file, path,
# LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC) => 0;
#
# for (lfs_size_t j = 0; j < size; j++) {
# char c = 'a' + (TEST_PRNG(&prng) % 26);
# lfs_ssize_t res = lfs_file_write(&lfs, &file, &c, 1);
# assert(res == 1 || res == LFS_ERR_NOSPC);
# if (res == LFS_ERR_NOSPC) {
# int err = lfs_file_close(&lfs, &file);
# assert(err == 0 || err == LFS_ERR_NOSPC);
# lfs_unmount(&lfs) => 0;
# goto exhausted;
# }
# }
#
# int err = lfs_file_close(&lfs, &file);
# assert(err == 0 || err == LFS_ERR_NOSPC);
# if (err == LFS_ERR_NOSPC) {
# lfs_unmount(&lfs) => 0;
# goto exhausted;
# }
# }
#
# for (uint32_t i = 0; i < FILES; i++) {
# // check for errors
# char path[1024];
# sprintf(path, "test%d", i);
# uint32_t prng = cycle * i;
# lfs_size_t size = 1 << ((TEST_PRNG(&prng) % 10)+2);
#
# lfs_file_t file;
# lfs_file_open(&lfs, &file, path, LFS_O_RDONLY) => 0;
# for (lfs_size_t j = 0; j < size; j++) {
# char c = 'a' + (TEST_PRNG(&prng) % 26);
# char r;
# lfs_file_read(&lfs, &file, &r, 1) => 1;
# assert(r == c);
# }
#
# lfs_file_close(&lfs, &file) => 0;
# }
# lfs_unmount(&lfs) => 0;
#
# cycle += 1;
# }
#
#exhausted:
# // should still be readable
# lfs_mount(&lfs, cfg) => 0;
# for (uint32_t i = 0; i < FILES; i++) {
# // check for errors
# char path[1024];
# struct lfs_info info;
# sprintf(path, "test%d", i);
# lfs_stat(&lfs, path, &info) => 0;
# }
# lfs_unmount(&lfs) => 0;
#
# LFS_WARN("completed %d cycles", cycle);
#'''
#
## These are a sort of high-level litmus test for wear-leveling. One definition
## of wear-leveling is that increasing a block device's space translates directly
## into increasing the block devices lifetime. This is something we can actually
## check for.
#
## wear-level test running a filesystem to exhaustion
#[cases.test_exhuastion_wear_leveling]
#defines.ERASE_CYCLES = 20
#defines.BLOCK_COUNT = 256 # small bd so test runs faster
#defines.BLOCK_CYCLES = 'ERASE_CYCLES / 2'
#defines.FILES = 10
#code = '''
# uint32_t run_cycles[2];
# const uint32_t run_block_count[2] = {BLOCK_COUNT/2, BLOCK_COUNT};
#
# for (int run = 0; run < 2; run++) {
# for (lfs_block_t b = 0; b < BLOCK_COUNT; b++) {
# lfs_emubd_setwear(cfg, b,
# (b < run_block_count[run]) ? 0 : ERASE_CYCLES) => 0;
# }
#
# lfs_t lfs;
# lfs_format(&lfs, cfg) => 0;
# lfs_mount(&lfs, cfg) => 0;
# lfs_mkdir(&lfs, "roadrunner") => 0;
# lfs_unmount(&lfs) => 0;
#
# uint32_t cycle = 0;
# while (true) {
# lfs_mount(&lfs, cfg) => 0;
# for (uint32_t i = 0; i < FILES; i++) {
# // chose name, roughly random seed, and random 2^n size
# char path[1024];
# sprintf(path, "roadrunner/test%d", i);
# uint32_t prng = cycle * i;
# lfs_size_t size = 1 << ((TEST_PRNG(&prng) % 10)+2);
#
# lfs_file_t file;
# lfs_file_open(&lfs, &file, path,
# LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC) => 0;
#
# for (lfs_size_t j = 0; j < size; j++) {
# char c = 'a' + (TEST_PRNG(&prng) % 26);
# lfs_ssize_t res = lfs_file_write(&lfs, &file, &c, 1);
# assert(res == 1 || res == LFS_ERR_NOSPC);
# if (res == LFS_ERR_NOSPC) {
# int err = lfs_file_close(&lfs, &file);
# assert(err == 0 || err == LFS_ERR_NOSPC);
# lfs_unmount(&lfs) => 0;
# goto exhausted;
# }
# }
#
# int err = lfs_file_close(&lfs, &file);
# assert(err == 0 || err == LFS_ERR_NOSPC);
# if (err == LFS_ERR_NOSPC) {
# lfs_unmount(&lfs) => 0;
# goto exhausted;
# }
# }
#
# for (uint32_t i = 0; i < FILES; i++) {
# // check for errors
# char path[1024];
# sprintf(path, "roadrunner/test%d", i);
# uint32_t prng = cycle * i;
# lfs_size_t size = 1 << ((TEST_PRNG(&prng) % 10)+2);
#
# lfs_file_t file;
# lfs_file_open(&lfs, &file, path, LFS_O_RDONLY) => 0;
# for (lfs_size_t j = 0; j < size; j++) {
# char c = 'a' + (TEST_PRNG(&prng) % 26);
# char r;
# lfs_file_read(&lfs, &file, &r, 1) => 1;
# assert(r == c);
# }
#
# lfs_file_close(&lfs, &file) => 0;
# }
# lfs_unmount(&lfs) => 0;
#
# cycle += 1;
# }
#
#exhausted:
# // should still be readable
# lfs_mount(&lfs, cfg) => 0;
# for (uint32_t i = 0; i < FILES; i++) {
# // check for errors
# char path[1024];
# struct lfs_info info;
# sprintf(path, "roadrunner/test%d", i);
# lfs_stat(&lfs, path, &info) => 0;
# }
# lfs_unmount(&lfs) => 0;
#
# run_cycles[run] = cycle;
# LFS_WARN("completed %d blocks %d cycles",
# run_block_count[run], run_cycles[run]);
# }
#
# // check we increased the lifetime by 2x with ~10% error
# LFS_ASSERT(run_cycles[1]*110/100 > 2*run_cycles[0]);
#'''
#
## wear-level test + expanding superblock
#[cases.test_exhaustion_wear_leveling_superblocks]
#defines.ERASE_CYCLES = 20
#defines.BLOCK_COUNT = 256 # small bd so test runs faster
#defines.BLOCK_CYCLES = 'ERASE_CYCLES / 2'
#defines.FILES = 10
#code = '''
# uint32_t run_cycles[2];
# const uint32_t run_block_count[2] = {BLOCK_COUNT/2, BLOCK_COUNT};
#
# for (int run = 0; run < 2; run++) {
# for (lfs_block_t b = 0; b < BLOCK_COUNT; b++) {
# lfs_emubd_setwear(cfg, b,
# (b < run_block_count[run]) ? 0 : ERASE_CYCLES) => 0;
# }
#
# lfs_t lfs;
# lfs_format(&lfs, cfg) => 0;
#
# uint32_t cycle = 0;
# while (true) {
# lfs_mount(&lfs, cfg) => 0;
# for (uint32_t i = 0; i < FILES; i++) {
# // chose name, roughly random seed, and random 2^n size
# char path[1024];
# sprintf(path, "test%d", i);
# uint32_t prng = cycle * i;
# lfs_size_t size = 1 << ((TEST_PRNG(&prng) % 10)+2);
#
# lfs_file_t file;
# lfs_file_open(&lfs, &file, path,
# LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC) => 0;
#
# for (lfs_size_t j = 0; j < size; j++) {
# char c = 'a' + (TEST_PRNG(&prng) % 26);
# lfs_ssize_t res = lfs_file_write(&lfs, &file, &c, 1);
# assert(res == 1 || res == LFS_ERR_NOSPC);
# if (res == LFS_ERR_NOSPC) {
# int err = lfs_file_close(&lfs, &file);
# assert(err == 0 || err == LFS_ERR_NOSPC);
# lfs_unmount(&lfs) => 0;
# goto exhausted;
# }
# }
#
# int err = lfs_file_close(&lfs, &file);
# assert(err == 0 || err == LFS_ERR_NOSPC);
# if (err == LFS_ERR_NOSPC) {
# lfs_unmount(&lfs) => 0;
# goto exhausted;
# }
# }
#
# for (uint32_t i = 0; i < FILES; i++) {
# // check for errors
# char path[1024];
# sprintf(path, "test%d", i);
# uint32_t prng = cycle * i;
# lfs_size_t size = 1 << ((TEST_PRNG(&prng) % 10)+2);
#
# lfs_file_t file;
# lfs_file_open(&lfs, &file, path, LFS_O_RDONLY) => 0;
# for (lfs_size_t j = 0; j < size; j++) {
# char c = 'a' + (TEST_PRNG(&prng) % 26);
# char r;
# lfs_file_read(&lfs, &file, &r, 1) => 1;
# assert(r == c);
# }
#
# lfs_file_close(&lfs, &file) => 0;
# }
# lfs_unmount(&lfs) => 0;
#
# cycle += 1;
# }
#
#exhausted:
# // should still be readable
# lfs_mount(&lfs, cfg) => 0;
# for (uint32_t i = 0; i < FILES; i++) {
# // check for errors
# char path[1024];
# struct lfs_info info;
# sprintf(path, "test%d", i);
# lfs_stat(&lfs, path, &info) => 0;
# }
# lfs_unmount(&lfs) => 0;
#
# run_cycles[run] = cycle;
# LFS_WARN("completed %d blocks %d cycles",
# run_block_count[run], run_cycles[run]);
# }
#
# // check we increased the lifetime by 2x with ~10% error
# LFS_ASSERT(run_cycles[1]*110/100 > 2*run_cycles[0]);
#'''
#
## test that we wear blocks roughly evenly
#[cases.test_exhaustion_wear_distribution]
#defines.ERASE_CYCLES = 0xffffffff
#defines.BLOCK_COUNT = 256 # small bd so test runs faster
#defines.BLOCK_CYCLES = [5, 4, 3, 2, 1]
#defines.CYCLES = 100
#defines.FILES = 10
#if = 'BLOCK_CYCLES < CYCLES/10'
#code = '''
# lfs_t lfs;
# lfs_format(&lfs, cfg) => 0;
# lfs_mount(&lfs, cfg) => 0;
# lfs_mkdir(&lfs, "roadrunner") => 0;
# lfs_unmount(&lfs) => 0;
#
# uint32_t cycle = 0;
# while (cycle < CYCLES) {
# lfs_mount(&lfs, cfg) => 0;
# for (uint32_t i = 0; i < FILES; i++) {
# // chose name, roughly random seed, and random 2^n size
# char path[1024];
# sprintf(path, "roadrunner/test%d", i);
# uint32_t prng = cycle * i;
# lfs_size_t size = 1 << 4; //((TEST_PRNG(&prng) % 10)+2);
#
# lfs_file_t file;
# lfs_file_open(&lfs, &file, path,
# LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC) => 0;
#
# for (lfs_size_t j = 0; j < size; j++) {
# char c = 'a' + (TEST_PRNG(&prng) % 26);
# lfs_ssize_t res = lfs_file_write(&lfs, &file, &c, 1);
# assert(res == 1 || res == LFS_ERR_NOSPC);
# if (res == LFS_ERR_NOSPC) {
# int err = lfs_file_close(&lfs, &file);
# assert(err == 0 || err == LFS_ERR_NOSPC);
# lfs_unmount(&lfs) => 0;
# goto exhausted;
# }
# }
#
# int err = lfs_file_close(&lfs, &file);
# assert(err == 0 || err == LFS_ERR_NOSPC);
# if (err == LFS_ERR_NOSPC) {
# lfs_unmount(&lfs) => 0;
# goto exhausted;
# }
# }
#
# for (uint32_t i = 0; i < FILES; i++) {
# // check for errors
# char path[1024];
# sprintf(path, "roadrunner/test%d", i);
# uint32_t prng = cycle * i;
# lfs_size_t size = 1 << 4; //((TEST_PRNG(&prng) % 10)+2);
#
# lfs_file_t file;
# lfs_file_open(&lfs, &file, path, LFS_O_RDONLY) => 0;
# for (lfs_size_t j = 0; j < size; j++) {
# char c = 'a' + (TEST_PRNG(&prng) % 26);
# char r;
# lfs_file_read(&lfs, &file, &r, 1) => 1;
# assert(r == c);
# }
#
# lfs_file_close(&lfs, &file) => 0;
# }
# lfs_unmount(&lfs) => 0;
#
# cycle += 1;
# }
#
#exhausted:
# // should still be readable
# lfs_mount(&lfs, cfg) => 0;
# for (uint32_t i = 0; i < FILES; i++) {
# // check for errors
# char path[1024];
# struct lfs_info info;
# sprintf(path, "roadrunner/test%d", i);
# lfs_stat(&lfs, path, &info) => 0;
# }
# lfs_unmount(&lfs) => 0;
#
# LFS_WARN("completed %d cycles", cycle);
#
# // check the wear on our block device
# lfs_emubd_wear_t minwear = -1;
# lfs_emubd_wear_t totalwear = 0;
# lfs_emubd_wear_t maxwear = 0;
# // skip 0 and 1 as superblock movement is intentionally avoided
# for (lfs_block_t b = 2; b < BLOCK_COUNT; b++) {
# lfs_emubd_wear_t wear = lfs_emubd_wear(cfg, b);
# printf("%08x: wear %d\n", b, wear);
# assert(wear >= 0);
# if (wear < minwear) {
# minwear = wear;
# }
# if (wear > maxwear) {
# maxwear = wear;
# }
# totalwear += wear;
# }
# lfs_emubd_wear_t avgwear = totalwear / BLOCK_COUNT;
# LFS_WARN("max wear: %d cycles", maxwear);
# LFS_WARN("avg wear: %d cycles", totalwear / (int)BLOCK_COUNT);
# LFS_WARN("min wear: %d cycles", minwear);
#
# // find standard deviation^2
# lfs_emubd_wear_t dev2 = 0;
# for (lfs_block_t b = 2; b < BLOCK_COUNT; b++) {
# lfs_emubd_wear_t wear = lfs_emubd_wear(cfg, b);
# assert(wear >= 0);
# lfs_emubd_swear_t diff = wear - avgwear;
# dev2 += diff*diff;
# }
# dev2 /= totalwear;
# LFS_WARN("std dev^2: %d", dev2);
# assert(dev2 < 8);
#'''
#