Added back heuristic-based power-loss testing

The main change here from the previous test framework design is:

1. Powerloss testing remains in-process, speeding up testing.

2. The state of a test, included all powerlosses, is encoded in the
   test id + leb16 encoded powerloss string. This means exhaustive
   testing can be run in CI, but then easily reproduced locally with
   full debugger support.

   For example:

   ./scripts/test.py test_dirs#reentrant_many_dir#10#1248g1g2 --gdb

   Will run the test test_dir, case reentrant_many_dir, permutation #10,
   with powerlosses at 1, 2, 4, 8, 16, and 32 cycles. Dropping into gdb
   if an assert fails.

The changes to the block-device are a work-in-progress for a
lazily-allocated/copy-on-write block device that I'm hoping will keep
exhaustive testing relatively low-cost.
This commit is contained in:
Christopher Haster
2022-08-19 18:57:55 -05:00
parent 01b11da31b
commit 61455b6191
8 changed files with 1391 additions and 571 deletions

View File

@@ -110,10 +110,10 @@ tags:
.PHONY: test-runner .PHONY: test-runner
test-runner: override CFLAGS+=--coverage test-runner: override CFLAGS+=--coverage
test-runner: $(BUILDDIR)runners/test_runner test-runner: $(BUILDDIR)runners/test_runner
rm -f $(TEST_GCDA)
.PHONY: test .PHONY: test
test: test-runner test: test-runner
rm -f $(TEST_GCDA)
./scripts/test.py --runner=$(BUILDDIR)runners/test_runner $(TESTFLAGS) ./scripts/test.py --runner=$(BUILDDIR)runners/test_runner $(TESTFLAGS)
.PHONY: test-list .PHONY: test-list

View File

