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