/ firmware / src / filesystems / littlefs_test.cpp
littlefs_test.cpp
  1  #ifdef PIO_UNIT_TESTING
  2  
  3  #include <config.h>
  4  #include <testing/utils.h>
  5  
  6  namespace filesystems::littlefs { void test(void); }
  7  
  8  #include <Arduino.h>
  9  #include <LittleFS.h>
 10  #include <stdio.h>
 11  
 12  static void test_littlefs_mounts(void) {
 13    WHEN("LittleFS is mounted");
 14    TEST_ASSERT_TRUE_MESSAGE(LittleFS.begin(true),
 15      "device: LittleFS.begin() failed");
 16  
 17    size_t total = LittleFS.totalBytes();
 18    size_t used  = LittleFS.usedBytes();
 19    TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE(0, total,
 20      "device: LittleFS total is 0");
 21  
 22    TEST_PRINTF("%u KB total, %u KB used", total / 1024, used / 1024);
 23  }
 24  
 25  static void test_littlefs_write_read_roundtrip(void) {
 26    GIVEN("LittleFS is mounted");
 27    LittleFS.begin(false);
 28  
 29    WHEN("a file is written and read back");
 30  
 31    const char *path = "/.test_lfs.tmp";
 32    const char *payload = "littlefs roundtrip test";
 33  
 34    File writer = LittleFS.open(path, FILE_WRITE);
 35    TEST_ASSERT_TRUE_MESSAGE((bool)writer, "device: open for write failed");
 36    writer.print(payload);
 37    writer.close();
 38  
 39    File reader = LittleFS.open(path, FILE_READ);
 40    TEST_ASSERT_TRUE_MESSAGE((bool)reader, "device: open for read failed");
 41    char buf[64] = {0};
 42    reader.readBytes(buf, sizeof(buf) - 1);
 43    reader.close();
 44  
 45    TEST_ASSERT_EQUAL_STRING_MESSAGE(payload, buf,
 46      "device: LittleFS read doesn't match write");
 47  
 48    LittleFS.remove(path);
 49  }
 50  
 51  static void test_littlefs_ssh_dir_persists(void) {
 52    GIVEN("LittleFS is mounted");
 53    LittleFS.begin(false);
 54  
 55    WHEN("a file is written to .ssh and LittleFS is remounted");
 56  
 57    if (!LittleFS.exists("/.ssh")) {
 58      LittleFS.mkdir("/.ssh");
 59    }
 60    TEST_ASSERT_TRUE_MESSAGE(LittleFS.exists("/.ssh"),
 61      "device: /.ssh directory missing");
 62  
 63    // Write a test file inside .ssh
 64    File f = LittleFS.open("/.ssh/.test_persist", FILE_WRITE);
 65    f.print("persist");
 66    f.close();
 67  
 68    // Remount
 69    LittleFS.end();
 70    LittleFS.begin(true);
 71  
 72    TEST_ASSERT_TRUE_MESSAGE(LittleFS.exists("/.ssh/.test_persist"),
 73      "device: file in /.ssh did not survive remount");
 74  
 75    LittleFS.remove("/.ssh/.test_persist");
 76  }
 77  
 78  static void test_littlefs_mkdir_nested(void) {
 79    GIVEN("LittleFS is mounted");
 80    LittleFS.begin(false);
 81  
 82    WHEN("nested directories are created");
 83  
 84    LittleFS.mkdir("/.test_nest");
 85    LittleFS.mkdir("/.test_nest/deep");
 86  
 87    File f = LittleFS.open("/.test_nest/deep/file.txt", FILE_WRITE);
 88    TEST_ASSERT_TRUE_MESSAGE((bool)f, "device: write to nested dir failed");
 89    f.print("deep");
 90    f.close();
 91  
 92    TEST_ASSERT_TRUE_MESSAGE(LittleFS.exists("/.test_nest/deep/file.txt"),
 93      "device: nested file missing");
 94  
 95    LittleFS.remove("/.test_nest/deep/file.txt");
 96    LittleFS.rmdir("/.test_nest/deep");
 97    LittleFS.rmdir("/.test_nest");
 98  }
 99  
