Преглед на файлове

Merge pull request #1103 from littlefs-project/devel

Minor release: v2.11
Christopher Haster преди 7 месеца
родител
ревизия
16ceb67934
променени са 14 файла, в които са добавени 571 реда и са изтрити 51 реда
  1. 19 8
      .github/workflows/test.yml
  2. 10 1
      Makefile
  3. 54 26
      lfs.c
  4. 6 2
      lfs.h
  5. 10 10
      lfs_util.h
  6. 5 0
      runners/bench_runner.c
  7. 5 0
      runners/test_runner.c
  8. 4 1
      scripts/bench.py
  9. 1 1
      scripts/changeprefix.py
  10. 4 1
      scripts/test.py
  11. 68 1
      tests/test_orphans.toml
  12. 168 0
      tests/test_relocations.toml
  13. 109 0
      tests/test_shrink.toml
  14. 108 0
      tests/test_superblocks.toml

+ 19 - 8
.github/workflows/test.yml

@@ -374,6 +374,22 @@ jobs:
         run: |
           CFLAGS="$CFLAGS -DLFS_NO_INTRINSICS" make test
 
+  test-shrink:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: install
+        run: |
+          # need a few things
+          sudo apt-get update -qq
+          sudo apt-get install -qq gcc python3 python3-pip
+          pip3 install toml
+          gcc --version
+          python3 --version
+      - name: test-no-intrinsics
+        run: |
+          CFLAGS="$CFLAGS -DLFS_SHRINKNONRELOCATING" make test
+
   # run with all trace options enabled to at least make sure these
   # all compile
   test-yes-trace:
@@ -454,8 +470,7 @@ jobs:
           TESTFLAGS="$TESTFLAGS --valgrind --context=1024 -Gdefault -Pnone" \
             make test
 
-  # test that compilation is warning free under clang
-  # run with Clang, mostly to check for Clang-specific warnings
+  # compile/run with Clang, mostly to check for Clang-specific warnings
   test-clang:
     runs-on: ubuntu-latest
     steps:
@@ -469,12 +484,8 @@ jobs:
           python3 --version
       - name: test-clang
         run: |
-          # override CFLAGS since Clang does not support -fcallgraph-info
-          # and -ftrack-macro-expansions
-          make \
-            CC=clang \
-            CFLAGS="$CFLAGS -MMD -g3 -I. -std=c99 -Wall -Wextra -pedantic" \
-            test
+          CC=clang \
+            make test
 
   # run benchmarks
   #

+ 10 - 1
Makefile

@@ -18,6 +18,12 @@ VALGRIND ?= valgrind
 GDB		 ?= gdb
 PERF	 ?= perf
 
+# guess clang or gcc (clang sometimes masquerades as gcc because of
+# course it does)
+ifneq ($(shell $(CC) --version | grep clang),)
+NO_GCC = 1
+endif
+
 SRC  ?= $(filter-out $(wildcard *.t.* *.b.*),$(wildcard *.c))
 OBJ  := $(SRC:%.c=$(BUILDDIR)/%.o)
 DEP  := $(SRC:%.c=$(BUILDDIR)/%.d)
@@ -59,12 +65,15 @@ BENCH_PERF  := $(BENCH_RUNNER:%=%.perf)
 BENCH_TRACE := $(BENCH_RUNNER:%=%.trace)
 BENCH_CSV   := $(BENCH_RUNNER:%=%.csv)
 
-CFLAGS += -fcallgraph-info=su
 CFLAGS += -g3
 CFLAGS += -I.
 CFLAGS += -std=c99 -Wall -Wextra -pedantic
 CFLAGS += -Wmissing-prototypes
+ifndef NO_GCC
+CFLAGS += -fcallgraph-info=su
 CFLAGS += -ftrack-macro-expansion=0
+endif
+
 ifdef DEBUG
 CFLAGS += -O0
 else

+ 54 - 26
lfs.c

@@ -3932,7 +3932,9 @@ static int lfs_remove_(lfs_t *lfs, const char *path) {
     }
 
     lfs->mlist = dir.next;