@@ -11,6 +11,79 @@
#include <stdlib.h> #include <stdlib.h>
// access to lazily-allocated/copy-on-write blocks
//
// Note we can only modify a block if we have exclusive access to it (rc == 1)
//
// TODO
__attribute__((unused))
static void lfs_testbd_incblock(lfs_testbd_t *bd, lfs_block_t block) {
if (bd->blocks[block]) {
bd->blocks[block]->rc += 1;
}
}
static void lfs_testbd_decblock(lfs_testbd_t *bd, lfs_block_t block) {
if (bd->blocks[block]) {
bd->blocks[block]->rc -= 1;
if (bd->blocks[block]->rc == 0) {
free(bd->blocks[block]);
bd->blocks[block] = NULL;
}
}
}
static const lfs_testbd_block_t *lfs_testbd_getblock(lfs_testbd_t *bd,
lfs_block_t block) {
return bd->blocks[block];
}
static lfs_testbd_block_t *lfs_testbd_mutblock(lfs_testbd_t *bd,
lfs_block_t block, lfs_size_t block_size) {
if (bd->blocks[block] && bd->blocks[block]->rc == 1) {
// rc == 1? can modify
return bd->blocks[block];
} else if (bd->blocks[block]) {
// rc > 1? need to create a copy
lfs_testbd_block_t *b = malloc(
sizeof(lfs_testbd_block_t) + block_size);
if (!b) {
return NULL;
}
memcpy(b, bd->blocks[block], sizeof(lfs_testbd_block_t) + block_size);
b->rc = 1;
lfs_testbd_decblock(bd, block);
bd->blocks[block] = b;
return b;
} else {
// no block? need to allocate
lfs_testbd_block_t *b = malloc(
sizeof(lfs_testbd_block_t) + block_size);
if (!b) {
return NULL;
}
b->rc = 1;
b->wear = 0;
// zero for consistency
memset(b->data,
(bd->cfg->erase_value != -1) ? bd->cfg->erase_value : 0,
block_size);
bd->blocks[block] = b;
return b;
}
}
// testbd create/destroy
int lfs_testbd_createcfg(const struct lfs_config *cfg, const char *path, int lfs_testbd_createcfg(const struct lfs_config *cfg, const char *path,
const struct lfs_testbd_config *bdcfg) { const struct lfs_testbd_config *bdcfg) {
LFS_TESTBD_TRACE("lfs_testbd_createcfg(%p {.context=%p, " LFS_TESTBD_TRACE("lfs_testbd_createcfg(%p {.context=%p, "
@@ -20,62 +93,35 @@ int lfs_testbd_createcfg(const struct lfs_config *cfg, const char *path,
"\"%s\", " "\"%s\", "
"%p {.erase_value=%"PRId32", .erase_cycles=%"PRIu32", " "%p {.erase_value=%"PRId32", .erase_cycles=%"PRIu32", "
".badblock_behavior=%"PRIu8", .power_cycles=%"PRIu32", " ".badblock_behavior=%"PRIu8", .power_cycles=%"PRIu32", "
".buffer=%p, .wear_buffer=%p})", ".powerloss_behavior=%"PRIu8", .powerloss_cb=%p, "
".powerloss_data=%p, .track_branches=%d})",
(void*)cfg, cfg->context, (void*)cfg, cfg->context,
(void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog, (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog,
(void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync,
cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count, cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count,
path, (void*)bdcfg, bdcfg->erase_value, bdcfg->erase_cycles, path, (void*)bdcfg, bdcfg->erase_value, bdcfg->erase_cycles,
bdcfg->badblock_behavior, bdcfg->power_cycles, bdcfg->badblock_behavior, bdcfg->power_cycles,
bdcfg->buffer, bdcfg->wear_buffer); bdcfg->powerloss_behavior, (void*)(uintptr_t)bdcfg->powerloss_cb,
bdcfg->powerloss_data, bdcfg->track_branches);
lfs_testbd_t *bd = cfg->context; lfs_testbd_t *bd = cfg->context;
bd->cfg = bdcfg; bd->cfg = bdcfg;
// allocate our block array, all blocks start as uninitialized
bd->blocks = malloc(cfg->block_count * sizeof(lfs_testbd_block_t*));
if (!bd->blocks) {
LFS_TESTBD_TRACE("lfs_testbd_createcfg -> %d", LFS_ERR_NOMEM);
return LFS_ERR_NOMEM;
}
memset(bd->blocks, 0, cfg->block_count * sizeof(lfs_testbd_block_t*));
// setup testing things // setup testing things
bd->persist = path;
bd->power_cycles = bd->cfg->power_cycles; bd->power_cycles = bd->cfg->power_cycles;
bd->branches = NULL;
bd->branch_capacity = 0;
bd->branch_count = 0;
// create scratch block if we need it (for emulating erase values) LFS_TESTBD_TRACE("lfs_testbd_createcfg -> %d", 0);
if (bd->cfg->erase_value != -1) { return 0;
if (bd->cfg->scratch_buffer) {
bd->scratch = bd->cfg->scratch_buffer;
} else {
bd->scratch = lfs_malloc(cfg->block_size);
if (!bd->scratch) {
LFS_TESTBD_TRACE("lfs_testbd_createcfg -> %d", LFS_ERR_NOMEM);
return LFS_ERR_NOMEM;
}
}
}
// create map of wear
if (bd->cfg->erase_cycles) {
if (bd->cfg->wear_buffer) {
bd->wear = bd->cfg->wear_buffer;
} else {
bd->wear = lfs_malloc(sizeof(lfs_testbd_wear_t)*cfg->block_count);
if (!bd->wear) {
LFS_TESTBD_TRACE("lfs_testbd_createcfg -> %d", LFS_ERR_NOMEM);
return LFS_ERR_NOMEM;
}
}
memset(bd->wear, 0, sizeof(lfs_testbd_wear_t) * cfg->block_count);
}
// create underlying block device
if (bd->persist) {
int err = lfs_filebd_create(cfg, path);
LFS_TESTBD_TRACE("lfs_testbd_createcfg -> %d", err);
return err;
} else {
bd->u.ram.cfg = (struct lfs_rambd_config){
.buffer = bd->cfg->buffer,
};
int err = lfs_rambd_createcfg(cfg, &bd->u.ram.cfg);
LFS_TESTBD_TRACE("lfs_testbd_createcfg -> %d", err);
return err;
}
} }
int lfs_testbd_create(const struct lfs_config *cfg, const char *path) { int lfs_testbd_create(const struct lfs_config *cfg, const char *path) {
@@ -99,65 +145,65 @@ int lfs_testbd_destroy(const struct lfs_config *cfg) {
LFS_TESTBD_TRACE("lfs_testbd_destroy(%p)", (void*)cfg); LFS_TESTBD_TRACE("lfs_testbd_destroy(%p)", (void*)cfg);
lfs_testbd_t *bd = cfg->context; lfs_testbd_t *bd = cfg->context;
if (bd->cfg->erase_value != -1 && !bd->cfg->scratch_buffer) { // decrement reference counts
lfs_free(bd->scratch); for (lfs_block_t i = 0; i < cfg->block_count; i++) {
} lfs_testbd_decblock(bd, i);
if (bd->cfg->erase_cycles && !bd->cfg->wear_buffer) {
lfs_free(bd->wear);
} }
if (bd->persist) { // free memory
int err = lfs_filebd_destroy(cfg); free(bd->blocks);
LFS_TESTBD_TRACE("lfs_testbd_destroy -> %d", err); free(bd->branches);
return err;
} else { LFS_TESTBD_TRACE("lfs_testbd_destroy -> %d", 0);
int err = lfs_rambd_destroy(cfg); return 0;
LFS_TESTBD_TRACE("lfs_testbd_destroy -> %d", err);
return err;
}
} }
/// Internal mapping to block devices ///
static int lfs_testbd_rawread(const struct lfs_config *cfg, lfs_block_t block,
lfs_off_t off, void *buffer, lfs_size_t size) {
lfs_testbd_t *bd = cfg->context;
if (bd->persist) {
return lfs_filebd_read(cfg, block, off, buffer, size);
} else {
return lfs_rambd_read(cfg, block, off, buffer, size);
}
}
static int lfs_testbd_rawprog(const struct lfs_config *cfg, lfs_block_t block,
lfs_off_t off, const void *buffer, lfs_size_t size) {
lfs_testbd_t *bd = cfg->context;
if (bd->persist) {
return lfs_filebd_prog(cfg, block, off, buffer, size);
} else {
return lfs_rambd_prog(cfg, block, off, buffer, size);
}
}
static int lfs_testbd_rawerase(const struct lfs_config *cfg, ///// Internal mapping to block devices ///
lfs_block_t block) { //static int lfs_testbd_rawread(const struct lfs_config *cfg, lfs_block_t block,
lfs_testbd_t *bd = cfg->context; // lfs_off_t off, void *buffer, lfs_size_t size) {
if (bd->persist) { // lfs_testbd_t *bd = cfg->context;
return lfs_filebd_erase(cfg, block); // if (bd->persist) {
} else { // return lfs_filebd_read(cfg, block, off, buffer, size);
return lfs_rambd_erase(cfg, block); // } else {
} // return lfs_rambd_read(cfg, block, off, buffer, size);
} // }
//}
//
//static int lfs_testbd_rawprog(const struct lfs_config *cfg, lfs_block_t block,
// lfs_off_t off, const void *buffer, lfs_size_t size) {
// lfs_testbd_t *bd = cfg->context;
// if (bd->persist) {
// return lfs_filebd_prog(cfg, block, off, buffer, size);
// } else {
// return lfs_rambd_prog(cfg, block, off, buffer, size);
// }
//}
//
//static int lfs_testbd_rawerase(const struct lfs_config *cfg,
// lfs_block_t block) {
// lfs_testbd_t *bd = cfg->context;
// if (bd->persist) {
// return lfs_filebd_erase(cfg, block);
// } else {
// return lfs_rambd_erase(cfg, block);
// }
//}
//
//static int lfs_testbd_rawsync(const struct lfs_config *cfg) {
// lfs_testbd_t *bd = cfg->context;
// if (bd->persist) {
// return lfs_filebd_sync(cfg);
// } else {
// return lfs_rambd_sync(cfg);
// }
//}
static int lfs_testbd_rawsync(const struct lfs_config *cfg) {
lfs_testbd_t *bd = cfg->context;
if (bd->persist) {
return lfs_filebd_sync(cfg);
} else {
return lfs_rambd_sync(cfg);
}
}
/// block device API ///
// block device API
int lfs_testbd_read(const struct lfs_config *cfg, lfs_block_t block, int lfs_testbd_read(const struct lfs_config *cfg, lfs_block_t block,
lfs_off_t off, void *buffer, lfs_size_t size) { lfs_off_t off, void *buffer, lfs_size_t size) {
LFS_TESTBD_TRACE("lfs_testbd_read(%p, " LFS_TESTBD_TRACE("lfs_testbd_read(%p, "
@@ -171,17 +217,27 @@ int lfs_testbd_read(const struct lfs_config *cfg, lfs_block_t block,
LFS_ASSERT(size % cfg->read_size == 0); LFS_ASSERT(size % cfg->read_size == 0);
LFS_ASSERT(off+size <= cfg->block_size); LFS_ASSERT(off+size <= cfg->block_size);
// block bad? // get the block
if (bd->cfg->erase_cycles && bd->wear[block] >= bd->cfg->erase_cycles && const lfs_testbd_block_t *b = lfs_testbd_getblock(bd, block);
bd->cfg->badblock_behavior == LFS_TESTBD_BADBLOCK_READERROR) { if (b) {
LFS_TESTBD_TRACE("lfs_testbd_read -> %d", LFS_ERR_CORRUPT); // block bad?
return LFS_ERR_CORRUPT; if (bd->cfg->erase_cycles && b->wear >= bd->cfg->erase_cycles &&
bd->cfg->badblock_behavior == LFS_TESTBD_BADBLOCK_READERROR) {
LFS_TESTBD_TRACE("lfs_testbd_read -> %d", LFS_ERR_CORRUPT);
return LFS_ERR_CORRUPT;
}
// read data
memcpy(buffer, &b->data[off], size);
} else {
// zero for consistency
memset(buffer,
(bd->cfg->erase_value != -1) ? bd->cfg->erase_value : 0,
size);
} }
// read LFS_TESTBD_TRACE("lfs_testbd_read -> %d", 0);
int err = lfs_testbd_rawread(cfg, block, off, buffer, size); return 0;
LFS_TESTBD_TRACE("lfs_testbd_read -> %d", err);
return err;
} }
int lfs_testbd_prog(const struct lfs_config *cfg, lfs_block_t block, int lfs_testbd_prog(const struct lfs_config *cfg, lfs_block_t block,
@@ -197,8 +253,15 @@ int lfs_testbd_prog(const struct lfs_config *cfg, lfs_block_t block,
LFS_ASSERT(size % cfg->prog_size == 0); LFS_ASSERT(size % cfg->prog_size == 0);
LFS_ASSERT(off+size <= cfg->block_size); LFS_ASSERT(off+size <= cfg->block_size);
// get the block
lfs_testbd_block_t *b = lfs_testbd_mutblock(bd, block, cfg->block_size);
if (!b) {
LFS_TESTBD_TRACE("lfs_testbd_prog -> %d", LFS_ERR_NOMEM);
return LFS_ERR_NOMEM;
}
// block bad? // block bad?
if (bd->cfg->erase_cycles && bd->wear[block] >= bd->cfg->erase_cycles) { if (bd->cfg->erase_cycles && b->wear >= bd->cfg->erase_cycles) {
if (bd->cfg->badblock_behavior == if (bd->cfg->badblock_behavior ==
LFS_TESTBD_BADBLOCK_PROGERROR) { LFS_TESTBD_BADBLOCK_PROGERROR) {
LFS_TESTBD_TRACE("lfs_testbd_prog -> %d", LFS_ERR_CORRUPT); LFS_TESTBD_TRACE("lfs_testbd_prog -> %d", LFS_ERR_CORRUPT);
@@ -212,54 +275,34 @@ int lfs_testbd_prog(const struct lfs_config *cfg, lfs_block_t block,
} }
} }
// emulate an erase value? // were we erased properly?
if (bd->cfg->erase_value != -1) { if (bd->cfg->erase_value != -1) {
int err = lfs_testbd_rawread(cfg, block, 0,
bd->scratch, cfg->block_size);
if (err) {
LFS_TESTBD_TRACE("lfs_testbd_prog -> %d", err);
return err;
}
// assert that program block was erased
for (lfs_off_t i = 0; i < size; i++) { for (lfs_off_t i = 0; i < size; i++) {
LFS_ASSERT(bd->scratch[off+i] == bd->cfg->erase_value); LFS_ASSERT(b->data[off+i] == bd->cfg->erase_value);
}
memcpy(&bd->scratch[off], buffer, size);
err = lfs_testbd_rawerase(cfg, block);
if (err) {
LFS_TESTBD_TRACE("lfs_testbd_prog -> %d", err);
return err;
}
err = lfs_testbd_rawprog(cfg, block, 0,
bd->scratch, cfg->block_size);
if (err) {
LFS_TESTBD_TRACE("lfs_testbd_prog -> %d", err);
return err;
}
} else {
// prog
int err = lfs_testbd_rawprog(cfg, block, off, buffer, size);
if (err) {
LFS_TESTBD_TRACE("lfs_testbd_prog -> %d", err);
return err;
} }
} }
// prog data
memcpy(&b->data[off], buffer, size);
// lose power? // lose power?
if (bd->power_cycles > 0) { if (bd->power_cycles > 0) {
bd->power_cycles -= 1; bd->power_cycles -= 1;
if (bd->power_cycles == 0) { if (bd->power_cycles == 0) {
// sync to make sure we persist the last changes
LFS_ASSERT(lfs_testbd_rawsync(cfg) == 0);
// simulate power loss // simulate power loss
exit(33); bd->cfg->powerloss_cb(bd->cfg->powerloss_data);
} }
} }
// // track power-loss branch?
// if (bd->cfg->track_branches) {
// int err = lfs_testbd_trackbranch(bd);
// if (err) {
// LFS_TESTBD_TRACE("lfs_testbd_prog -> %d", err);
// return err;
// }
// }
LFS_TESTBD_TRACE("lfs_testbd_prog -> %d", 0); LFS_TESTBD_TRACE("lfs_testbd_prog -> %d", 0);
return 0; return 0;
} }
@@ -271,9 +314,16 @@ int lfs_testbd_erase(const struct lfs_config *cfg, lfs_block_t block) {
// check if erase is valid // check if erase is valid
LFS_ASSERT(block < cfg->block_count); LFS_ASSERT(block < cfg->block_count);
// get the block
lfs_testbd_block_t *b = lfs_testbd_mutblock(bd, block, cfg->block_size);
if (!b) {
LFS_TESTBD_TRACE("lfs_testbd_prog -> %d", LFS_ERR_NOMEM);
return LFS_ERR_NOMEM;
}
// block bad? // block bad?
if (bd->cfg->erase_cycles) { if (bd->cfg->erase_cycles) {
if (bd->wear[block] >= bd->cfg->erase_cycles) { if (b->wear >= bd->cfg->erase_cycles) {
if (bd->cfg->badblock_behavior == if (bd->cfg->badblock_behavior ==
LFS_TESTBD_BADBLOCK_ERASEERROR) { LFS_TESTBD_BADBLOCK_ERASEERROR) {
LFS_TESTBD_TRACE("lfs_testbd_erase -> %d", LFS_ERR_CORRUPT); LFS_TESTBD_TRACE("lfs_testbd_erase -> %d", LFS_ERR_CORRUPT);
@@ -285,70 +335,69 @@ int lfs_testbd_erase(const struct lfs_config *cfg, lfs_block_t block) {
} }
} else { } else {
// mark wear // mark wear
bd->wear[block] += 1; b->wear += 1;
} }
} }
// emulate an erase value? // emulate an erase value?
if (bd->cfg->erase_value != -1) { if (bd->cfg->erase_value != -1) {
memset(bd->scratch, bd->cfg->erase_value, cfg->block_size); memset(b->data, bd->cfg->erase_value, cfg->block_size);
int err = lfs_testbd_rawerase(cfg, block);
if (err) {
LFS_TESTBD_TRACE("lfs_testbd_erase -> %d", err);
return err;
}
err = lfs_testbd_rawprog(cfg, block, 0,
bd->scratch, cfg->block_size);
if (err) {
LFS_TESTBD_TRACE("lfs_testbd_erase -> %d", err);
return err;
}
} else {
// erase
int err = lfs_testbd_rawerase(cfg, block);
if (err) {
LFS_TESTBD_TRACE("lfs_testbd_erase -> %d", err);
return err;
}
} }
// lose power? // lose power?
if (bd->power_cycles > 0) { if (bd->power_cycles > 0) {
bd->power_cycles -= 1; bd->power_cycles -= 1;
if (bd->power_cycles == 0) { if (bd->power_cycles == 0) {
// sync to make sure we persist the last changes
LFS_ASSERT(lfs_testbd_rawsync(cfg) == 0);
// simulate power loss // simulate power loss
exit(33); bd->cfg->powerloss_cb(bd->cfg->powerloss_data);
} }
} }
// // track power-loss branch?
// if (bd->cfg->track_branches) {
// int err = lfs_testbd_trackbranch(bd);
// if (err) {
// LFS_TESTBD_TRACE("lfs_testbd_prog -> %d", err);
// return err;
// }
// }
LFS_TESTBD_TRACE("lfs_testbd_prog -> %d", 0); LFS_TESTBD_TRACE("lfs_testbd_prog -> %d", 0);
return 0; return 0;
} }
int lfs_testbd_sync(const struct lfs_config *cfg) { int lfs_testbd_sync(const struct lfs_config *cfg) {
LFS_TESTBD_TRACE("lfs_testbd_sync(%p)", (void*)cfg); LFS_TESTBD_TRACE("lfs_testbd_sync(%p)", (void*)cfg);
int err = lfs_testbd_rawsync(cfg);
LFS_TESTBD_TRACE("lfs_testbd_sync -> %d", err); // do nothing
return err; (void)cfg;
LFS_TESTBD_TRACE("lfs_testbd_sync -> %d", 0);
return 0;
} }
/// simulated wear operations /// // simulated wear operations
lfs_testbd_swear_t lfs_testbd_getwear(const struct lfs_config *cfg, lfs_testbd_swear_t lfs_testbd_getwear(const struct lfs_config *cfg,
lfs_block_t block) { lfs_block_t block) {
LFS_TESTBD_TRACE("lfs_testbd_getwear(%p, %"PRIu32")", (void*)cfg, block); LFS_TESTBD_TRACE("lfs_testbd_getwear(%p, %"PRIu32")", (void*)cfg, block);
lfs_testbd_t *bd = cfg->context; lfs_testbd_t *bd = cfg->context;
// check if block is valid // check if block is valid
LFS_ASSERT(bd->cfg->erase_cycles);
LFS_ASSERT(block < cfg->block_count); LFS_ASSERT(block < cfg->block_count);
LFS_TESTBD_TRACE("lfs_testbd_getwear -> %"PRIu32, bd->wear[block]); // get the wear
return bd->wear[block]; lfs_testbd_wear_t wear;
const lfs_testbd_block_t *b = lfs_testbd_getblock(bd, block);
if (b) {
wear = b->wear;
} else {
wear = 0;
}
LFS_TESTBD_TRACE("lfs_testbd_getwear -> %"PRIu32, wear);
return wear;
} }
int lfs_testbd_setwear(const struct lfs_config *cfg, int lfs_testbd_setwear(const struct lfs_config *cfg,
@@ -357,11 +406,58 @@ int lfs_testbd_setwear(const struct lfs_config *cfg,
lfs_testbd_t *bd = cfg->context; lfs_testbd_t *bd = cfg->context;
// check if block is valid // check if block is valid
LFS_ASSERT(bd->cfg->erase_cycles);
LFS_ASSERT(block < cfg->block_count); LFS_ASSERT(block < cfg->block_count);
bd->wear[block] = wear; // set the wear
lfs_testbd_block_t *b = lfs_testbd_mutblock(bd, block, cfg->block_size);
if (!b) {
LFS_TESTBD_TRACE("lfs_testbd_setwear -> %"PRIu32, LFS_ERR_NOMEM);
return LFS_ERR_NOMEM;
}
b->wear = wear;
LFS_TESTBD_TRACE("lfs_testbd_setwear -> %d", 0); LFS_TESTBD_TRACE("lfs_testbd_setwear -> %"PRIu32, 0);
return 0; return 0;
} }
lfs_testbd_spowercycles_t lfs_testbd_getpowercycles(
const struct lfs_config *cfg) {
LFS_TESTBD_TRACE("lfs_testbd_getpowercycles(%p)", (void*)cfg);
lfs_testbd_t *bd = cfg->context;
LFS_TESTBD_TRACE("lfs_testbd_getpowercycles -> %"PRIi32, bd->power_cycles);
return bd->power_cycles;
}
int lfs_testbd_setpowercycles(const struct lfs_config *cfg,
lfs_testbd_powercycles_t power_cycles) {
LFS_TESTBD_TRACE("lfs_testbd_setpowercycles(%p, %"PRIi32")",
(void*)cfg, power_cycles);
lfs_testbd_t *bd = cfg->context;
bd->power_cycles = power_cycles;
LFS_TESTBD_TRACE("lfs_testbd_getpowercycles -> %d", 0);
return 0;
}
//int lfs_testbd_getbranch(const struct lfs_config *cfg,
// lfs_testbd_powercycles_t branch, lfs_testbd_t *bd) {
// LFS_TESTBD_TRACE("lfs_testbd_getbranch(%p, %zu, %p)",
// (void*)cfg, branch, bd);
// lfs_testbd_t *bd = cfg->context;
//
// // TODO
//
// LFS_TESTBD_TRACE("lfs_testbd_getbranch -> %d", 0);
// return 0;
//}
lfs_testbd_spowercycles_t lfs_testbd_getbranchcount(
const struct lfs_config *cfg) {
LFS_TESTBD_TRACE("lfs_testbd_getbranchcount(%p)", (void*)cfg);
lfs_testbd_t *bd = cfg->context;
LFS_TESTBD_TRACE("lfs_testbd_getbranchcount -> %"PRIu32, bd->branch_count);
return bd->branch_count;
}

View File

@@ -29,23 +29,33 @@ extern "C"
#endif #endif
#endif #endif
// Mode determining how "bad blocks" behave during testing. This simulates // Mode determining how "bad-blocks" behave during testing. This simulates
// some real-world circumstances such as progs not sticking (prog-noop), // some real-world circumstances such as progs not sticking (prog-noop),
// a readonly disk (erase-noop), and ECC failures (read-error). // a readonly disk (erase-noop), and ECC failures (read-error).
// //
// Not that read-noop is not allowed. Read _must_ return a consistent (but // Not that read-noop is not allowed. Read _must_ return a consistent (but
// may be arbitrary) value on every read. // may be arbitrary) value on every read.
enum lfs_testbd_badblock_behavior { typedef enum lfs_testbd_badblock_behavior {
LFS_TESTBD_BADBLOCK_PROGERROR, LFS_TESTBD_BADBLOCK_PROGERROR,
LFS_TESTBD_BADBLOCK_ERASEERROR, LFS_TESTBD_BADBLOCK_ERASEERROR,
LFS_TESTBD_BADBLOCK_READERROR, LFS_TESTBD_BADBLOCK_READERROR,
LFS_TESTBD_BADBLOCK_PROGNOOP, LFS_TESTBD_BADBLOCK_PROGNOOP,
LFS_TESTBD_BADBLOCK_ERASENOOP, LFS_TESTBD_BADBLOCK_ERASENOOP,
}; } lfs_testbd_badblock_behavior_t;
// Mode determining how power-loss behaves during testing. For now this
// only supports a noop behavior, leaving the data on-disk untouched.
typedef enum lfs_testbd_powerloss_behavior {
LFS_TESTBD_POWERLOSS_NOOP,
} lfs_testbd_powerloss_behavior_t;
// Type for measuring wear // Type for measuring wear
typedef uint32_t lfs_testbd_wear_t; typedef uint32_t lfs_testbd_wear_t;
typedef int32_t lfs_testbd_swear_t; typedef int32_t lfs_testbd_swear_t;
// Type for tracking power-cycles
typedef uint32_t lfs_testbd_powercycles_t;
typedef int32_t lfs_testbd_spowercycles_t;
// testbd config, this is required for testing // testbd config, this is required for testing
struct lfs_testbd_config { struct lfs_testbd_config {
@@ -55,42 +65,77 @@ struct lfs_testbd_config {
int32_t erase_value; int32_t erase_value;
// Number of erase cycles before a block becomes "bad". The exact behavior // Number of erase cycles before a block becomes "bad". The exact behavior
// of bad blocks is controlled by the badblock_mode. // of bad blocks is controlled by badblock_behavior.
uint32_t erase_cycles; uint32_t erase_cycles;
// The mode determining how bad blocks fail // The mode determining how bad-blocks fail
uint8_t badblock_behavior; lfs_testbd_badblock_behavior_t badblock_behavior;
// Number of write operations (erase/prog) before forcefully killing // Number of write operations (erase/prog) before triggering a power-loss.
// the program with exit. Simulates power-loss. 0 disables. // power_cycles=0 disables this. The exact behavior of power-loss is
uint32_t power_cycles; // controlled by a combination of powerloss_behavior and powerloss_cb.
lfs_testbd_powercycles_t power_cycles;
// Optional buffer for RAM block device. // The mode determining how power-loss affects disk
void *buffer; lfs_testbd_powerloss_behavior_t powerloss_behavior;
// Optional buffer for wear. // Function to call to emulate power-loss. The exact behavior of power-loss
void *wear_buffer; // is up to the runner to provide.
void (*powerloss_cb)(void*);
// Optional buffer for scratch memory, needed when erase_value != -1. // Data for power-loss callback
void *scratch_buffer; void *powerloss_data;
// True to track when power-loss could have occured. Note this involves
// heavy memory usage!
bool track_branches;
// // Optional buffer for RAM block device.
// void *buffer;
//
// // Optional buffer for wear.
// void *wear_buffer;
//
// // Optional buffer for scratch memory, needed when erase_value != -1.
// void *scratch_buffer;
}; };
// A reference counted block
typedef struct lfs_testbd_block {
uint32_t rc;
lfs_testbd_wear_t wear;
uint8_t data[];
} lfs_testbd_block_t;
// testbd state // testbd state
typedef struct lfs_testbd { typedef struct lfs_testbd {
union { // array of copy-on-write blocks
struct { lfs_testbd_block_t **blocks;
lfs_filebd_t bd;
} file;
struct {
lfs_rambd_t bd;
struct lfs_rambd_config cfg;
} ram;
} u;
bool persist;
uint32_t power_cycles; uint32_t power_cycles;
lfs_testbd_wear_t *wear;
uint8_t *scratch; // array of tracked branches
struct lfs_testbd *branches;
lfs_testbd_powercycles_t branch_count;
lfs_testbd_powercycles_t branch_capacity;
// TODO file?
// union {
// struct {
// lfs_filebd_t bd;
// } file;
// struct {
// lfs_rambd_t bd;
// struct lfs_rambd_config cfg;
// } ram;
// } u;
//
// bool persist;
// uint32_t power_cycles;
// lfs_testbd_wear_t *wear;
// uint8_t *scratch;
const struct lfs_testbd_config *cfg; const struct lfs_testbd_config *cfg;
} lfs_testbd_t; } lfs_testbd_t;
@@ -139,6 +184,22 @@ lfs_testbd_swear_t lfs_testbd_getwear(const struct lfs_config *cfg,
int lfs_testbd_setwear(const struct lfs_config *cfg, int lfs_testbd_setwear(const struct lfs_config *cfg,
lfs_block_t block, lfs_testbd_wear_t wear); lfs_block_t block, lfs_testbd_wear_t wear);
// Get the remaining power-cycles
lfs_testbd_spowercycles_t lfs_testbd_getpowercycles(
const struct lfs_config *cfg);
// Manually set the remaining power-cycles
int lfs_testbd_setpowercycles(const struct lfs_config *cfg,
lfs_testbd_powercycles_t power_cycles);
// Get a power-loss branch, requires track_branches=true
int lfs_testbd_getbranch(const struct lfs_config *cfg,
lfs_testbd_powercycles_t branch, lfs_testbd_t *bd);
// Get the current number of power-loss branches
lfs_testbd_spowercycles_t lfs_testbd_getbranchcount(
const struct lfs_config *cfg);
#ifdef __cplusplus #ifdef __cplusplus
} /* extern "C" */ } /* extern "C" */

2
lfs.h
View File

@@ -8,8 +8,6 @@
#ifndef LFS_H #ifndef LFS_H
#define LFS_H #define LFS_H
#include <stdint.h>
#include <stdbool.h>
#include "lfs_util.h" #include "lfs_util.h"
#ifdef __cplusplus #ifdef __cplusplus

View File

@@ -23,6 +23,7 @@
// System includes // System includes
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <sys/types.h>
#include <string.h> #include <string.h>
#include <inttypes.h> #include <inttypes.h>

File diff suppressed because it is too large Load Diff

View File

@@ -5,18 +5,16 @@
// generated test configurations // generated test configurations
enum test_types { enum test_flags {
TEST_NORMAL = 0x1, TEST_REENTRANT = 0x1,
TEST_REENTRANT = 0x2,
}; };
typedef uint8_t test_flags_t;
typedef uint8_t test_types_t;
struct test_case { struct test_case {
const char *id; const char *id;
const char *name; const char *name;
const char *path; const char *path;
test_types_t types; test_flags_t flags;
size_t permutations; size_t permutations;
intmax_t (*const *const *defines)(void); intmax_t (*const *const *defines)(void);
@@ -29,7 +27,7 @@ struct test_suite {
const char *id; const char *id;
const char *name; const char *name;
const char *path; const char *path;
test_types_t types; test_flags_t flags;
const char *const *define_names; const char *const *define_names;
size_t define_count; size_t define_count;
@@ -54,6 +52,7 @@ intmax_t test_define(size_t define);
#define ERASE_VALUE test_predefine(7) #define ERASE_VALUE test_predefine(7)
#define ERASE_CYCLES test_predefine(8) #define ERASE_CYCLES test_predefine(8)
#define BADBLOCK_BEHAVIOR test_predefine(9) #define BADBLOCK_BEHAVIOR test_predefine(9)
#define POWERLOSS_BEHAVIOR test_predefine(10)
#define TEST_PREDEFINE_NAMES { \ #define TEST_PREDEFINE_NAMES { \
"READ_SIZE", \ "READ_SIZE", \
@@ -66,17 +65,19 @@ intmax_t test_define(size_t define);
"ERASE_VALUE", \ "ERASE_VALUE", \
"ERASE_CYCLES", \ "ERASE_CYCLES", \
"BADBLOCK_BEHAVIOR", \ "BADBLOCK_BEHAVIOR", \
"POWERLOSS_BEHAVIOR", \
} }
#define TEST_PREDEFINE_COUNT 10 #define TEST_PREDEFINE_COUNT 11
// default predefines // default predefines
#define TEST_DEFAULTS { \ #define TEST_DEFAULTS { \
/* LOOKAHEAD_SIZE */ 16, \ /* LOOKAHEAD_SIZE */ 16, \
/* BLOCK_CYCLES */ -1, \ /* BLOCK_CYCLES */ -1, \
/* ERASE_VALUE */ 0xff, \ /* ERASE_VALUE */ 0xff, \
/* ERASE_CYCLES */ 0, \ /* ERASE_CYCLES */ 0, \
/* BADBLOCK_BEHAVIOR */ LFS_TESTBD_BADBLOCK_PROGERROR, \ /* BADBLOCK_BEHAVIOR */ LFS_TESTBD_BADBLOCK_PROGERROR, \
/* POWERLOSS_BEHAVIOR */ LFS_TESTBD_POWERLOSS_NOOP, \
} }
#define TEST_DEFAULT_DEFINE_COUNT 5 #define TEST_DEFAULT_DEFINE_COUNT 5

View File

@@ -73,8 +73,6 @@ class TestCase:
self.in_ = config.pop('in', self.in_ = config.pop('in',
config.pop('suite_in', None)) config.pop('suite_in', None))
self.normal = config.pop('normal',
config.pop('suite_normal', True))
self.reentrant = config.pop('reentrant', self.reentrant = config.pop('reentrant',
config.pop('suite_reentrant', False)) config.pop('suite_reentrant', False))
@@ -159,7 +157,6 @@ class TestSuite:
# a couple of these we just forward to all cases # a couple of these we just forward to all cases
defines = config.pop('defines', {}) defines = config.pop('defines', {})
in_ = config.pop('in', None) in_ = config.pop('in', None)
normal = config.pop('normal', True)
reentrant = config.pop('reentrant', False) reentrant = config.pop('reentrant', False)
self.cases = [] self.cases = []
@@ -172,7 +169,6 @@ class TestSuite:
'suite': self.name, 'suite': self.name,
'suite_defines': defines, 'suite_defines': defines,
'suite_in': in_, 'suite_in': in_,
'suite_normal': normal,
'suite_reentrant': reentrant, 'suite_reentrant': reentrant,
**case})) **case}))
@@ -181,7 +177,6 @@ class TestSuite:
set(case.defines) for case in self.cases)) set(case.defines) for case in self.cases))
# combine other per-case things # combine other per-case things
self.normal = any(case.normal for case in self.cases)
self.reentrant = any(case.reentrant for case in self.cases) self.reentrant = any(case.reentrant for case in self.cases)
for k in config.keys(): for k in config.keys():
@@ -236,6 +231,12 @@ def compile(**args):
f.write = write f.write = write
f.writeln = writeln f.writeln = writeln
f.writeln("// Generated by %s:" % sys.argv[0])
f.writeln("//")
f.writeln("// %s" % ' '.join(sys.argv))
f.writeln("//")
f.writeln()
# redirect littlefs tracing # redirect littlefs tracing
f.writeln('#define LFS_TRACE_(fmt, ...) do { \\') f.writeln('#define LFS_TRACE_(fmt, ...) do { \\')
f.writeln(8*' '+'extern FILE *test_trace; \\') f.writeln(8*' '+'extern FILE *test_trace; \\')
@@ -366,10 +367,10 @@ def compile(**args):
f.writeln(4*' '+'.id = "%s",' % suite.id()) f.writeln(4*' '+'.id = "%s",' % suite.id())
f.writeln(4*' '+'.name = "%s",' % suite.name) f.writeln(4*' '+'.name = "%s",' % suite.name)
f.writeln(4*' '+'.path = "%s",' % suite.path) f.writeln(4*' '+'.path = "%s",' % suite.path)
f.writeln(4*' '+'.types = %s,' f.writeln(4*' '+'.flags = %s,'
% ' | '.join(filter(None, [ % (' | '.join(filter(None, [
'TEST_NORMAL' if suite.normal else None, 'TEST_REENTRANT' if suite.reentrant else None]))
'TEST_REENTRANT' if suite.reentrant else None]))) or 0))
if suite.defines: if suite.defines:
# create suite define names # create suite define names
f.writeln(4*' '+'.define_names = (const char *const[]){') f.writeln(4*' '+'.define_names = (const char *const[]){')
@@ -384,10 +385,10 @@ def compile(**args):
f.writeln(12*' '+'.id = "%s",' % case.id()) f.writeln(12*' '+'.id = "%s",' % case.id())
f.writeln(12*' '+'.name = "%s",' % case.name) f.writeln(12*' '+'.name = "%s",' % case.name)
f.writeln(12*' '+'.path = "%s",' % case.path) f.writeln(12*' '+'.path = "%s",' % case.path)
f.writeln(12*' '+'.types = %s,' f.writeln(12*' '+'.flags = %s,'
% ' | '.join(filter(None, [ % (' | '.join(filter(None, [
'TEST_NORMAL' if case.normal else None, 'TEST_REENTRANT' if case.reentrant else None]))
'TEST_REENTRANT' if case.reentrant else None]))) or 0))
f.writeln(12*' '+'.permutations = %d,' f.writeln(12*' '+'.permutations = %d,'
% len(case.permutations)) % len(case.permutations))
if case.defines: if case.defines:
@@ -461,12 +462,13 @@ def runner(**args):
'--error-exitcode=4', '--error-exitcode=4',
'-q']) '-q'])
# filter tests? # other context
if args.get('normal'): cmd.append('-n')
if args.get('reentrant'): cmd.append('-r')
if args.get('geometry'): if args.get('geometry'):
cmd.append('-G%s' % args.get('geometry')) cmd.append('-G%s' % args.get('geometry'))
if args.get('powerloss'):
cmd.append('-p%s' % args.get('powerloss'))
# defines? # defines?
if args.get('define'): if args.get('define'):
for define in args.get('define'): for define in args.get('define'):
@@ -476,12 +478,13 @@ def runner(**args):
def list_(**args): def list_(**args):
cmd = runner(**args) cmd = runner(**args)
if args.get('summary'): cmd.append('--summary') if args.get('summary'): cmd.append('--summary')
if args.get('list_suites'): cmd.append('--list-suites') if args.get('list_suites'): cmd.append('--list-suites')
if args.get('list_cases'): cmd.append('--list-cases') if args.get('list_cases'): cmd.append('--list-cases')
if args.get('list_paths'): cmd.append('--list-paths') if args.get('list_paths'): cmd.append('--list-paths')
if args.get('list_defines'): cmd.append('--list-defines') if args.get('list_defines'): cmd.append('--list-defines')
if args.get('list_geometries'): cmd.append('--list-geometries') if args.get('list_geometries'): cmd.append('--list-geometries')
if args.get('list_powerlosses'): cmd.append('--list-powerlosses')
if args.get('verbose'): if args.get('verbose'):
print(' '.join(shlex.quote(c) for c in cmd)) print(' '.join(shlex.quote(c) for c in cmd))
@@ -598,11 +601,12 @@ def run_stage(name, runner_, **args):
passed_suite_perms = co.defaultdict(lambda: 0) passed_suite_perms = co.defaultdict(lambda: 0)
passed_case_perms = co.defaultdict(lambda: 0) passed_case_perms = co.defaultdict(lambda: 0)
passed_perms = 0 passed_perms = 0
powerlosses = 0
failures = [] failures = []
killed = False killed = False
pattern = re.compile('^(?:' pattern = re.compile('^(?:'
'(?P<op>running|finished|skipped) ' '(?P<op>running|finished|skipped|powerloss) '
'(?P<id>(?P<case>(?P<suite>[^#]+)#[^\s#]+)[^\s]*)' '(?P<id>(?P<case>(?P<suite>[^#]+)#[^\s#]+)[^\s]*)'
'|' '(?P<path>[^:]+):(?P<lineno>\d+):(?P<op_>assert):' '|' '(?P<path>[^:]+):(?P<lineno>\d+):(?P<op_>assert):'
' *(?P<message>.*)' ')$') ' *(?P<message>.*)' ')$')
@@ -613,6 +617,7 @@ def run_stage(name, runner_, **args):
nonlocal passed_suite_perms nonlocal passed_suite_perms
nonlocal passed_case_perms nonlocal passed_case_perms
nonlocal passed_perms nonlocal passed_perms
nonlocal powerlosses
nonlocal locals nonlocal locals
# run the tests! # run the tests!
@@ -659,6 +664,9 @@ def run_stage(name, runner_, **args):
last_id = m.group('id') last_id = m.group('id')
last_output = [] last_output = []
last_assert = None last_assert = None
elif op == 'powerloss':
last_id = m.group('id')
powerlosses += 1
elif op == 'finished': elif op == 'finished':
passed_suite_perms[m.group('suite')] += 1 passed_suite_perms[m.group('suite')] += 1
passed_case_perms[m.group('case')] += 1 passed_case_perms[m.group('case')] += 1
@@ -766,6 +774,8 @@ def run_stage(name, runner_, **args):
len(expected_case_perms)) len(expected_case_perms))
if not args.get('by_cases') else None, if not args.get('by_cases') else None,
'%d/%d perms' % (passed_perms, expected_perms), '%d/%d perms' % (passed_perms, expected_perms),
'%dpls!' % powerlosses
if powerlosses else None,
'\x1b[31m%d/%d failures\x1b[m' '\x1b[31m%d/%d failures\x1b[m'
% (len(failures), expected_perms) % (len(failures), expected_perms)
if failures else None])))) if failures else None]))))
@@ -785,6 +795,7 @@ def run_stage(name, runner_, **args):
return ( return (
expected_perms, expected_perms,
passed_perms, passed_perms,
powerlosses,
failures, failures,
killed) killed)
@@ -806,33 +817,34 @@ def run(**args):
expected = 0 expected = 0
passed = 0 passed = 0
powerlosses = 0
failures = [] failures = []
for type, by in it.product( for by in (expected_case_perms.keys() if args.get('by_cases')
['normal', 'reentrant'], else expected_suite_perms.keys() if args.get('by_suites')
expected_case_perms.keys() if args.get('by_cases') else [None]):
else expected_suite_perms.keys() if args.get('by_suites')
else [None]):
# rebuild runner for each stage to override test identifier if needed # rebuild runner for each stage to override test identifier if needed
stage_runner = runner(**args | { stage_runner = runner(**args | {
'test_ids': [by] if by is not None else args.get('test_ids', []), 'test_ids': [by] if by is not None else args.get('test_ids', [])})
'normal': type == 'normal',
'reentrant': type == 'reentrant'})
# spawn jobs for stage # spawn jobs for stage
expected_, passed_, failures_, killed = run_stage( expected_, passed_, powerlosses_, failures_, killed = run_stage(
'%s %s' % (type, by or 'tests'), stage_runner, **args) by or 'tests', stage_runner, **args)
expected += expected_ expected += expected_
passed += passed_ passed += passed_
powerlosses += powerlosses_
failures.extend(failures_) failures.extend(failures_)
if (failures and not args.get('keep_going')) or killed: if (failures and not args.get('keep_going')) or killed:
break break
# show summary # show summary
print() print()
print('\x1b[%dmdone:\x1b[m %d/%d passed, %d/%d failed, in %.2fs' print('\x1b[%dmdone:\x1b[m %s' # %d/%d passed, %d/%d failed%s, in %.2fs'
% (32 if not failures else 31, % (32 if not failures else 31,
passed, expected, len(failures), expected, ', '.join(filter(None, [
time.time()-start)) '%d/%d passed' % (passed, expected),
'%d/%d failed' % (len(failures), expected),
'%dpls!' % powerlosses if powerlosses else None,
'in %.2fs' % (time.time()-start)]))))
print() print()
# print each failure # print each failure
@@ -844,7 +856,7 @@ def run(**args):
for failure in failures: for failure in failures:
# show summary of failure # show summary of failure
path, lineno = runner_paths[testcase(failure.id)] path, lineno = runner_paths[testcase(failure.id)]
defines = runner_defines[failure.id] defines = runner_defines.get(failure.id, {})
print('\x1b[01m%s:%d:\x1b[01;31mfailure:\x1b[m %s%s failed' print('\x1b[01m%s:%d:\x1b[01;31mfailure:\x1b[m %s%s failed'
% (path, lineno, failure.id, % (path, lineno, failure.id,
@@ -913,8 +925,9 @@ def main(**args):
or args.get('list_cases') or args.get('list_cases')
or args.get('list_paths') or args.get('list_paths')
or args.get('list_defines') or args.get('list_defines')
or args.get('list_defaults')
or args.get('list_geometries') or args.get('list_geometries')
or args.get('list_defaults')): or args.get('list_powerlosses')):
list_(**args) list_(**args)
else: else:
run(**args) run(**args)
@@ -930,7 +943,7 @@ if __name__ == "__main__":
help="Description of testis to run. May be a directory, path, or \ help="Description of testis to run. May be a directory, path, or \
test identifier. Test identifiers are of the form \ test identifier. Test identifiers are of the form \
<suite_name>#<case_name>#<permutation>, but suffixes can be \ <suite_name>#<case_name>#<permutation>, but suffixes can be \
dropped to run any matching tests. Defaults to %r." % TEST_PATHS) dropped to run any matching tests. Defaults to %s." % TEST_PATHS)
parser.add_argument('-v', '--verbose', action='store_true', parser.add_argument('-v', '--verbose', action='store_true',
help="Output commands that run behind the scenes.") help="Output commands that run behind the scenes.")
# test flags # test flags
@@ -945,20 +958,21 @@ if __name__ == "__main__":
help="List the path for each test case.") help="List the path for each test case.")
test_parser.add_argument('--list-defines', action='store_true', test_parser.add_argument('--list-defines', action='store_true',
help="List the defines for each test permutation.") help="List the defines for each test permutation.")
test_parser.add_argument('--list-geometries', action='store_true',
help="List the disk geometries used for testing.")
test_parser.add_argument('--list-defaults', action='store_true', test_parser.add_argument('--list-defaults', action='store_true',
help="List the default defines in this test-runner.") help="List the default defines in this test-runner.")
test_parser.add_argument('--list-geometries', action='store_true',
help="List the disk geometries used for testing.")
test_parser.add_argument('--list-powerlosses', action='store_true',
help="List the available power-loss scenarios.")
test_parser.add_argument('-D', '--define', action='append', test_parser.add_argument('-D', '--define', action='append',
help="Override a test define.") help="Override a test define.")
test_parser.add_argument('-G', '--geometry', test_parser.add_argument('-G', '--geometry',
help="Filter by geometry.") help="Filter by geometry.")
test_parser.add_argument('-n', '--normal', action='store_true', test_parser.add_argument('-p', '--powerloss',
help="Filter for normal tests. Can be combined.") help="Comma-separated list of power-loss scenarios to test. \
test_parser.add_argument('-r', '--reentrant', action='store_true', Defaults to 0,l.")
help="Filter for reentrant tests. Can be combined.")
test_parser.add_argument('-d', '--disk', test_parser.add_argument('-d', '--disk',
help="Use this file as the disk.") help="Redirect block device operations to this file.")
test_parser.add_argument('-t', '--trace', test_parser.add_argument('-t', '--trace',
help="Redirect trace output to this file.") help="Redirect trace output to this file.")
test_parser.add_argument('-o', '--output', test_parser.add_argument('-o', '--output',