100  static void test_littlefs_hostkey_path(void) {
101    GIVEN("LittleFS is mounted");
102    LittleFS.begin(false);
103  
104    WHEN("a host key is written and read via both APIs");
105  
106    LittleFS.mkdir("/.ssh");
107    File writer = LittleFS.open(config::ssh::HOSTKEY_PATH, FILE_WRITE);
108    TEST_ASSERT_TRUE_MESSAGE((bool)writer, "device: cannot open hostkey path for writing");
109    writer.print("fake-key-data");
110    writer.close();
111  
112    TEST_ASSERT_TRUE_MESSAGE(LittleFS.exists(config::ssh::HOSTKEY_PATH),
113      "device: hostkey file missing via LittleFS API");
114  
115    // Verify the VFS path (what libssh uses) resolves to the same file
116    String vfs_path = String(LittleFS.mountpoint()) + config::ssh::HOSTKEY_PATH;
117    FILE *vfs_fp = fopen(vfs_path.c_str(), "r");
118    TEST_ASSERT_NOT_NULL_MESSAGE(vfs_fp,
119      "device: fopen via VFS mountpoint failed — paths may be inconsistent");
120    char vfs_buf[32] = {0};
121    fread(vfs_buf, 1, sizeof(vfs_buf) - 1, vfs_fp);
122    fclose(vfs_fp);
123  
124    TEST_ASSERT_EQUAL_STRING_MESSAGE("fake-key-data", vfs_buf,
125      "device: VFS path content doesn't match LittleFS write");
126  
127    // Remount and verify persistence
128    LittleFS.end();
129    LittleFS.begin(false);
130  
131    TEST_ASSERT_TRUE_MESSAGE(LittleFS.exists(config::ssh::HOSTKEY_PATH),
132      "device: hostkey file missing after remount");
133  
134    LittleFS.remove(config::ssh::HOSTKEY_PATH);
135  }
136  
137  static void test_littlefs_rmdir_non_empty(void) {
138    GIVEN("LittleFS is mounted");
139    LittleFS.begin(false);
140  
141    WHEN("rmdir is called on a non-empty directory");
142  
143    LittleFS.mkdir("/.test_rmdir");
144    File f = LittleFS.open("/.test_rmdir/child.txt", FILE_WRITE);
145    TEST_ASSERT_TRUE_MESSAGE((bool)f, "device: failed to create file in dir");
146    f.print("content");
147    f.close();
148  
149    bool result = LittleFS.rmdir("/.test_rmdir");
150    TEST_PRINTF("rmdir on non-empty dir returned: %s",
151             result ? "true (recursive!)" : "false (non-recursive)");
152    TEST_ASSERT_FALSE_MESSAGE(result,
153      "device: rmdir removed a non-empty directory — it IS recursive");
154  
155    LittleFS.remove("/.test_rmdir/child.txt");
156    LittleFS.rmdir("/.test_rmdir");
157  }
158  
159  static void test_littlefs_remove_on_directory(void) {
160    GIVEN("LittleFS is mounted");
161    LittleFS.begin(false);
162  
163    WHEN("remove() is called on a directory");
164  
165    LittleFS.mkdir("/.test_rm_dir");
166    TEST_ASSERT_TRUE_MESSAGE(LittleFS.exists("/.test_rm_dir"),
167      "device: mkdir failed");
168  
169    bool result = LittleFS.remove("/.test_rm_dir");
170    TEST_PRINTF("remove() on directory returned: %s",
171             result ? "true (handles dirs)" : "false (files only)");
172  
173    if (LittleFS.exists("/.test_rm_dir"))
174      LittleFS.rmdir("/.test_rm_dir");
175  }
176  
177  static void test_littlefs_rename(void) {
178    GIVEN("LittleFS is mounted");
179    LittleFS.begin(false);
180  
181    WHEN("a file is renamed");
182  
183    const char *src = "/.test_rename_src.tmp";
184    const char *dst = "/.test_rename_dst.tmp";
185    const char *payload = "rename-test-payload";
186  
187    if (LittleFS.exists(dst)) LittleFS.remove(dst);
188  
189    File f = LittleFS.open(src, FILE_WRITE);
190    TEST_ASSERT_TRUE_MESSAGE((bool)f, "device: open src for write failed");
191    f.print(payload);
192    f.close();
193  
194    bool renamed = LittleFS.rename(src, dst);
195    TEST_ASSERT_TRUE_MESSAGE(renamed, "device: rename returned false");
196    TEST_ASSERT_FALSE_MESSAGE(LittleFS.exists(src),
197      "device: source still exists after rename");
198    TEST_ASSERT_TRUE_MESSAGE(LittleFS.exists(dst),
199      "device: destination missing after rename");
200  
201    File reader = LittleFS.open(dst, FILE_READ);
202    TEST_ASSERT_TRUE_MESSAGE((bool)reader, "device: open dst for read failed");
203    char buf[64] = {0};
204    reader.readBytes(buf, sizeof(buf) - 1);
205    reader.close();
206  
207    TEST_ASSERT_EQUAL_STRING_MESSAGE(payload, buf,
208      "device: renamed file content doesn't match");
209  
210    LittleFS.remove(dst);
211  }
212  
213  void filesystems::littlefs::test(void) {
214    MODULE("LittleFS");
215    RUN_TEST(test_littlefs_mounts);
216    RUN_TEST(test_littlefs_write_read_roundtrip);
217    RUN_TEST(test_littlefs_ssh_dir_persists);
218    RUN_TEST(test_littlefs_mkdir_nested);
219    RUN_TEST(test_littlefs_hostkey_path);
220    RUN_TEST(test_littlefs_rmdir_non_empty);
221    RUN_TEST(test_littlefs_remove_on_directory);
222    RUN_TEST(test_littlefs_rename);
223  }
224  
225  #endif