Added generalize fuzz testing for B-trees, fixed single-child bug
A single child is just another condition to watch out for during B-tree merge, since a single-child obviously can't have a sibling. This is a good safety to have, but I was surprised this can happen. But it turns out to be quite easy since our rbyds defer the B-tree operations until compaction. A merge down to a single child won't propagate the merge until the parent compacts.
This commit is contained in:
@@ -2232,6 +2232,7 @@ static int lfsr_rbyd_append(lfs_t *lfs, lfsr_rbyd_t *rbyd,
|
|||||||
other_id_ = id_;
|
other_id_ = id_;
|
||||||
} else if (tag == LFSR_TAG_SHRINK) {
|
} else if (tag == LFSR_TAG_SHRINK) {
|
||||||
LFS_ASSERT(id < rbyd->weight);
|
LFS_ASSERT(id < rbyd->weight);
|
||||||
|
LFS_ASSERT(lfsr_data_len(data) <= rbyd->weight);
|
||||||
// noop?
|
// noop?
|
||||||
if (lfsr_data_len(data) == 0) {
|
if (lfsr_data_len(data) == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -3320,8 +3321,6 @@ static lfs_ssize_t lfsr_btree_get(lfs_t *lfs,
|
|||||||
buffer, size);
|
buffer, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO drop the root during merges
|
|
||||||
// TODO inline?
|
|
||||||
static int lfsr_btree_commit(lfs_t *lfs,
|
static int lfsr_btree_commit(lfs_t *lfs,
|
||||||
lfsr_btree_t *btree, lfs_size_t id,
|
lfsr_btree_t *btree, lfs_size_t id,
|
||||||
lfsr_rbyd_t *rbyd, const struct lfsr_attr *attrs) {
|
lfsr_rbyd_t *rbyd, const struct lfsr_attr *attrs) {
|
||||||
@@ -3821,6 +3820,11 @@ static int lfsr_btree_commit(lfs_t *lfs,
|
|||||||
goto abort;
|
goto abort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// only child? can't merge
|
||||||
|
if (pweight == parent.weight) {
|
||||||
|
goto abort;
|
||||||
|
}
|
||||||
|
|
||||||
// last child? try the left sibling
|
// last child? try the left sibling
|
||||||
lfs_ssize_t sid;
|
lfs_ssize_t sid;
|
||||||
lfs_ssize_t sdelta;
|
lfs_ssize_t sdelta;
|
||||||
|
|||||||
+279
-8
@@ -452,7 +452,6 @@ code = '''
|
|||||||
memmove(&sim[id+1], &sim[id], sim_size-id);
|
memmove(&sim[id+1], &sim[id], sim_size-id);
|
||||||
sim[id] = alphas[i % 26];
|
sim[id] = alphas[i % 26];
|
||||||
sim_size += 1;
|
sim_size += 1;
|
||||||
printf("%c\n", alphas[i % 26]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that btree matches sim
|
// check that btree matches sim
|
||||||
@@ -2068,10 +2067,282 @@ code = '''
|
|||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
#
|
|
||||||
## [cases.test_btree_split]
|
# TODO [cases.test_btree_split]
|
||||||
## [cases.test_btree_split_fuzz]
|
# TODO [cases.test_btree_split_fuzz]
|
||||||
#
|
|
||||||
## [cases.test_btree_general_fuzz]
|
|
||||||
#
|
|
||||||
## TODO also test copy-on-write!
|
# Some more general fuzz testing
|
||||||
|
[cases.test_btree_general_fuzz]
|
||||||
|
defines.N = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
|
||||||
|
defines.ITER = 100
|
||||||
|
in = 'lfs.c'
|
||||||
|
code = '''
|
||||||
|
const char *alphas = "abcdefghijklmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
// iterate through severals seeds that we can reproduce easily
|
||||||
|
for (uint32_t seed = 1; seed < ITER+1; seed++) {
|
||||||
|
// create lfs here since we need to reset each iteration, we're
|
||||||
|
// space constrained and we can't expect gc to work at this point
|
||||||
|
lfs_t lfs;
|
||||||
|
lfs_init(&lfs, cfg) => 0;
|
||||||
|
// create free lookahead
|
||||||
|
memset(lfs.free.buffer, 0, lfs.cfg->lookahead_size);
|
||||||
|
lfs.free.off = 0;
|
||||||
|
lfs.free.size = lfs_min(8*lfs.cfg->lookahead_size,
|
||||||
|
lfs.cfg->block_count);
|
||||||
|
lfs.free.i = 0;
|
||||||
|
lfs_alloc_ack(&lfs);
|
||||||
|
|
||||||
|
// create a btree
|
||||||
|
lfsr_btree_t btree = LFSR_BTREE_NULL;
|
||||||
|
|
||||||
|
// set up a simulation to compare against
|
||||||
|
//
|
||||||
|
// fun fact this is slower than our actual tree! unfun fact this is
|
||||||
|
// starting to be a problem...
|
||||||
|
char *sim = malloc(N);
|
||||||
|
lfs_size_t sim_size = 0;
|
||||||
|
memset(sim, 0, N);
|
||||||
|
|
||||||
|
uint32_t prng = seed;
|
||||||
|
for (lfs_size_t i = 0; i < (N-REMAINING); i++) {
|
||||||
|
// choose a pseudo-random op
|
||||||
|
uint8_t op = TEST_PRNG(&prng) % 3;
|
||||||
|
// choose a pseudo-random id
|
||||||
|
lfs_size_t id = TEST_PRNG(&prng) % (sim_size+1);
|
||||||
|
|
||||||
|
if (op == 0 || id == sim_size) {
|
||||||
|
// push to btree
|
||||||
|
lfsr_btree_push(&lfs, &btree, id,
|
||||||
|
LFSR_TAG_INLINED, 1,
|
||||||
|
&alphas[i % 26], 1) => 0;
|
||||||
|
|
||||||
|
// push to sim
|
||||||
|
memmove(&sim[id+1], &sim[id], sim_size-id);
|
||||||
|
sim[id] = alphas[i % 26];
|
||||||
|
sim_size += 1;
|
||||||
|
|
||||||
|
} else if (op == 1) {
|
||||||
|
// update btree
|
||||||
|
lfsr_btree_update(&lfs, &btree, id,
|
||||||
|
LFSR_TAG_INLINED, 1,
|
||||||
|
&alphas[i % 26], 1) => 0;
|
||||||
|
|
||||||
|
// update sim
|
||||||
|
sim[id] = alphas[i % 26];
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// pop from btree
|
||||||
|
lfsr_btree_pop(&lfs, &btree, id) => 0;
|
||||||
|
|
||||||
|
// pop from sim
|
||||||
|
memmove(&sim[id], &sim[id+1], sim_size-(id+1));
|
||||||
|
sim_size -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that btree matches sim
|
||||||
|
printf("expd: [");
|
||||||
|
bool first = true;
|
||||||
|
for (lfs_size_t i = 0; i < sim_size; i++) {
|
||||||
|
if (!first) {
|
||||||
|
printf(", ");
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
printf("%c", sim[i]);
|
||||||
|
}
|
||||||
|
printf("]\n");
|
||||||
|
printf("btree: 0x%x.%x 0x%x w%d\n",
|
||||||
|
btree.u.trunk.block,
|
||||||
|
btree.u.trunk.limit,
|
||||||
|
btree.tag,
|
||||||
|
btree.weight);
|
||||||
|
assert(btree.weight == sim_size);
|
||||||
|
|
||||||
|
uint8_t buffer[4];
|
||||||
|
lfsr_tag_t tag_;
|
||||||
|
lfs_size_t id_;
|
||||||
|
lfs_size_t weight_;
|
||||||
|
for (lfs_size_t i = 0; i < sim_size; i++) {
|
||||||
|
lfsr_btree_get(&lfs, &btree, i,
|
||||||
|
&tag_, &id_, &weight_,
|
||||||
|
buffer, 4) => 1;
|
||||||
|
assert(tag_ == LFSR_TAG_INLINED);
|
||||||
|
assert(id_ == i);
|
||||||
|
assert(weight_ == 1);
|
||||||
|
assert(memcmp(buffer, &sim[i], 1) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// and no extra elements
|
||||||
|
lfsr_btree_get(&lfs, &btree, sim_size,
|
||||||
|
&tag_, &id_, &weight_,
|
||||||
|
buffer, 4) => LFS_ERR_NOENT;
|
||||||
|
|
||||||
|
// clean up sim
|
||||||
|
free(sim);
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
[cases.test_btree_general_sparse_fuzz]
|
||||||
|
defines.N = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
|
||||||
|
defines.W = 5
|
||||||
|
defines.ITER = 100
|
||||||
|
in = 'lfs.c'
|
||||||
|
code = '''
|
||||||
|
const char *alphas = "abcdefghijklmnopqrstuvwxyz";
|
||||||
|
|
||||||
|
// iterate through severals seeds that we can reproduce easily
|
||||||
|
for (uint32_t seed = 1; seed < ITER+1; seed++) {
|
||||||
|
// create lfs here since we need to reset each iteration, we're
|
||||||
|
// space constrained and we can't expect gc to work at this point
|
||||||
|
lfs_t lfs;
|
||||||
|
lfs_init(&lfs, cfg) => 0;
|
||||||
|
// create free lookahead
|
||||||
|
memset(lfs.free.buffer, 0, lfs.cfg->lookahead_size);
|
||||||
|
lfs.free.off = 0;
|
||||||
|
lfs.free.size = lfs_min(8*lfs.cfg->lookahead_size,
|
||||||
|
lfs.cfg->block_count);
|
||||||
|
lfs.free.i = 0;
|
||||||
|
lfs_alloc_ack(&lfs);
|
||||||
|
|
||||||
|
// create a btree
|
||||||
|
lfsr_btree_t btree = LFSR_BTREE_NULL;
|
||||||
|
|
||||||
|
// set up a simulation to compare against
|
||||||
|
//
|
||||||
|
// fun fact this is slower than our actual tree! unfun fact this is
|
||||||
|
// starting to be a problem...
|
||||||
|
char *sim = malloc(N);
|
||||||
|
lfs_size_t *sim_weights = malloc(N*sizeof(lfs_size_t));
|
||||||
|
lfs_size_t sim_size = 0;
|
||||||
|
memset(sim, 0, N);
|
||||||
|
memset(sim_weights, 0, N*sizeof(lfs_size_t));
|
||||||
|
|
||||||
|
uint32_t prng = seed;
|
||||||
|
for (lfs_size_t i = 0; i < (N-REMAINING); i++) {
|
||||||
|
// choose a pseudo-random op
|
||||||
|
uint8_t op = TEST_PRNG(&prng) % 3;
|
||||||
|
// choose a pseudo-random id
|
||||||
|
lfs_size_t id = TEST_PRNG(&prng) % (sim_size+1);
|
||||||
|
// choose a pseudo-random weight
|
||||||
|
lfs_size_t weight = 1 + (TEST_PRNG(&prng) % W);
|
||||||
|
|
||||||
|
// calculate actual id in btree space
|
||||||
|
lfs_size_t weighted_id = 0;
|
||||||
|
for (lfs_size_t j = 0; j < id; j++) {
|
||||||
|
weighted_id += sim_weights[j];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op == 0 || id == sim_size) {
|
||||||
|
// push to btree
|
||||||
|
lfsr_btree_push(&lfs, &btree, weighted_id,
|
||||||
|
LFSR_TAG_INLINED, weight,
|
||||||
|
&alphas[i % 26], 1) => 0;
|
||||||
|
|
||||||
|
// push to sim
|
||||||
|
memmove(&sim[id+1], &sim[id], sim_size-id);
|
||||||
|
memmove(&sim_weights[id+1], &sim_weights[id],
|
||||||
|
(sim_size-id)*sizeof(lfs_size_t));
|
||||||
|
sim[id] = alphas[i % 26];
|
||||||
|
sim_weights[id] = weight;
|
||||||
|
sim_size += 1;
|
||||||
|
|
||||||
|
} else if (op == 1) {
|
||||||
|
// update btree
|
||||||
|
lfsr_btree_update(&lfs, &btree,
|
||||||
|
weighted_id+sim_weights[id]-1, LFSR_TAG_INLINED, weight,
|
||||||
|
&alphas[i % 26], 1) => 0;
|
||||||
|
|
||||||
|
// update sim
|
||||||
|
sim[id] = alphas[i % 26];
|
||||||
|
sim_weights[id] = weight;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// remove from btree
|
||||||
|
lfsr_btree_pop(&lfs, &btree,
|
||||||
|
weighted_id+sim_weights[id]-1) => 0;
|
||||||
|
|
||||||
|
// remove from sim
|
||||||
|
memmove(&sim[id], &sim[id+1], sim_size-(id+1));
|
||||||
|
memmove(&sim_weights[id], &sim_weights[id+1],
|
||||||
|
(sim_size-(id+1))*sizeof(lfs_size_t));
|
||||||
|
sim_size -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that btree matches sim
|
||||||
|
printf("expd: [");
|
||||||
|
bool first = true;
|
||||||
|
for (lfs_size_t i = 0; i < sim_size; i++) {
|
||||||
|
if (!first) {
|
||||||
|
printf(", ");
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
printf("%c", sim[i]);
|
||||||
|
}
|
||||||
|
printf("]\n");
|
||||||
|
printf("btree: 0x%x.%x 0x%x w%d\n",
|
||||||
|
btree.u.trunk.block,
|
||||||
|
btree.u.trunk.limit,
|
||||||
|
btree.tag,
|
||||||
|
btree.weight);
|
||||||
|
|
||||||
|
lfs_size_t total_weight = 0;
|
||||||
|
for (lfs_size_t j = 0; j < sim_size; j++) {
|
||||||
|
total_weight += sim_weights[j];
|
||||||
|
}
|
||||||
|
assert(btree.weight == total_weight);
|
||||||
|
|
||||||
|
uint8_t buffer[4];
|
||||||
|
lfsr_tag_t tag_;
|
||||||
|
lfs_size_t id_;
|
||||||
|
lfs_size_t weight_;
|
||||||
|
for (lfs_size_t i = 0; i < sim_size; i++) {
|
||||||
|
// calculate actual id in btree space
|
||||||
|
lfs_size_t weighted_id = 0;
|
||||||
|
for (lfs_size_t j = 0; j < i; j++) {
|
||||||
|
weighted_id += sim_weights[j];
|
||||||
|
}
|
||||||
|
|
||||||
|
lfsr_btree_get(&lfs, &btree, weighted_id+sim_weights[i]-1,
|
||||||
|
&tag_, &id_, &weight_,
|
||||||
|
buffer, 4) => 1;
|
||||||
|
assert(tag_ == LFSR_TAG_INLINED);
|
||||||
|
assert(id_ == weighted_id+sim_weights[i]-1);
|
||||||
|
assert(weight_ == sim_weights[i]);
|
||||||
|
assert(memcmp(buffer, &sim[i], 1) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// and no extra elements
|
||||||
|
lfsr_btree_get(&lfs, &btree, total_weight,
|
||||||
|
&tag_, &id_, &weight_,
|
||||||
|
buffer, 4) => LFS_ERR_NOENT;
|
||||||
|
|
||||||
|
// also test that we can traverse the tree without prior knowledge
|
||||||
|
id_ = -1;
|
||||||
|
for (lfs_size_t i = 0; i < sim_size; i++) {
|
||||||
|
// calculate actual id in btree space
|
||||||
|
lfs_size_t weighted_id = 0;
|
||||||
|
for (lfs_size_t j = 0; j < i; j++) {
|
||||||
|
weighted_id += sim_weights[j];
|
||||||
|
}
|
||||||
|
|
||||||
|
lfsr_btree_get(&lfs, &btree, id_+1,
|
||||||
|
&tag_, &id_, &weight_,
|
||||||
|
buffer, 4) => 1;
|
||||||
|
assert(tag_ == LFSR_TAG_INLINED);
|
||||||
|
assert(id_ == weighted_id+sim_weights[i]-1);
|
||||||
|
assert(weight_ == sim_weights[i]);
|
||||||
|
assert(memcmp(buffer, &sim[i], 1) == 0);
|
||||||
|
}
|
||||||
|
lfsr_btree_get(&lfs, &btree, id_+1,
|
||||||
|
&tag_, &id_, &weight_,
|
||||||
|
buffer, 4) => LFS_ERR_NOENT;
|
||||||
|
|
||||||
|
// clean up sim
|
||||||
|
free(sim);
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user