diff --git a/lfs.c b/lfs.c index 33fe615a..b2c37543 100644 --- a/lfs.c +++ b/lfs.c @@ -4420,6 +4420,31 @@ static inline lfs_size_t lfsr_mdir_weight(const lfsr_mdir_t *mdir) { return mdir->rbyd.weight; } +// track "opened" mdirs that may need to by updated +static void lfsr_mdir_addopened(lfs_t *lfs, lfsr_openedmdir_t *opened) { + opened->next = lfs->opened; + lfs->opened = opened; +} + +static void lfsr_mdir_removeopened(lfs_t *lfs, lfsr_openedmdir_t *opened) { + for (lfsr_openedmdir_t **p = &lfs->opened; *p; p = &(*p)->next) { + if (*p == opened) { + *p = (*p)->next; + break; + } + } +} + +static bool lfsr_mdir_isopened(lfs_t *lfs, const lfsr_openedmdir_t *opened) { + for (lfsr_openedmdir_t *p = lfs->opened; p; p = p->next) { + if (p == opened) { + return true; + } + } + + return false; +} + static int lfsr_mdir_alloc(lfs_t *lfs, lfsr_mdir_t *mdir, lfs_ssize_t mid) { // allocate two blocks lfs_block_t blocks[2]; @@ -4756,6 +4781,7 @@ compact:; static int lfsr_mdir_commit(lfs_t *lfs, lfsr_mdir_t *mdir, lfs_ssize_t *rid, const lfsr_attr_t *attrs, lfs_size_t attr_count) { + LFS_ASSERT(mdir->mid != -2); // attempt to commit/compact the mdir normally lfsr_mdir_t mdir_ = *mdir; @@ -5321,10 +5347,46 @@ static int lfsr_mdir_commit(lfs_t *lfs, lfsr_mdir_t *mdir, lfs_ssize_t *rid, lfs->mroot = mroot_; lfs->mtree = mtree_; + // update any opened mdirs + for (lfsr_openedmdir_t *opened = lfs->opened; + opened; + opened = opened->next) { + if (opened->mdir.mid == mdir->mid + // avoid double-updating our current mdir + && &opened->mdir != mdir) { + LFS_ASSERT(opened->rid < (lfs_ssize_t)opened->mdir.rbyd.weight); + LFS_ASSERT(opened->rid != -1); + + // first play out any attrs that change our rid + for (lfs_size_t i = 0; i < attr_count; i++) { + if (opened->rid >= attrs[i].id) { + // removed? + if (opened->rid + attrs[i].delta < attrs[i].id) { + opened->rid = -2; + opened->mdir.mid = -2; + } else { + opened->rid += attrs[i].delta; + } + } + } + + // update mdir to follow rid + if (opened->rid == -2) { + // skip removed mdirs + } else if ((lfs_size_t)opened->rid < mdir_.rbyd.weight) { + opened->mdir = mdir_; + } else { + opened->rid = opened->rid - mdir_.rbyd.weight; + opened->mdir = msibling_; + } + } + } + // update mdir to follow requested rid lfs_ssize_t rid_ = *rid; LFS_ASSERT(rid_ <= (lfs_ssize_t)mdir->rbyd.weight); - if (rid_ < 0) { + LFS_ASSERT(rid_ != -2); + if (rid_ == -1) { *mdir = mroot_; } else if ((lfs_size_t)rid_ < mdir_.rbyd.weight) { *mdir = mdir_; @@ -10222,6 +10284,8 @@ static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) { lfs->lfs1 = NULL; #endif + lfs->opened = NULL; + return 0; cleanup: diff --git a/lfs.h b/lfs.h index c0f2964b..5af32386 100644 --- a/lfs.h +++ b/lfs.h @@ -371,8 +371,7 @@ typedef union lfsr_btree { } lfsr_btree_t; typedef struct lfsr_mdir { - // -3 => deleted - // -2 => an out-of-tree mdir + // -2 => deleted // -1 => mroot // >=0 => bid in the mtree lfs_ssize_t mid; @@ -380,6 +379,13 @@ typedef struct lfsr_mdir { lfsr_rbyd_t rbyd; } lfsr_mdir_t; +typedef struct lfsr_openedmdir { + struct lfsr_openedmdir *next; + // this rid is followed during splits + lfs_ssize_t rid; + lfsr_mdir_t mdir; +} lfsr_openedmdir_t; + typedef struct lfs_mdir { lfs_block_t pair[2]; @@ -473,6 +479,9 @@ typedef struct lfs { lfsr_mdir_t mroot; lfsr_btree_t mtree; + // linked-list of opened mdirs + lfsr_openedmdir_t *opened; + #ifdef LFS_MIGRATE struct lfs1 *lfs1; #endif diff --git a/tests/test_mtree.toml b/tests/test_mtree.toml index 44f4b955..eb84a6aa 100644 --- a/tests/test_mtree.toml +++ b/tests/test_mtree.toml @@ -2461,3 +2461,445 @@ code = ''' lfsr_unmount(&lfs) => 0; } ''' + + +## Neighboring mdir updates ## + +[cases.test_mtree_neighbor] +in = 'lfs.c' +code = ''' + const char *alphas = "abcdefghijklmnopqrstuvwxyz"; + lfs_t lfs; + lfsr_format(&lfs, cfg) => 0; + lfsr_mount(&lfs, cfg) => 0; + + // setup our neighbors + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(0, MKINLINED, +1, &alphas[0 % 26], 1), + LFSR_ATTR(1, MKINLINED, +1, &alphas[1 % 26], 1))) => 0; + // this test only works if these all fit in the mroot + assert(lfsr_mtree_isinlined(&lfs)); + + lfsr_openedmdir_t left_neighbor = {.rid=0, .mdir=lfs.mroot}; + lfsr_openedmdir_t right_neighbor = {.rid=1, .mdir=lfs.mroot}; + lfsr_mdir_addopened(&lfs, &left_neighbor); + lfsr_mdir_addopened(&lfs, &right_neighbor); + + // insert a new entry, this should update our neighbors + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(1, MKINLINED, +1, &alphas[2 % 26], 1))) => 0; + + // assert that our entry is still in the mtree + assert(lfs.mroot.rbyd.weight == 3); + + uint8_t buffer[1]; + lfsr_mdir_get(&lfs, &lfs.mroot, 1, LFSR_TAG_INLINED, + buffer, 1) => 1; + assert(memcmp(buffer, &alphas[2 % 26], 1) == 0); + + // assert that our neighbors were updated correctly + assert(left_neighbor.rid == 0); + assert(left_neighbor.mdir.mid == -1); + assert(memcmp(&left_neighbor.mdir, &lfs.mroot, sizeof(lfsr_mdir_t)) == 0); + assert(right_neighbor.rid == 2); + assert(right_neighbor.mdir.mid == -1); + assert(memcmp(&right_neighbor.mdir, &lfs.mroot, sizeof(lfsr_mdir_t)) == 0); + + lfsr_mdir_removeopened(&lfs, &left_neighbor); + lfsr_mdir_removeopened(&lfs, &right_neighbor); + lfsr_unmount(&lfs) => 0; +''' + +[cases.test_mtree_neighbor_remove_l] +in = 'lfs.c' +code = ''' + const char *alphas = "abcdefghijklmnopqrstuvwxyz"; + lfs_t lfs; + lfsr_format(&lfs, cfg) => 0; + lfsr_mount(&lfs, cfg) => 0; + + // setup our neighbors + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(0, MKINLINED, +1, &alphas[0 % 26], 1), + LFSR_ATTR(1, MKINLINED, +1, &alphas[1 % 26], 1))) => 0; + // this test only works if these all fit in the mroot + assert(lfsr_mtree_isinlined(&lfs)); + + lfsr_openedmdir_t left_neighbor = {.rid=0, .mdir=lfs.mroot}; + lfsr_openedmdir_t right_neighbor = {.rid=1, .mdir=lfs.mroot}; + lfsr_mdir_addopened(&lfs, &left_neighbor); + lfsr_mdir_addopened(&lfs, &right_neighbor); + + // try removing our left entry + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(0, MKUNR, -1, NULL, 0))) => 0; + + // assert that an entry was removed + assert(lfs.mroot.rbyd.weight == 1); + + // assert that our neighbors were updated correctly + assert(left_neighbor.rid == -2); + assert(left_neighbor.mdir.mid == -2); + assert(right_neighbor.rid == 0); + assert(right_neighbor.mdir.mid == -1); + assert(memcmp(&right_neighbor.mdir, &lfs.mroot, sizeof(lfsr_mdir_t)) == 0); + + lfsr_mdir_removeopened(&lfs, &left_neighbor); + lfsr_mdir_removeopened(&lfs, &right_neighbor); + lfsr_unmount(&lfs) => 0; +''' + +[cases.test_mtree_neighbor_remove_r] +in = 'lfs.c' +code = ''' + const char *alphas = "abcdefghijklmnopqrstuvwxyz"; + lfs_t lfs; + lfsr_format(&lfs, cfg) => 0; + lfsr_mount(&lfs, cfg) => 0; + + // setup our neighbors + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(0, MKINLINED, +1, &alphas[0 % 26], 1), + LFSR_ATTR(1, MKINLINED, +1, &alphas[1 % 26], 1))) => 0; + // this test only works if these all fit in the mroot + assert(lfsr_mtree_isinlined(&lfs)); + + lfsr_openedmdir_t left_neighbor = {.rid=0, .mdir=lfs.mroot}; + lfsr_openedmdir_t right_neighbor = {.rid=1, .mdir=lfs.mroot}; + lfsr_mdir_addopened(&lfs, &left_neighbor); + lfsr_mdir_addopened(&lfs, &right_neighbor); + + // try removing our left entry + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(1, MKUNR, -1, NULL, 0))) => 0; + + // assert that an entry was removed + assert(lfs.mroot.rbyd.weight == 1); + + // assert that our neighbors were updated correctly + assert(left_neighbor.rid == 0); + assert(left_neighbor.mdir.mid == -1); + assert(memcmp(&left_neighbor.mdir, &lfs.mroot, sizeof(lfsr_mdir_t)) == 0); + assert(right_neighbor.rid == -2); + assert(right_neighbor.mdir.mid == -2); + + lfsr_mdir_removeopened(&lfs, &left_neighbor); + lfsr_mdir_removeopened(&lfs, &right_neighbor); + lfsr_unmount(&lfs) => 0; +''' + +[cases.test_mtree_neighbor_uninline] +# this should be set so only one entry can fit in a metadata block +defines.SIZE = 'BLOCK_SIZE / 4' +in = 'lfs.c' +code = ''' + const char *alphas = "abcdefghijklmnopqrstuvwxyz"; + lfs_t lfs; + lfsr_format(&lfs, cfg) => 0; + lfsr_mount(&lfs, cfg) => 0; + + // setup our neighbors + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(0, MKINLINED, +1, &alphas[0 % 26], 1), + LFSR_ATTR(1, MKINLINED, +1, &alphas[1 % 26], 1))) => 0; + // this test only works if these all fit in the mroot + assert(lfsr_mtree_isinlined(&lfs)); + + lfsr_openedmdir_t left_neighbor = {.rid=0, .mdir=lfs.mroot}; + lfsr_openedmdir_t right_neighbor = {.rid=1, .mdir=lfs.mroot}; + lfsr_mdir_addopened(&lfs, &left_neighbor); + lfsr_mdir_addopened(&lfs, &right_neighbor); + + // prepare mroot with a large attr so the next entry can not fit + uint8_t buffer[SIZE]; + memset(buffer, alphas[2 % 26], SIZE); + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(-1, UATTR(1), 0, buffer, SIZE))) => 0; + + // create a large entry that needs to be uninlined (but not split!) + memset(buffer, alphas[3 % 26], SIZE); + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(1, MKINLINED, +1, buffer, SIZE))) => 0; + + // force mroot to compact + lfs.mroot.rbyd.off = BLOCK_SIZE; + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, NULL, 0) => 0; + + // assert mdir was unininlined correctly + assert(lfsr_mtree_weight(&lfs) == 1); + // assert mroot now has no entries + assert(lfs.mroot.rbyd.weight == 0); + + // assert that our attr is still in the mroot + lfsr_mdir_get(&lfs, &lfs.mroot, -1, LFSR_TAG_UATTR(1), + buffer, SIZE) => SIZE; + assert(memcmp(buffer, &alphas[2 % 26], 1) == 0); + + // assert that our entry is still in the mtree + lfsr_mdir_t mdir; + lfsr_mtree_lookup(&lfs, 0, &mdir) => 0; + assert(mdir.rbyd.weight == 3); + + lfsr_mdir_get(&lfs, &mdir, 1, LFSR_TAG_INLINED, + buffer, SIZE) => SIZE; + assert(memcmp(buffer, &alphas[3 % 26], 1) == 0); + + // assert that our neighbors were updated correctly + assert(left_neighbor.rid == 0); + assert(left_neighbor.mdir.mid == 0); + assert(memcmp(&left_neighbor.mdir, &mdir, sizeof(lfsr_mdir_t)) == 0); + assert(right_neighbor.rid == 2); + assert(right_neighbor.mdir.mid == 0); + assert(memcmp(&right_neighbor.mdir, &mdir, sizeof(lfsr_mdir_t)) == 0); + + lfsr_mdir_removeopened(&lfs, &left_neighbor); + lfsr_mdir_removeopened(&lfs, &right_neighbor); + lfsr_unmount(&lfs) => 0; +''' + +[cases.test_mtree_neighbor_uninline_split] +# this should be set so only one entry can fit in a metadata block +defines.SIZE = 'BLOCK_SIZE / 4' +in = 'lfs.c' +code = ''' + const char *alphas = "abcdefghijklmnopqrstuvwxyz"; + lfs_t lfs; + lfsr_format(&lfs, cfg) => 0; + lfsr_mount(&lfs, cfg) => 0; + + // setup our neighbors + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(0, MKINLINED, +1, &alphas[0 % 26], 1), + LFSR_ATTR(1, MKINLINED, +1, &alphas[1 % 26], 1))) => 0; + // this test only works if these all fit in the mroot + assert(lfsr_mtree_isinlined(&lfs)); + + lfsr_openedmdir_t left_neighbor = {.rid=0, .mdir=lfs.mroot}; + lfsr_openedmdir_t right_neighbor = {.rid=1, .mdir=lfs.mroot}; + lfsr_mdir_addopened(&lfs, &left_neighbor); + lfsr_mdir_addopened(&lfs, &right_neighbor); + + // create 2 large entries that needs to be uninlined and split + uint8_t buffer[SIZE]; + memset(buffer, alphas[2 % 26], SIZE); + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(1, MKINLINED, +1, buffer, SIZE))) => 0; + + memset(buffer, alphas[3 % 26], SIZE); + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(2, MKINLINED, +1, buffer, SIZE))) => 0; + + // force mroot to compact + lfs.mroot.rbyd.off = BLOCK_SIZE; + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, NULL, 0) => 0; + + // assert mdirs were unininlined and split + assert(lfsr_mtree_weight(&lfs) == 2); + // assert mroot now has no entries + assert(lfs.mroot.rbyd.weight == 0); + + // assert that our entries are still in the mtree + lfsr_mdir_t mdir; + lfsr_mtree_lookup(&lfs, 0, &mdir) => 0; + assert(mdir.rbyd.weight == 2); + lfsr_mdir_get(&lfs, &mdir, 1, LFSR_TAG_INLINED, + buffer, SIZE) => SIZE; + assert(memcmp(buffer, &alphas[2 % 26], 1) == 0); + + lfsr_mdir_t msibling; + lfsr_mtree_lookup(&lfs, 1, &msibling) => 0; + assert(msibling.rbyd.weight == 2); + lfsr_mdir_get(&lfs, &msibling, 0, LFSR_TAG_INLINED, + buffer, SIZE) => SIZE; + assert(memcmp(buffer, &alphas[3 % 26], 1) == 0); + + // assert that our neighbors were updated correctly + assert(left_neighbor.rid == 0); + assert(left_neighbor.mdir.mid == 0); + assert(memcmp(&left_neighbor.mdir, &mdir, sizeof(lfsr_mdir_t)) == 0); + assert(right_neighbor.rid == 1); + assert(right_neighbor.mdir.mid == 1); + assert(memcmp(&right_neighbor.mdir, &msibling, sizeof(lfsr_mdir_t)) == 0); + + lfsr_mdir_removeopened(&lfs, &left_neighbor); + lfsr_mdir_removeopened(&lfs, &right_neighbor); + lfsr_unmount(&lfs) => 0; +''' + +[cases.test_mtree_neighbor_split] +# this should be set so only one entry can fit in a metadata block +defines.SIZE = 'BLOCK_SIZE / 4' +in = 'lfs.c' +code = ''' + const char *alphas = "abcdefghijklmnopqrstuvwxyz"; + lfs_t lfs; + lfsr_format(&lfs, cfg) => 0; + lfsr_mount(&lfs, cfg) => 0; + + // setup our neighbors + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(0, MKINLINED, +1, &alphas[0 % 26], 1), + LFSR_ATTR(1, MKINLINED, +1, &alphas[1 % 26], 1))) => 0; + // this test only works if these all fit in the mroot + assert(lfsr_mtree_isinlined(&lfs)); + + lfsr_openedmdir_t left_neighbor = {.rid=0, .mdir=lfs.mroot}; + lfsr_openedmdir_t right_neighbor = {.rid=1, .mdir=lfs.mroot}; + lfsr_mdir_addopened(&lfs, &left_neighbor); + lfsr_mdir_addopened(&lfs, &right_neighbor); + + // create an uninlined mdir + uint8_t buffer[SIZE]; + memset(buffer, alphas[2 % 26], SIZE); + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(-1, UATTR(1), 0, buffer, SIZE))) => 0; + + memset(buffer, alphas[3 % 26], SIZE); + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(1, MKINLINED, +1, buffer, SIZE))) => 0; + + // force mroot to compact + lfs.mroot.rbyd.off = BLOCK_SIZE; + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, NULL, 0) => 0; + + // assert mdir was unininlined correctly + assert(lfsr_mtree_weight(&lfs) == 1); + // assert mroot now has no entries + assert(lfs.mroot.rbyd.weight == 0); + + // now add another large entry to the mdir, forcing a split + lfsr_mdir_t mdir; + lfsr_mtree_lookup(&lfs, 0, &mdir) => 0; + assert(mdir.rbyd.weight == 3); + + memset(buffer, alphas[4 % 26], SIZE); + lfsr_mdir_commit(&lfs, &mdir, &(lfs_ssize_t){2}, LFSR_ATTRS( + LFSR_ATTR(2, MKINLINED, +1, buffer, SIZE))) => 0; + + // force mdir to compact + mdir.rbyd.off = BLOCK_SIZE; + lfsr_mdir_commit(&lfs, &mdir, &(lfs_ssize_t){2}, NULL, 0) => 0; + + // assert mdir was split correctly + assert(lfsr_mtree_weight(&lfs) == 2); + // assert mroot still has no entries + assert(lfs.mroot.rbyd.weight == 0); + + // assert that our attr is still in the mroot + lfsr_mdir_get(&lfs, &lfs.mroot, -1, LFSR_TAG_UATTR(1), + buffer, SIZE) => SIZE; + assert(memcmp(buffer, &alphas[2 % 26], 1) == 0); + + // assert that our entries are still in the mtree + lfsr_mtree_lookup(&lfs, 0, &mdir) => 0; + assert(mdir.rbyd.weight == 2); + lfsr_mdir_get(&lfs, &mdir, 1, LFSR_TAG_INLINED, + buffer, SIZE) => SIZE; + assert(memcmp(buffer, &alphas[3 % 26], 1) == 0); + + lfsr_mdir_t msibling; + lfsr_mtree_lookup(&lfs, 1, &msibling) => 0; + assert(msibling.rbyd.weight == 2); + lfsr_mdir_get(&lfs, &msibling, 0, LFSR_TAG_INLINED, + buffer, SIZE) => SIZE; + assert(memcmp(buffer, &alphas[4 % 26], 1) == 0); + + // assert that our neighbors were updated correctly + assert(left_neighbor.rid == 0); + assert(left_neighbor.mdir.mid == 0); + assert(memcmp(&left_neighbor.mdir, &mdir, sizeof(lfsr_mdir_t)) == 0); + assert(right_neighbor.rid == 1); + assert(right_neighbor.mdir.mid == 1); + assert(memcmp(&right_neighbor.mdir, &msibling, sizeof(lfsr_mdir_t)) == 0); + + lfsr_mdir_removeopened(&lfs, &left_neighbor); + lfsr_mdir_removeopened(&lfs, &right_neighbor); + lfsr_unmount(&lfs) => 0; +''' + +[cases.test_mtree_neighbor_relocate] +# this should be set so only one entry can fit in a metadata block +defines.SIZE = 'BLOCK_SIZE / 4' +# make it so blocks relocate every two compacts +defines.BLOCK_CYCLES = 2 +in = 'lfs.c' +code = ''' + const char *alphas = "abcdefghijklmnopqrstuvwxyz"; + lfs_t lfs; + lfsr_format(&lfs, cfg) => 0; + lfsr_mount(&lfs, cfg) => 0; + + // setup our neighbors + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(0, MKINLINED, +1, &alphas[0 % 26], 1), + LFSR_ATTR(1, MKINLINED, +1, &alphas[1 % 26], 1))) => 0; + // this test only works if these all fit in the mroot + assert(lfsr_mtree_isinlined(&lfs)); + + lfsr_openedmdir_t left_neighbor = {.rid=0, .mdir=lfs.mroot}; + lfsr_openedmdir_t right_neighbor = {.rid=1, .mdir=lfs.mroot}; + lfsr_mdir_addopened(&lfs, &left_neighbor); + lfsr_mdir_addopened(&lfs, &right_neighbor); + + // prepare mroot with a large attr so the next entry can not fit + uint8_t buffer[SIZE]; + memset(buffer, alphas[2 % 26], SIZE); + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(-1, UATTR(1), 0, buffer, SIZE))) => 0; + + // create a large entry that needs to be uninlined (but not split!) + memset(buffer, alphas[3 % 26], SIZE); + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, LFSR_ATTRS( + LFSR_ATTR(1, MKINLINED, +1, buffer, SIZE))) => 0; + + // force mroot to compact + lfs.mroot.rbyd.off = BLOCK_SIZE; + lfsr_mdir_commit(&lfs, &lfs.mroot, &(lfs_ssize_t){-1}, NULL, 0) => 0; + + // assert mtree has one mdir + assert(lfsr_mtree_weight(&lfs) == 1); + // assert mroot now has no entries + assert(lfs.mroot.rbyd.weight == 0); + + // force mdir to compact twice, this should relocate + lfsr_mdir_t mdir; + lfsr_mtree_lookup(&lfs, 0, &mdir) => 0; + assert(mdir.rbyd.weight == 3); + + lfsr_mdir_t old_mdir = mdir; + + mdir.rbyd.off = BLOCK_SIZE; + lfsr_mdir_commit(&lfs, &mdir, &(lfs_ssize_t){0}, NULL, 0) => 0; + mdir.rbyd.off = BLOCK_SIZE; + memset(buffer, alphas[4 % 26], SIZE); + lfsr_mdir_commit(&lfs, &mdir, &(lfs_ssize_t){0}, LFSR_ATTRS( + LFSR_ATTR(1, INLINED, 0, buffer, SIZE))) => 0; + + // assert we relocated + assert(!lfsr_mdir_eq(&old_mdir, &mdir)); + + // assert that our attr is still in the mroot + lfsr_mdir_get(&lfs, &lfs.mroot, -1, LFSR_TAG_UATTR(1), + buffer, SIZE) => SIZE; + assert(memcmp(buffer, &alphas[2 % 26], 1) == 0); + + // assert that our entry is still in the mtree + lfsr_mtree_lookup(&lfs, 0, &mdir) => 0; + assert(mdir.rbyd.weight == 3); + + lfsr_mdir_get(&lfs, &mdir, 1, LFSR_TAG_INLINED, + buffer, SIZE) => SIZE; + assert(memcmp(buffer, &alphas[4 % 26], 1) == 0); + + // assert that our neighbors were updated correctly + assert(left_neighbor.rid == 0); + assert(left_neighbor.mdir.mid == 0); + assert(memcmp(&left_neighbor.mdir, &mdir, sizeof(lfsr_mdir_t)) == 0); + assert(right_neighbor.rid == 2); + assert(right_neighbor.mdir.mid == 0); + assert(memcmp(&right_neighbor.mdir, &mdir, sizeof(lfsr_mdir_t)) == 0); + + lfsr_mdir_removeopened(&lfs, &left_neighbor); + lfsr_mdir_removeopened(&lfs, &right_neighbor); + lfsr_unmount(&lfs) => 0; +'''