| /* |
| * Copyright (c) 2015 Gurjant Kalsi <me@gurjantkalsi.com> |
| * |
| * Permission is hereby granted, free of charge, to any person obtaining |
| * a copy of this software and associated documentation files |
| * (the "Software"), to deal in the Software without restriction, |
| * including without limitation the rights to use, copy, modify, merge, |
| * publish, distribute, sublicense, and/or sell copies of the Software, |
| * and to permit persons to whom the Software is furnished to do so, |
| * subject to the following conditions: |
| * |
| * The above copyright notice and this permission notice shall be |
| * included in all copies or substantial portions of the Software. |
| * |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
| * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
| * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
| * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY |
| * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, |
| * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE |
| * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| */ |
| |
| #if LK_DEBUGLEVEL > 1 |
| |
| #include <string.h> |
| #include <err.h> |
| #include <stdlib.h> |
| |
| #include <lib/bio.h> |
| #include <lib/console.h> |
| #include <lib/fs/spifs.h> |
| |
| #define FS_NAME "spifs" |
| #define MNT_PATH "/s" |
| #define TEST_FILE_PATH "/s/test" |
| #define TEST_PATH_MAX_SIZE 16 |
| |
| typedef bool(*test_func)(const char *); |
| |
| typedef struct { |
| test_func func; |
| const char* name; |
| uint32_t toc_pages; |
| } test; |
| |
| static bool test_empty_after_format(const char *); |
| static bool test_double_create_file(const char *); |
| static bool test_write_read_normal(const char *); |
| static bool test_write_past_eof(const char *); |
| static bool test_full_toc(const char *); |
| static bool test_full_fs(const char *); |
| static bool test_write_past_end_of_capacity(const char *); |
| static bool test_rm_reclaim(const char *); |
| static bool test_corrupt_toc(const char *); |
| static bool test_write_with_offset(const char *); |
| static bool test_read_write_big(const char *); |
| static bool test_rm_active_dirent(const char *); |
| |
| static test tests[] = { |
| {&test_empty_after_format, "Test no files in ToC after format.", 1}, |
| {&test_write_read_normal, "Test the normal read/write file paths.", 1}, |
| {&test_double_create_file, "Test file cannot be created if it already exists.", 1}, |
| {&test_write_past_eof, "Test that file can grow up to capacity.", 1}, |
| {&test_full_toc, "Test that files cannot be created once the ToC is full.", 2}, |
| {&test_full_fs, "Test that files cannot be created once the device is full.", 1}, |
| {&test_rm_reclaim, "Test that files can be deleted and that used space is reclaimed.", 1}, |
| {&test_write_past_end_of_capacity, "Test that we cannot write past the capacity of a file.", 1}, |
| {&test_corrupt_toc, "Test that FS can be mounted with one corrupt ToC.", 1}, |
| {&test_write_with_offset, "Test that files can be written to at an offset.", 1}, |
| {&test_read_write_big, "Test that an unaligned ~10kb buffer can be written and read.", 1}, |
| {&test_rm_active_dirent, "Test that we can remove a file with an open dirent.", 1}, |
| }; |
| |
| bool test_setup(const char *dev_name, uint32_t toc_pages) |
| { |
| spifs_format_args_t args = { |
| .toc_pages = toc_pages, |
| }; |
| |
| status_t res = fs_format_device(FS_NAME, dev_name, (void*)&args); |
| if (res != NO_ERROR) { |
| printf("spifs_format failed dev = %s, toc_pages = %u, retcode = %d\n", |
| dev_name, toc_pages, res); |
| return false; |
| } |
| |
| res = fs_mount(MNT_PATH, FS_NAME, dev_name); |
| if (res != NO_ERROR) { |
| printf("fs_mount failed path = %s, fs name = %s, dev name = %s," |
| " retcode = %d\n", MNT_PATH, FS_NAME, dev_name, res); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool test_teardown(void) |
| { |
| if (fs_unmount(MNT_PATH) != NO_ERROR) { |
| printf("Unmount failed\n"); |
| return false; |
| } |
| |
| return true;; |
| } |
| |
| static bool test_empty_after_format(const char *dev_name) |
| { |
| dirhandle *dhandle; |
| status_t err = fs_open_dir(MNT_PATH, &dhandle); |
| if (err != NO_ERROR) { |
| return false; |
| } |
| |
| struct dirent ent; |
| if (fs_read_dir(dhandle, &ent) >= 0) { |
| fs_close_dir(dhandle); |
| return false; |
| } |
| |
| fs_close_dir(dhandle); |
| return true; |
| } |
| |
| static bool test_double_create_file(const char *dev_name) |
| { |
| status_t status; |
| |
| struct dirent *ent = malloc(sizeof(*ent)); |
| size_t num_files = 0; |
| |
| filehandle *handle; |
| status = fs_create_file(TEST_FILE_PATH, &handle, 10); |
| if (status != NO_ERROR) { |
| goto err; |
| } |
| fs_close_file(handle); |
| |
| filehandle *duphandle; |
| status = fs_create_file(TEST_FILE_PATH, &duphandle, 20); |
| if (status != ERR_ALREADY_EXISTS) { |
| goto err; |
| } |
| |
| dirhandle *dhandle; |
| status = fs_open_dir(MNT_PATH, &dhandle); |
| if (status != NO_ERROR) { |
| goto err; |
| } |
| |
| while ((status = fs_read_dir(dhandle, ent)) >= 0) { |
| num_files++; |
| } |
| |
| status = NO_ERROR; |
| |
| fs_close_dir(dhandle); |
| |
| |
| err: |
| free(ent); |
| |
| return status == NO_ERROR ? num_files == 1 : false; |
| } |
| |
| static bool test_write_read_normal(const char *dev_name) |
| { |
| char test_message[] = "spifs test"; |
| char test_buf[sizeof(test_message)]; |
| |
| bdev_t *dev = bio_open(dev_name); |
| if (!dev) { |
| return false; |
| } |
| uint8_t erase_byte = dev->erase_byte; |
| bio_close(dev); |
| |
| filehandle *handle; |
| status_t status = |
| fs_create_file(TEST_FILE_PATH, &handle, sizeof(test_message)); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| ssize_t bytes; |
| |
| // New files should be initialized to 'erase_byte' |
| bytes = fs_read_file(handle, test_buf, 0, sizeof(test_buf)); |
| if (bytes != sizeof(test_buf)) { |
| return false; |
| } |
| |
| for (size_t i = 0; i < sizeof(test_buf); i++) { |
| if (test_buf[i] != erase_byte) { |
| return false; |
| } |
| } |
| |
| bytes = fs_write_file(handle, test_message, 0, sizeof(test_message)); |
| if (bytes != sizeof(test_message)) { |
| return false; |
| } |
| |
| bytes = fs_read_file(handle, test_buf, 0, sizeof(test_buf)); |
| if (bytes != sizeof(test_buf)) { |
| return false; |
| } |
| |
| status = fs_close_file(handle); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| return strncmp(test_message, test_buf, sizeof(test_message)) == 0; |
| } |
| |
| static bool test_write_past_eof(const char *dev_name) |
| { |
| char test_message[] = "spifs test"; |
| |
| // Create a 0 length file. |
| filehandle *handle; |
| status_t status = |
| fs_create_file(TEST_FILE_PATH, &handle, 0); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| ssize_t bytes = fs_write_file(handle, test_message, 0, sizeof(test_message)); |
| if (bytes != sizeof(test_message)) { |
| return false; |
| } |
| |
| // Make sure the file grows. |
| struct file_stat stat; |
| fs_stat_file(handle, &stat); |
| if (stat.is_dir != false && stat.size != sizeof(test_message)) { |
| return false; |
| } |
| |
| fs_close_file(handle); |
| |
| return true; |
| } |
| |
| static bool test_full_toc(const char *dev_name) |
| { |
| struct fs_stat stat; |
| |
| fs_stat_fs(MNT_PATH, &stat); |
| |
| char test_file_name[TEST_PATH_MAX_SIZE]; |
| |
| filehandle *handle; |
| for (size_t i = 0; i < stat.free_inodes; i++) { |
| memset(test_file_name, 0, TEST_PATH_MAX_SIZE); |
| |
| char filenum[] = "000"; |
| filenum[0] += i / 100; |
| filenum[1] += (i / 10) % 10; |
| filenum[2] += i % 10; |
| |
| strlcat(test_file_name, MNT_PATH, sizeof(test_file_name)); |
| strlcat(test_file_name, "/", sizeof(test_file_name)); |
| strlcat(test_file_name, filenum, sizeof(test_file_name)); |
| |
| status_t status = |
| fs_create_file(test_file_name, &handle, 1); |
| |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| fs_close_file(handle); |
| } |
| |
| // There shouldn't be enough space for this file since we've exhausted all |
| // the inodes. |
| |
| status_t status = fs_create_file(TEST_FILE_PATH, &handle, 1); |
| if (status != ERR_TOO_BIG) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static bool test_rm_reclaim(const char *dev_name) |
| { |
| // Create some number of files that's a power of 2; |
| size_t n_files = 4; |
| |
| struct fs_stat stat; |
| |
| fs_stat_fs(MNT_PATH, &stat); |
| |
| size_t file_size = stat.free_space / (n_files + 1); |
| |
| char test_file_name[TEST_PATH_MAX_SIZE]; |
| |
| filehandle *handle; |
| for (size_t i = 0; i < n_files; i++) { |
| memset(test_file_name, 0, TEST_PATH_MAX_SIZE); |
| |
| char filenum[] = "000"; |
| filenum[0] += i / 100; |
| filenum[1] += (i / 10) % 10; |
| filenum[2] += i % 10; |
| |
| strcat(test_file_name, MNT_PATH); |
| strcat(test_file_name, filenum); |
| |
| status_t status = |
| fs_create_file(test_file_name, &handle, file_size); |
| |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| fs_close_file(handle); |
| } |
| |
| // Try to create a new Big file. |
| char filename[] = "BIGFILE"; |
| memset(test_file_name, 0, TEST_PATH_MAX_SIZE); |
| strcat(test_file_name, MNT_PATH); |
| strcat(test_file_name, filename); |
| |
| status_t status; |
| |
| // This should fail because there's no more space. |
| fs_stat_fs(MNT_PATH, &stat); |
| status = fs_create_file(test_file_name, &handle, stat.free_space + 1); |
| if (status != ERR_TOO_BIG) { |
| return false; |
| } |
| |
| // Delete an existing file to make space for the new file. |
| char existing_filename[] = "001"; |
| memset(test_file_name, 0, TEST_PATH_MAX_SIZE); |
| strcat(test_file_name, MNT_PATH); |
| strcat(test_file_name, existing_filename); |
| |
| status = fs_remove_file(test_file_name); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| |
| // Now this should go through because we've reclaimed the space. |
| status = fs_create_file(test_file_name, &handle, stat.free_space + 1); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| fs_close_file(handle); |
| return true; |
| } |
| |
| static bool test_full_fs(const char *dev_name) |
| { |
| struct fs_stat stat; |
| |
| fs_stat_fs(MNT_PATH, &stat); |
| |
| char second_file_path[TEST_PATH_MAX_SIZE]; |
| memset(second_file_path, 0, TEST_PATH_MAX_SIZE); |
| strcpy(second_file_path, MNT_PATH); |
| strcat(second_file_path, "/fail"); |
| |
| filehandle *handle; |
| status_t status = fs_create_file(TEST_FILE_PATH, &handle, stat.free_space); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| fs_close_file(handle); |
| |
| // There shouldn't be enough space for this file since we've used all the |
| // space. |
| status = fs_create_file(second_file_path, &handle, 1); |
| if (status != ERR_TOO_BIG) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static bool test_write_past_end_of_capacity(const char *dev_name) |
| { |
| filehandle *handle; |
| status_t status = fs_create_file(TEST_FILE_PATH, &handle, 0); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| struct file_stat stat; |
| status = fs_stat_file(handle, &stat); |
| if (status != NO_ERROR) { |
| goto finish; |
| } |
| |
| // We shouldn't be able to write past the capacity of a file. |
| char buf[1]; |
| status = fs_write_file(handle, buf, stat.capacity, 1); |
| if (status == ERR_OUT_OF_RANGE) { |
| status = NO_ERROR; |
| } else { |
| status = ERR_IO; |
| } |
| |
| finish: |
| fs_close_file(handle); |
| return status == NO_ERROR; |
| } |
| |
| static bool test_corrupt_toc(const char *dev_name) |
| { |
| // Create a zero byte file. |
| filehandle *handle; |
| status_t status = fs_create_file(TEST_FILE_PATH, &handle, 0); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| // Grow the file to one byte. This should trigger a ToC flush. Now the |
| // ToC record for this file should exist in both ToCs. Therefore corrupting |
| // either of the ToCs will still yield this file readable. |
| char buf[1] = { 'a' }; |
| status = fs_write_file(handle, buf, 0, 1); |
| if (status != 1) { |
| return false; |
| } |
| |
| status = fs_close_file(handle); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| status = fs_unmount(MNT_PATH); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| // Now we're going to manually corrupt one of the ToCs |
| bdev_t *dev = bio_open(dev_name); |
| if (!dev) { |
| return false; |
| } |
| |
| // Directly write 0s to the block that contains the front-ToC. |
| size_t block_size = dev->block_size; |
| uint8_t *block_buf = memalign(CACHE_LINE, block_size); |
| if (!block_buf) { |
| return false; |
| } |
| memset(block_buf, 0, block_size); |
| |
| ssize_t bytes = bio_write_block(dev, block_buf, 0, 1); |
| |
| free(block_buf); |
| |
| bio_close(dev); |
| |
| if (bytes != (ssize_t)block_size) { |
| return false; |
| } |
| |
| // Mount the FS again and make sure that the file we created is still there. |
| status = fs_mount(MNT_PATH, FS_NAME, dev_name); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| status = fs_open_file(TEST_FILE_PATH, &handle); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| struct file_stat stat; |
| status = fs_stat_file(handle, &stat); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| status = fs_close_file(handle); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| static bool test_write_with_offset(const char *dev_name) |
| { |
| size_t repeats = 3; |
| char test_message[] = "test"; |
| size_t msg_len = strnlen(test_message, sizeof(test_message)); |
| char test_buf[msg_len * repeats]; |
| |
| filehandle *handle; |
| status_t status = fs_create_file(TEST_FILE_PATH, &handle, msg_len); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| ssize_t bytes; |
| for (size_t pos = 0; pos < repeats; pos++) { |
| bytes = fs_write_file(handle, test_message, pos * msg_len, msg_len); |
| if ((size_t)bytes != msg_len) { |
| return false; |
| } |
| } |
| |
| bytes = fs_read_file(handle, test_buf, 0, msg_len * repeats); |
| if ((size_t)bytes != msg_len * repeats) { |
| return false; |
| } |
| |
| status = fs_close_file(handle); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| bool success = true; |
| for (size_t i = 0; i < repeats; i++) { |
| success &= (memcmp(test_message, |
| test_buf + i * msg_len, |
| msg_len) == 0); |
| } |
| return success; |
| } |
| |
| static bool test_read_write_big(const char *dev_name) |
| { |
| bool success = true; |
| |
| size_t buflen = 10013; |
| |
| uint8_t *rbuf = malloc(buflen); |
| if (!rbuf) { |
| return false; |
| } |
| |
| uint8_t *wbuf = malloc(buflen); |
| if (!wbuf) { |
| free(rbuf); |
| return false; |
| } |
| |
| for (size_t i = 0; i < buflen; i++) { |
| wbuf[i] = rand() % sizeof(uint8_t); |
| } |
| |
| filehandle *handle; |
| status_t status = fs_create_file(TEST_FILE_PATH, &handle, buflen); |
| if (status != NO_ERROR) { |
| success = false; |
| goto err; |
| } |
| |
| ssize_t bytes = fs_write_file(handle, wbuf, 0, buflen); |
| if ((size_t)bytes != buflen) { |
| success = false; |
| goto err; |
| } |
| |
| bytes = fs_read_file(handle, rbuf, 0, buflen); |
| if ((size_t)bytes != buflen) { |
| success = false; |
| goto err; |
| } |
| |
| for (size_t i = 0; i < buflen; i++) { |
| if (wbuf[i] != rbuf[i]) { |
| success = false; |
| break; |
| } |
| } |
| |
| err: |
| success &= fs_close_file(handle) == NO_ERROR; |
| |
| free(rbuf); |
| free(wbuf); |
| return success; |
| } |
| |
| static bool test_rm_active_dirent(const char *dev_name) |
| { |
| filehandle *handle; |
| status_t status = fs_create_file(TEST_FILE_PATH, &handle, 0); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| status = fs_close_file(handle); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| dirhandle *dhandle; |
| status = fs_open_dir(MNT_PATH, &dhandle); |
| if (status != NO_ERROR) { |
| return false; |
| } |
| |
| // Dir handle should now be pointing to the only file in our FS. |
| status = fs_remove_file(TEST_FILE_PATH); |
| if (status != NO_ERROR) { |
| fs_close_dir(dhandle); |
| return false; |
| } |
| |
| bool success = true; |
| struct dirent *ent = malloc(sizeof(*ent)); |
| if (fs_read_dir(dhandle, ent) >= 0) { |
| success = false; |
| } |
| |
| success &= fs_close_dir(dhandle) == NO_ERROR; |
| free(ent); |
| |
| return success; |
| } |
| |
| static int cmd_spifs(int argc, const cmd_args *argv) |
| { |
| if (argc < 3) { |
| notenoughargs: |
| printf("not enough arguments:\n"); |
| usage: |
| printf("%s test <device>\n", argv[0].str); |
| return -1; |
| } |
| |
| if (strcmp(argv[1].str, "test")) { |
| goto usage; |
| } |
| |
| // Make sure this block device is legit. |
| bdev_t *dev = bio_open(argv[2].str); |
| if (!dev) { |
| printf("error: could not open block device %s\n", argv[2].str); |
| return -1; |
| } |
| bio_close(dev); |
| |
| size_t passed = 0; |
| size_t attempted = 0; |
| for (size_t i = 0; i < countof(tests); i++) { |
| ++attempted; |
| if (!test_setup(argv[2].str, tests[i].toc_pages)) { |
| printf("Test Setup failed before %s. Exiting.\n", tests[i].name); |
| break; |
| } |
| |
| if (tests[i].func(argv[2].str)) { |
| printf(" [Passed] %s\n", tests[i].name); |
| ++passed; |
| } else { |
| printf(" [Failed] %s\n", tests[i].name); |
| } |
| |
| if (!test_teardown()) { |
| printf("Test teardown failed after %s. Exiting.\n", tests[i].name); |
| break; |
| } |
| } |
| printf("\nPassed %u of %u tests.\n", passed, attempted); |
| |
| if (attempted != countof(tests)) { |
| printf("(Skipped %u)\n", countof(tests) - attempted); |
| } |
| |
| return countof(tests) - passed; |
| } |
| |
| STATIC_COMMAND_START |
| STATIC_COMMAND("spifs", "commands related to the spifs implementation.", &cmd_spifs) |
| STATIC_COMMAND_END(spifs); |
| |
| #endif // LK_DEBUGLEVEL > 1 |