-    if (lfs_tag_type3(tag) == LFS_TYPE_DIR) {
+    if (lfs_gstate_hasorphans(&lfs->gstate)) {
+        LFS_ASSERT(lfs_tag_type3(tag) == LFS_TYPE_DIR);
+
         // fix orphan
         err = lfs_fs_preporphans(lfs, -1);
         if (err) {
@@ -4076,8 +4078,10 @@ static int lfs_rename_(lfs_t *lfs, const char *oldpath, const char *newpath) {
     }
 
     lfs->mlist = prevdir.next;
-    if (prevtag != LFS_ERR_NOENT
-            && lfs_tag_type3(prevtag) == LFS_TYPE_DIR) {
+    if (lfs_gstate_hasorphans(&lfs->gstate)) {
+        LFS_ASSERT(prevtag != LFS_ERR_NOENT
+                && lfs_tag_type3(prevtag) == LFS_TYPE_DIR);
+
         // fix orphan
         err = lfs_fs_preporphans(lfs, -1);
         if (err) {
@@ -5233,40 +5237,64 @@ static int lfs_fs_gc_(lfs_t *lfs) {
 #endif
 
 #ifndef LFS_READONLY
+#ifdef LFS_SHRINKNONRELOCATING
+static int lfs_shrink_checkblock(void *data, lfs_block_t block) {
+    lfs_size_t threshold = *((lfs_size_t*)data);
+    if (block >= threshold) {
+        return LFS_ERR_NOTEMPTY;
+    }
+    return 0;
+}
+#endif
+
 static int lfs_fs_grow_(lfs_t *lfs, lfs_size_t block_count) {
-    // shrinking is not supported
-    LFS_ASSERT(block_count >= lfs->block_count);
+    int err;
 
-    if (block_count > lfs->block_count) {
-        lfs->block_count = block_count;
+    if (block_count == lfs->block_count) {
+        return 0;
+    }
 
-        // fetch the root
-        lfs_mdir_t root;
-        int err = lfs_dir_fetch(lfs, &root, lfs->root);
+    
+#ifndef LFS_SHRINKNONRELOCATING
+    // shrinking is not supported
+    LFS_ASSERT(block_count >= lfs->block_count);
+#endif
+#ifdef LFS_SHRINKNONRELOCATING
+    if (block_count < lfs->block_count) {
+        err = lfs_fs_traverse_(lfs, lfs_shrink_checkblock, &block_count, true);
         if (err) {
             return err;
         }
+    }
+#endif
 
-        // update the superblock
-        lfs_superblock_t superblock;
-        lfs_stag_t tag = lfs_dir_get(lfs, &root, LFS_MKTAG(0x7ff, 0x3ff, 0),
-                LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)),
-                &superblock);
-        if (tag < 0) {
-            return tag;
-        }
-        lfs_superblock_fromle32(&superblock);
+    lfs->block_count = block_count;
 
-        superblock.block_count = lfs->block_count;
+    // fetch the root
+    lfs_mdir_t root;
+    err = lfs_dir_fetch(lfs, &root, lfs->root);
+    if (err) {
+        return err;
+    }
 
-        lfs_superblock_tole32(&superblock);
-        err = lfs_dir_commit(lfs, &root, LFS_MKATTRS(
-                {tag, &superblock}));
-        if (err) {
-            return err;
-        }
+    // update the superblock
+    lfs_superblock_t superblock;
+    lfs_stag_t tag = lfs_dir_get(lfs, &root, LFS_MKTAG(0x7ff, 0x3ff, 0),
+            LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)),
+            &superblock);
+    if (tag < 0) {
+        return tag;
     }
+    lfs_superblock_fromle32(&superblock);
+
+    superblock.block_count = lfs->block_count;
 
+    lfs_superblock_tole32(&superblock);
+    err = lfs_dir_commit(lfs, &root, LFS_MKATTRS(
+            {tag, &superblock}));
+    if (err) {
+        return err;
+    }
     return 0;
 }
 #endif

+ 6 - 2
lfs.h

@@ -21,7 +21,7 @@ extern "C"
 // Software library version
 // Major (top-nibble), incremented on backwards incompatible changes
 // Minor (bottom-nibble), incremented on feature additions
-#define LFS_VERSION 0x0002000a
+#define LFS_VERSION 0x0002000b
 #define LFS_VERSION_MAJOR (0xffff & (LFS_VERSION >> 16))
 #define LFS_VERSION_MINOR (0xffff & (LFS_VERSION >>  0))
 
@@ -766,7 +766,11 @@ int lfs_fs_gc(lfs_t *lfs);
 // Grows the filesystem to a new size, updating the superblock with the new
 // block count.
 //
-// Note: This is irreversible.
+// If LFS_SHRINKNONRELOCATING is defined, this function will also accept
+// block_counts smaller than the current configuration, after checking
+// that none of the blocks that are being removed are in use.
+// Note that littlefs's pseudorandom block allocation means that
+// this is very unlikely to work in the general case.
 //
 // Returns a negative error code on failure.
 int lfs_fs_grow(lfs_t *lfs, lfs_size_t block_count);

+ 10 - 10
lfs_util.h

@@ -195,10 +195,10 @@ static inline uint32_t lfs_fromle32(uint32_t a) {
     (defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__))
     return __builtin_bswap32(a);
 #else
-    return (((uint8_t*)&a)[0] <<  0) |
-           (((uint8_t*)&a)[1] <<  8) |
-           (((uint8_t*)&a)[2] << 16) |
-           (((uint8_t*)&a)[3] << 24);
+    return ((uint32_t)((uint8_t*)&a)[0] <<  0) |
+           ((uint32_t)((uint8_t*)&a)[1] <<  8) |
+           ((uint32_t)((uint8_t*)&a)[2] << 16) |
+           ((uint32_t)((uint8_t*)&a)[3] << 24);
 #endif
 }
 
@@ -218,10 +218,10 @@ static inline uint32_t lfs_frombe32(uint32_t a) {
     (defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
     return a;
 #else
-    return (((uint8_t*)&a)[0] << 24) |
-           (((uint8_t*)&a)[1] << 16) |
-           (((uint8_t*)&a)[2] <<  8) |
-           (((uint8_t*)&a)[3] <<  0);
+    return ((uint32_t)((uint8_t*)&a)[0] << 24) |
+           ((uint32_t)((uint8_t*)&a)[1] << 16) |
+           ((uint32_t)((uint8_t*)&a)[2] <<  8) |
+           ((uint32_t)((uint8_t*)&a)[3] <<  0);
 #endif
 }
 
@@ -231,8 +231,8 @@ static inline uint32_t lfs_tobe32(uint32_t a) {
 
 // Calculate CRC-32 with polynomial = 0x04c11db7
 #ifdef LFS_CRC
-uint32_t lfs_crc(uint32_t crc, const void *buffer, size_t size) {
-    return LFS_CRC(crc, buffer, size)
+static inline uint32_t lfs_crc(uint32_t crc, const void *buffer, size_t size) {
+    return LFS_CRC(crc, buffer, size);
 }
 #else
 uint32_t lfs_crc(uint32_t crc, const void *buffer, size_t size);

+ 5 - 0
runners/bench_runner.c

@@ -123,8 +123,13 @@ typedef struct bench_id {
 
 
 // bench suites are linked into a custom ld section
+#if defined(__APPLE__)
+extern struct bench_suite __start__bench_suites __asm("section$start$__DATA$_bench_suites");
+extern struct bench_suite __stop__bench_suites __asm("section$end$__DATA$_bench_suites");
+#else
 extern struct bench_suite __start__bench_suites;
 extern struct bench_suite __stop__bench_suites;
+#endif
 
 const struct bench_suite *bench_suites = &__start__bench_suites;
 #define BENCH_SUITE_COUNT \

+ 5 - 0
runners/test_runner.c

@@ -136,8 +136,13 @@ typedef struct test_id {
 
 
 // test suites are linked into a custom ld section
+#if defined(__APPLE__)
+extern struct test_suite __start__test_suites __asm("section$start$__DATA$_test_suites");
+extern struct test_suite __stop__test_suites __asm("section$end$__DATA$_test_suites");
+#else
 extern struct test_suite __start__test_suites;
 extern struct test_suite __stop__test_suites;
+#endif
 
 const struct test_suite *test_suites = &__start__test_suites;
 #define TEST_SUITE_COUNT \

+ 4 - 1
scripts/bench.py

@@ -404,12 +404,15 @@ def compile(bench_paths, **args):
                         f.writeln()
 
                 # create suite struct
-                #
+                f.writeln('#if defined(__APPLE__)')
+                f.writeln('__attribute__((section("__DATA,_bench_suites")))')
+                f.writeln('#else')
                 # note we place this in the custom bench_suites section with
                 # minimum alignment, otherwise GCC ups the alignment to
                 # 32-bytes for some reason
                 f.writeln('__attribute__((section("_bench_suites"), '
                     'aligned(1)))')
+                f.writeln('#endif')
                 f.writeln('const struct bench_suite __bench__%s__suite = {'
                     % suite.name)
                 f.writeln(4*' '+'.name = "%s",' % suite.name)

+ 1 - 1
scripts/changeprefix.py

@@ -73,7 +73,7 @@ def changefile(from_prefix, to_prefix, from_path, to_path, *,
         shutil.copystat(from_path, to_path)
 
     if to_path_temp:
-        os.rename(to_path, from_path)
+        shutil.move(to_path, from_path)
     elif from_path != '-':
         os.remove(from_path)
 

+ 4 - 1
scripts/test.py

@@ -412,12 +412,15 @@ def compile(test_paths, **args):
                         f.writeln()
 
                 # create suite struct
-                #
+                f.writeln('#if defined(__APPLE__)')
+                f.writeln('__attribute__((section("__DATA,_test_suites")))')
+                f.writeln('#else')
                 # note we place this in the custom test_suites section with
                 # minimum alignment, otherwise GCC ups the alignment to
                 # 32-bytes for some reason
                 f.writeln('__attribute__((section("_test_suites"), '
                     'aligned(1)))')
+                f.writeln('#endif')
                 f.writeln('const struct test_suite __test__%s__suite = {'
                     % suite.name)
                 f.writeln(4*' '+'.name = "%s",' % suite.name)

+ 68 - 1
tests/test_orphans.toml

@@ -207,7 +207,8 @@ code = '''
 [cases.test_orphans_reentrant]
 reentrant = true
 # TODO fix this case, caused by non-DAG trees
-if = '!(DEPTH == 3 && CACHE_SIZE != 64)'
+# NOTE the second condition is required
+if = '!(DEPTH == 3 && CACHE_SIZE != 64) && 2*FILES < BLOCK_COUNT'
 defines = [
     {FILES=6,  DEPTH=1, CYCLES=20},
     {FILES=26, DEPTH=1, CYCLES=20},
@@ -271,3 +272,69 @@ code = '''
     lfs_unmount(&lfs) => 0;
 '''
 
+# non-reentrant testing for orphans, this is the same as reentrant
+# testing, but we test way more states than we could under powerloss
+[cases.test_orphans_nonreentrant]
+# TODO fix this case, caused by non-DAG trees
+# NOTE the second condition is required
+if = '!(DEPTH == 3 && CACHE_SIZE != 64) && 2*FILES < BLOCK_COUNT'
+defines = [
+    {FILES=6,   DEPTH=1, CYCLES=2000},
+    {FILES=26,  DEPTH=1, CYCLES=2000},
+    {FILES=3,   DEPTH=3, CYCLES=2000},
+]
+code = '''
+    lfs_t lfs;
+    lfs_format(&lfs, cfg) => 0;
+    lfs_mount(&lfs, cfg) => 0;
+
+    uint32_t prng = 1;
+    const char alpha[] = "abcdefghijklmnopqrstuvwxyz";
+    for (unsigned i = 0; i < CYCLES; i++) {
+        // create random path
+        char full_path[256];
+        for (unsigned d = 0; d < DEPTH; d++) {
+            sprintf(&full_path[2*d], "/%c", alpha[TEST_PRNG(&prng) % FILES]);
+        }
+
+        // if it does not exist, we create it, else we destroy
+        struct lfs_info info;
+        int res = lfs_stat(&lfs, full_path, &info);
+        if (res == LFS_ERR_NOENT) {
+            // create each directory in turn, ignore if dir already exists
+            for (unsigned d = 0; d < DEPTH; d++) {
+                char path[1024];
+                strcpy(path, full_path);
+                path[2*d+2] = '\0';
+                int err = lfs_mkdir(&lfs, path);
+                assert(!err || err == LFS_ERR_EXIST);
+            }
+
+            for (unsigned d = 0; d < DEPTH; d++) {
+                char path[1024];
+                strcpy(path, full_path);
+                path[2*d+2] = '\0';
+                lfs_stat(&lfs, path, &info) => 0;
+                assert(strcmp(info.name, &path[2*d+1]) == 0);
+                assert(info.type == LFS_TYPE_DIR);
+            }
+        } else {
+            // is valid dir?
+            assert(strcmp(info.name, &full_path[2*(DEPTH-1)+1]) == 0);
+            assert(info.type == LFS_TYPE_DIR);
+
+            // try to delete path in reverse order, ignore if dir is not empty
+            for (int d = DEPTH-1; d >= 0; d--) {
+                char path[1024];
+                strcpy(path, full_path);
+                path[2*d+2] = '\0';
+                int err = lfs_remove(&lfs, path);
+                assert(!err || err == LFS_ERR_NOTEMPTY);
+            }
+
+            lfs_stat(&lfs, full_path, &info) => LFS_ERR_NOENT;
+        }
+    }
+    lfs_unmount(&lfs) => 0;
+'''
+

+ 168 - 0
tests/test_relocations.toml

@@ -341,3 +341,171 @@ code = '''
     }
     lfs_unmount(&lfs) => 0;
 '''
+
+# non-reentrant testing for orphans, this is the same as reentrant
+# testing, but we test way more states than we could under powerloss
+[cases.test_relocations_nonreentrant]
+# TODO fix this case, caused by non-DAG trees
+# NOTE the second condition is required
+if = '!(DEPTH == 3 && CACHE_SIZE != 64) && 2*FILES < BLOCK_COUNT'
+defines = [
+    {FILES=6,  DEPTH=1, CYCLES=2000, BLOCK_CYCLES=1},
+    {FILES=26, DEPTH=1, CYCLES=2000, BLOCK_CYCLES=1},
+    {FILES=3,  DEPTH=3, CYCLES=2000, BLOCK_CYCLES=1},
+]
+code = '''
+    lfs_t lfs;
+    lfs_format(&lfs, cfg) => 0;
+    lfs_mount(&lfs, cfg) => 0;
+
+    uint32_t prng = 1;
+    const char alpha[] = "abcdefghijklmnopqrstuvwxyz";
+    for (unsigned i = 0; i < CYCLES; i++) {
+        // create random path
+        char full_path[256];
+        for (unsigned d = 0; d < DEPTH; d++) {
+            sprintf(&full_path[2*d], "/%c", alpha[TEST_PRNG(&prng) % FILES]);
+        }
+
+        // if it does not exist, we create it, else we destroy
+        struct lfs_info info;
+        int res = lfs_stat(&lfs, full_path, &info);
+        if (res == LFS_ERR_NOENT) {
+            // create each directory in turn, ignore if dir already exists
+            for (unsigned d = 0; d < DEPTH; d++) {
+                char path[1024];
+                strcpy(path, full_path);
+                path[2*d+2] = '\0';
+                int err = lfs_mkdir(&lfs, path);
+                assert(!err || err == LFS_ERR_EXIST);
+            }
+
+            for (unsigned d = 0; d < DEPTH; d++) {
+                char path[1024];
+                strcpy(path, full_path);
+                path[2*d+2] = '\0';
+                lfs_stat(&lfs, path, &info) => 0;
+                assert(strcmp(info.name, &path[2*d+1]) == 0);
+                assert(info.type == LFS_TYPE_DIR);
+            }
+        } else {
+            // is valid dir?
+            assert(strcmp(info.name, &full_path[2*(DEPTH-1)+1]) == 0);
+            assert(info.type == LFS_TYPE_DIR);
+
+            // try to delete path in reverse order, ignore if dir is not empty
+            for (unsigned d = DEPTH-1; d+1 > 0; d--) {
+                char path[1024];
+                strcpy(path, full_path);
+                path[2*d+2] = '\0';
+                int err = lfs_remove(&lfs, path);
+                assert(!err || err == LFS_ERR_NOTEMPTY);
+            }
+
+            lfs_stat(&lfs, full_path, &info) => LFS_ERR_NOENT;
+        }
+    }
+    lfs_unmount(&lfs) => 0;
+'''
+
+# non-reentrant testing for relocations, but now with random renames!
+[cases.test_relocations_nonreentrant_renames]
+# TODO fix this case, caused by non-DAG trees
+# NOTE the second condition is required
+if = '!(DEPTH == 3 && CACHE_SIZE != 64) && 2*FILES < BLOCK_COUNT'
+defines = [
+    {FILES=6,  DEPTH=1, CYCLES=2000, BLOCK_CYCLES=1},
+    {FILES=26, DEPTH=1, CYCLES=2000, BLOCK_CYCLES=1},
+    {FILES=3,  DEPTH=3, CYCLES=2000, BLOCK_CYCLES=1},
+]
+code = '''
+    lfs_t lfs;
+    lfs_format(&lfs, cfg) => 0;
+    lfs_mount(&lfs, cfg) => 0;
+
+    uint32_t prng = 1;
+    const char alpha[] = "abcdefghijklmnopqrstuvwxyz";
+    for (unsigned i = 0; i < CYCLES; i++) {
+        // create random path
+        char full_path[256];
+        for (unsigned d = 0; d < DEPTH; d++) {
+            sprintf(&full_path[2*d], "/%c", alpha[TEST_PRNG(&prng) % FILES]);
+        }
+
+        // if it does not exist, we create it, else we destroy
+        struct lfs_info info;
+        int res = lfs_stat(&lfs, full_path, &info);
+        assert(!res || res == LFS_ERR_NOENT);
+        if (res == LFS_ERR_NOENT) {
+            // create each directory in turn, ignore if dir already exists
+            for (unsigned d = 0; d < DEPTH; d++) {
+                char path[1024];
+                strcpy(path, full_path);
+                path[2*d+2] = '\0';
+                int err = lfs_mkdir(&lfs, path);
+                assert(!err || err == LFS_ERR_EXIST);
+            }
+
+            for (unsigned d = 0; d < DEPTH; d++) {
+                char path[1024];
+                strcpy(path, full_path);
+                path[2*d+2] = '\0';
+                lfs_stat(&lfs, path, &info) => 0;
+                assert(strcmp(info.name, &path[2*d+1]) == 0);
+                assert(info.type == LFS_TYPE_DIR);
+            }
+        } else {
+            assert(strcmp(info.name, &full_path[2*(DEPTH-1)+1]) == 0);
+            assert(info.type == LFS_TYPE_DIR);
+
+            // create new random path
+            char new_path[256];
+            for (unsigned d = 0; d < DEPTH; d++) {
+                sprintf(&new_path[2*d], "/%c", alpha[TEST_PRNG(&prng) % FILES]);
+            }
+
+            // if new path does not exist, rename, otherwise destroy
+            res = lfs_stat(&lfs, new_path, &info);
+            assert(!res || res == LFS_ERR_NOENT);
+            if (res == LFS_ERR_NOENT) {
+                // stop once some dir is renamed
+                for (unsigned d = 0; d < DEPTH; d++) {
+                    char path[1024];
+                    strcpy(&path[2*d], &full_path[2*d]);
+                    path[2*d+2] = '\0';
+                    strcpy(&path[128+2*d], &new_path[2*d]);
+                    path[128+2*d+2] = '\0';
+                    int err = lfs_rename(&lfs, path, path+128);
+                    assert(!err || err == LFS_ERR_NOTEMPTY);
+                    if (!err) {
+                        strcpy(path, path+128);
+                    }
+                }
+
+                for (unsigned d = 0; d < DEPTH; d++) {
+                    char path[1024];
+                    strcpy(path, new_path);
+                    path[2*d+2] = '\0';
+                    lfs_stat(&lfs, path, &info) => 0;
+                    assert(strcmp(info.name, &path[2*d+1]) == 0);
+                    assert(info.type == LFS_TYPE_DIR);
+                }
+
+                lfs_stat(&lfs, full_path, &info) => LFS_ERR_NOENT;
+            } else {
+                // try to delete path in reverse order,
+                // ignore if dir is not empty
+                for (unsigned d = DEPTH-1; d+1 > 0; d--) {
+                    char path[1024];
+                    strcpy(path, full_path);
+                    path[2*d+2] = '\0';
+                    int err = lfs_remove(&lfs, path);
+                    assert(!err || err == LFS_ERR_NOTEMPTY);
+                }
+
+                lfs_stat(&lfs, full_path, &info) => LFS_ERR_NOENT;
+            }
+        }
+    }
+    lfs_unmount(&lfs) => 0;
+'''

+ 109 - 0
tests/test_shrink.toml

@@ -0,0 +1,109 @@
+# simple shrink
+[cases.test_shrink_simple]
+defines.BLOCK_COUNT = [10, 15, 20]
+defines.AFTER_BLOCK_COUNT = [5, 10, 15, 19]
+   
+if = "AFTER_BLOCK_COUNT <= BLOCK_COUNT"
+code = '''
+#ifdef LFS_SHRINKNONRELOCATING
+    lfs_t lfs;
+    lfs_format(&lfs, cfg) => 0;
+    lfs_mount(&lfs, cfg) => 0;
+    lfs_fs_grow(&lfs, AFTER_BLOCK_COUNT) => 0;
+    lfs_unmount(&lfs);
+    if (BLOCK_COUNT != AFTER_BLOCK_COUNT) {
+        lfs_mount(&lfs, cfg) => LFS_ERR_INVAL;
+    }
+    lfs_t lfs2 = lfs;
+    struct lfs_config cfg2 = *cfg;
+    cfg2.block_count = AFTER_BLOCK_COUNT;
+    lfs2.cfg = &cfg2;
+    lfs_mount(&lfs2, &cfg2) => 0;
+    lfs_unmount(&lfs2) => 0;
+#endif
+'''
+
+# shrinking full
+[cases.test_shrink_full]
+defines.BLOCK_COUNT = [10, 15, 20]
+defines.AFTER_BLOCK_COUNT = [5, 7, 10, 12, 15, 17, 20]
+defines.FILES_COUNT = [7, 8, 9, 10]
+if = "AFTER_BLOCK_COUNT <= BLOCK_COUNT && FILES_COUNT + 2 < BLOCK_COUNT"
+code = '''
+#ifdef LFS_SHRINKNONRELOCATING
+    lfs_t lfs;
+    lfs_format(&lfs, cfg) => 0;
+    // create FILES_COUNT files of BLOCK_SIZE - 50 bytes (to avoid inlining)
+    lfs_mount(&lfs, cfg) => 0;
+    for (int i = 0; i < FILES_COUNT + 1; i++) {
+        lfs_file_t file;
+        char path[1024];
+        sprintf(path, "file_%03d", i);
+        lfs_file_open(&lfs, &file, path,
+                LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0;
+        char wbuffer[BLOCK_SIZE];
+        memset(wbuffer, 'b', BLOCK_SIZE);
+        // Ensure one block is taken per file, but that files are not inlined.
+        lfs_size_t size = BLOCK_SIZE - 0x40;
+        sprintf(wbuffer, "Hi %03d", i);
+        lfs_file_write(&lfs, &file, wbuffer, size) => size;
+        lfs_file_close(&lfs, &file) => 0;
+    }
+
+    int err = lfs_fs_grow(&lfs, AFTER_BLOCK_COUNT);
+    if (err == 0) {
+        for (int i = 0; i < FILES_COUNT + 1; i++) {
+            lfs_file_t file;
+            char path[1024];
+            sprintf(path, "file_%03d", i);
+            lfs_file_open(&lfs, &file, path,
+                    LFS_O_RDONLY ) => 0;
+            lfs_size_t size = BLOCK_SIZE - 0x40;
+            char wbuffer[size];
+            char wbuffer_ref[size];
+            // Ensure one block is taken per file, but that files are not inlined.
+            memset(wbuffer_ref, 'b', size);
+            sprintf(wbuffer_ref, "Hi %03d", i);
+            lfs_file_read(&lfs, &file, wbuffer, BLOCK_SIZE) => size;
+            lfs_file_close(&lfs, &file) => 0;
+            for (lfs_size_t j = 0; j < size; j++) {
+                wbuffer[j] => wbuffer_ref[j];
+            }
+        }
+    } else {
+        assert(err == LFS_ERR_NOTEMPTY);
+    }
+
+    lfs_unmount(&lfs) => 0;
+    if (err == 0 ) {
+        if ( AFTER_BLOCK_COUNT != BLOCK_COUNT ) {
+            lfs_mount(&lfs, cfg) => LFS_ERR_INVAL;
+        }
+
+        lfs_t lfs2 = lfs;
+        struct lfs_config cfg2 = *cfg;
+        cfg2.block_count = AFTER_BLOCK_COUNT;
+        lfs2.cfg = &cfg2;
+        lfs_mount(&lfs2, &cfg2) => 0;
+        for (int i = 0; i < FILES_COUNT + 1; i++) {
+            lfs_file_t file;
+            char path[1024];
+            sprintf(path, "file_%03d", i);
+            lfs_file_open(&lfs2, &file, path,
+                    LFS_O_RDONLY ) => 0;
+            lfs_size_t size = BLOCK_SIZE - 0x40;
+            char wbuffer[size];
+            char wbuffer_ref[size];
+            // Ensure one block is taken per file, but that files are not inlined.
+            memset(wbuffer_ref, 'b', size);
+            sprintf(wbuffer_ref, "Hi %03d", i);
+            lfs_file_read(&lfs2, &file, wbuffer, BLOCK_SIZE) => size;
+            lfs_file_close(&lfs2, &file) => 0;
+            for (lfs_size_t j = 0; j < size; j++) {
+                wbuffer[j] => wbuffer_ref[j];
+            }
+        }
+        lfs_unmount(&lfs2);
+    }
+#endif
+'''

+ 108 - 0
tests/test_superblocks.toml

@@ -524,6 +524,114 @@ code = '''
     lfs_unmount(&lfs) => 0;
 '''
 
+
+# mount and grow the filesystem
+[cases.test_superblocks_shrink]
+defines.BLOCK_COUNT = 'ERASE_COUNT'
+defines.BLOCK_COUNT_2 = ['ERASE_COUNT/2', 'ERASE_COUNT/4', '2']
+defines.KNOWN_BLOCK_COUNT = [true, false]
+code = '''
+#ifdef LFS_SHRINKNONRELOCATING
+    lfs_t lfs;
+    lfs_format(&lfs, cfg) => 0;
+
+    if (KNOWN_BLOCK_COUNT) {
+        cfg->block_count = BLOCK_COUNT;
+    } else {
+        cfg->block_count = 0;
+    }
+
+    // mount with block_size < erase_size
+    lfs_mount(&lfs, cfg) => 0;
+    struct lfs_fsinfo fsinfo;
+    lfs_fs_stat(&lfs, &fsinfo) => 0;
+    assert(fsinfo.block_size == BLOCK_SIZE);
+    assert(fsinfo.block_count == BLOCK_COUNT);
+    lfs_unmount(&lfs) => 0;
+
+    // same size is a noop
+    lfs_mount(&lfs, cfg) => 0;
+    lfs_fs_grow(&lfs, BLOCK_COUNT) => 0;
+    lfs_fs_stat(&lfs, &fsinfo) => 0;
+    assert(fsinfo.block_size == BLOCK_SIZE);
+    assert(fsinfo.block_count == BLOCK_COUNT);
+    lfs_unmount(&lfs) => 0;
+
+    lfs_mount(&lfs, cfg) => 0;
+    lfs_fs_stat(&lfs, &fsinfo) => 0;
+    assert(fsinfo.block_size == BLOCK_SIZE);
+    assert(fsinfo.block_count == BLOCK_COUNT);
+    lfs_unmount(&lfs) => 0;
+
+    // grow to new size
+    lfs_mount(&lfs, cfg) => 0;
+    lfs_fs_grow(&lfs, BLOCK_COUNT_2) => 0;
+    lfs_fs_stat(&lfs, &fsinfo) => 0;
+    assert(fsinfo.block_size == BLOCK_SIZE);
+    assert(fsinfo.block_count == BLOCK_COUNT_2);
+    lfs_unmount(&lfs) => 0;
+
+    if (KNOWN_BLOCK_COUNT) {
+        cfg->block_count = BLOCK_COUNT_2;
+    } else {
+        cfg->block_count = 0;
+    }
+
+    lfs_mount(&lfs, cfg) => 0;
+    lfs_fs_stat(&lfs, &fsinfo) => 0;
+    assert(fsinfo.block_size == BLOCK_SIZE);
+    assert(fsinfo.block_count == BLOCK_COUNT_2);
+    lfs_unmount(&lfs) => 0;
+
+    // mounting with the previous size should fail
+    cfg->block_count = BLOCK_COUNT;
+    lfs_mount(&lfs, cfg) => LFS_ERR_INVAL;
+
+    if (KNOWN_BLOCK_COUNT) {
+        cfg->block_count = BLOCK_COUNT_2;
+    } else {
+        cfg->block_count = 0;
+    }
+
+    // same size is a noop
+    lfs_mount(&lfs, cfg) => 0;
+    lfs_fs_grow(&lfs, BLOCK_COUNT_2) => 0;
+    lfs_fs_stat(&lfs, &fsinfo) => 0;
+    assert(fsinfo.block_size == BLOCK_SIZE);
+    assert(fsinfo.block_count == BLOCK_COUNT_2);
+    lfs_unmount(&lfs) => 0;
+
+    lfs_mount(&lfs, cfg) => 0;
+    lfs_fs_stat(&lfs, &fsinfo) => 0;
+    assert(fsinfo.block_size == BLOCK_SIZE);
+    assert(fsinfo.block_count == BLOCK_COUNT_2);
+    lfs_unmount(&lfs) => 0;
+
+    // do some work
+    lfs_mount(&lfs, cfg) => 0;
+    lfs_fs_stat(&lfs, &fsinfo) => 0;
+    assert(fsinfo.block_size == BLOCK_SIZE);
+    assert(fsinfo.block_count == BLOCK_COUNT_2);
+    lfs_file_t file;
+    lfs_file_open(&lfs, &file, "test",
+            LFS_O_CREAT | LFS_O_EXCL | LFS_O_WRONLY) => 0;
+    lfs_file_write(&lfs, &file, "hello!", 6) => 6;
+    lfs_file_close(&lfs, &file) => 0;
+    lfs_unmount(&lfs) => 0;
+
+    lfs_mount(&lfs, cfg) => 0;
+    lfs_fs_stat(&lfs, &fsinfo) => 0;
+    assert(fsinfo.block_size == BLOCK_SIZE);
+    assert(fsinfo.block_count == BLOCK_COUNT_2);
+    lfs_file_open(&lfs, &file, "test", LFS_O_RDONLY) => 0;
+    uint8_t buffer[256];
+    lfs_file_read(&lfs, &file, buffer, sizeof(buffer)) => 6;
+    lfs_file_close(&lfs, &file) => 0;
+    assert(memcmp(buffer, "hello!", 6) == 0);
+    lfs_unmount(&lfs) => 0;
+#endif
+'''
+
 # test that metadata_max does not cause problems for superblock compaction
 [cases.test_superblocks_metadata_max]
 defines.METADATA_MAX = [