feat(storage): add a test app for std::filesystem features

This commit is contained in:
Ivan Grokhotkov
2024-05-07 15:31:38 +02:00
parent a1042c0cc2
commit 2ca6d2d4b4
15 changed files with 610 additions and 0 deletions

View File

@@ -38,3 +38,15 @@ tools/test_apps/storage/sdmmc_console:
- sdmmc
- esp_driver_sdmmc
- esp_driver_sdspi
tools/test_apps/storage/std_filesystem:
enable:
- if: IDF_TARGET in ["esp32", "esp32c3"]
reason: one Xtensa and one RISC-V chip should be enough
disable:
- if: IDF_TOOLCHAIN == "clang"
reason: Issue with C++ exceptions on Xtensa, issue with getrandom linking on RISC-V
depends_components:
- vfs
- newlib
- fatfs

View File

@@ -0,0 +1,8 @@
# The following five lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(COMPONENTS main)
list(PREPEND SDKCONFIG_DEFAULTS "$ENV{IDF_PATH}/tools/test_apps/configs/sdkconfig.debug_helpers" "sdkconfig.defaults")
project(std_filesystem_test)

View File

@@ -0,0 +1,70 @@
| Supported Targets | ESP32 | ESP32-C3 |
| ----------------- | ----- | -------- |
This is a test app which verifies that std::filesystem features work in ESP-IDF. The tests are written using [Catch2](https://github.com/catchorg/Catch2) managed [component](https://components.espressif.com/components/espressif/catch2/).
To run the tests:
```shell
idf.py flash monitor
```
Or, in QEMU:
```shell
idf.py qemu monitor
```
Or, using pytest:
```shell
idf.py build
pytest --embedded-services idf,qemu --target esp32 --ignore managed_components
```
## Feature Support
Please update `_cplusplus_filesystem` section in cplusplus.rst when modifying this table.
| Feature | Supported | Tested | Comment |
|------------------------------|-----------|--------|---------------------------------------------------------------------------------------------------------------|
| absolute | y | y | |
| canonical | y | y | |
| weakly_canonical | y | y | |
| relative | y | y | |
| proximate | y | y | |
| copy | y | y | this function has complex behavior, not sure about test coverage |
| copy_file | y | y | |
| copy_symlink | n | n | symlinks are not supported |
| create_directory | y | y | |
| create_directories | y | y | |
| create_hard_link | n | n | hard links are not supported |
| create_symlink | n | n | symlinks are not supported |
| create_directory_symlink | n | n | symlinks are not supported |
| current_path | partial | y | setting path is not supported in IDF |
| exists | y | y | |
| equivalent | y | y | |
| file_size | y | y | |
| hard_link_count | n | n | hard links are not supported |
| last_write_time | y | y | |
| permissions | partial | y | setting permissions is not supported |
| read_symlink | n | n | symlinks are not supported |
| remove | y | y | |
| remove_all | y | y | |
| rename | y | y | |
| resize_file | n | y | doesn't work, toolchain has to be built with _GLIBCXX_HAVE_TRUNCATE |
| space | n | y | doesn't work, toolchain has to be built with _GLIBCXX_HAVE_SYS_STATVFS_H and statvfs function must be defined |
| status | y | y | |
| symlink_status | n | n | symlinks are not supported |
| temp_directory_path | y | y | works if /tmp directory has been mounted |
| directory_iterator | y | y | |
| recursive_directory_iterator | y | y | |
| is_block_file | y | y | |
| is_character_file | y | y | |
| is_directory | y | y | |
| is_empty | y | y | |
| is_fifo | y | y | |
| is_other | n | n | |
| is_regular_file | y | y | |
| is_socket | y | y | |
| is_symlink | y | y | |

View File

@@ -0,0 +1,10 @@
idf_component_register(SRCS
"test_std_filesystem_main.cpp"
"test_ops.cpp"
"test_paths.cpp"
"test_status.cpp"
INCLUDE_DIRS "."
PRIV_REQUIRES vfs fatfs
WHOLE_ARCHIVE)
fatfs_create_spiflash_image(storage ${CMAKE_CURRENT_LIST_DIR}/test_fs_image FLASH_IN_PROJECT)

View File

@@ -0,0 +1,2 @@
dependencies:
espressif/catch2: "^3.7.0"

View File

@@ -0,0 +1 @@
1234567890

View File

@@ -0,0 +1,196 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <cstddef>
#include <filesystem>
#include <catch2/catch_test_macros.hpp>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/utime.h>
#include "esp_vfs_fat.h"
#include "wear_levelling.h"
class OpsTest {
private:
wl_handle_t m_wl_handle;
public:
OpsTest()
{
esp_vfs_fat_mount_config_t mount_config = VFS_FAT_MOUNT_DEFAULT_CONFIG();
esp_err_t err = esp_vfs_fat_spiflash_mount_rw_wl("/test", "storage", &mount_config, &m_wl_handle);
if (err != ESP_OK) {
throw std::runtime_error("Failed to mount FAT filesystem");
}
}
~OpsTest()
{
esp_vfs_fat_spiflash_unmount_rw_wl("/test", m_wl_handle);
}
void test_create_remove()
{
std::filesystem::create_directory("/test/dir");
CHECK(std::filesystem::exists("/test/dir"));
CHECK(std::filesystem::remove("/test/dir"));
CHECK(!std::filesystem::exists("/test/dir"));
}
/*
The following two tests rely on the following directory structure
in the generated FAT filesystem:
/test
└── test_dir_iter
├── dir1
│   └── f1
└── dir2
└── dir3
└── f3
*/
void test_directory_iterator()
{
std::filesystem::directory_iterator it("/test/test_dir_iter");
CHECK(it != std::filesystem::directory_iterator());
CHECK(it->path() == "/test/test_dir_iter/dir1");
CHECK(it->is_directory());
++it;
CHECK(it != std::filesystem::directory_iterator());
CHECK(it->path() == "/test/test_dir_iter/dir2");
CHECK(it->is_directory());
++it;
CHECK(it == std::filesystem::directory_iterator());
}
void test_recursive_directory_iterator()
{
std::filesystem::recursive_directory_iterator it("/test/test_dir_iter");
CHECK(it != std::filesystem::recursive_directory_iterator());
CHECK(it->path() == "/test/test_dir_iter/dir1");
CHECK(it->is_directory());
++it;
CHECK(it != std::filesystem::recursive_directory_iterator());
CHECK(it->path() == "/test/test_dir_iter/dir1/f1");
CHECK(it->is_regular_file());
++it;
CHECK(it != std::filesystem::recursive_directory_iterator());
CHECK(it->path() == "/test/test_dir_iter/dir2");
CHECK(it->is_directory());
++it;
CHECK(it != std::filesystem::recursive_directory_iterator());
CHECK(it->path() == "/test/test_dir_iter/dir2/dir3");
CHECK(it->is_directory());
++it;
CHECK(it != std::filesystem::recursive_directory_iterator());
CHECK(it->path() == "/test/test_dir_iter/dir2/dir3/f3");
CHECK(it->is_regular_file());
++it;
CHECK(it == std::filesystem::recursive_directory_iterator());
}
void test_copy_remove_recursive_copy()
{
if (std::filesystem::exists("/test/copy_dir")) {
CHECK(std::filesystem::remove_all("/test/copy_dir"));
}
CHECK(std::filesystem::create_directory("/test/copy_dir"));
REQUIRE_NOTHROW(std::filesystem::copy("/test/test_dir_iter/dir1/f1", "/test/copy_dir/f1"));
CHECK(std::filesystem::exists("/test/copy_dir/f1"));
CHECK(std::filesystem::remove("/test/copy_dir/f1"));
CHECK(std::filesystem::remove("/test/copy_dir"));
REQUIRE_NOTHROW(std::filesystem::copy("/test/test_dir_iter", "/test/copy_dir", std::filesystem::copy_options::recursive));
CHECK(std::filesystem::exists("/test/copy_dir/dir1/f1"));
CHECK(std::filesystem::exists("/test/copy_dir/dir2/dir3/f3"));
CHECK(std::filesystem::remove_all("/test/copy_dir"));
}
void test_create_directories()
{
if (std::filesystem::exists("/test/create_dir")) {
CHECK(std::filesystem::remove_all("/test/create_dir"));
}
CHECK(std::filesystem::create_directories("/test/create_dir/dir1/dir2"));
CHECK(std::filesystem::exists("/test/create_dir/dir1/dir2"));
CHECK(std::filesystem::remove_all("/test/create_dir"));
}
void test_rename_file()
{
if (std::filesystem::exists("/test/rename_file")) {
CHECK(std::filesystem::remove("/test/rename_file"));
}
std::filesystem::create_directory("/test/rename_file");
std::filesystem::copy_file("/test/file", "/test/rename_file/file");
CHECK(std::filesystem::exists("/test/rename_file/file"));
std::filesystem::rename("/test/rename_file/file", "/test/rename_file/file2");
CHECK(std::filesystem::exists("/test/rename_file/file2"));
CHECK(std::filesystem::remove_all("/test/rename_file"));
}
void test_file_size_resize()
{
if (std::filesystem::exists("/test/file_size")) {
CHECK(std::filesystem::remove("/test/file_size"));
}
std::filesystem::copy_file("/test/file", "/test/file_size");
CHECK(std::filesystem::file_size("/test/file_size") == 11);
// Not supported: libstdc++ has to be built with _GLIBCXX_HAVE_TRUNCATE
CHECK_THROWS(std::filesystem::resize_file("/test/file_size", 20));
CHECK(std::filesystem::remove("/test/file_size"));
}
void test_file_last_write_time()
{
if (std::filesystem::exists("/test/file_time")) {
CHECK(std::filesystem::remove("/test/file_time"));
}
std::filesystem::copy_file("/test/file", "/test/file_time");
auto time = std::filesystem::last_write_time("/test/file_time");
struct stat st = {};
stat("/test/file_time", &st);
struct utimbuf times = {st.st_atime, st.st_mtime + 1000000000};
utime("/test/file_time", &times);
auto time2 = std::filesystem::last_write_time("/test/file_time");
CHECK(time2 > time);
}
void test_space()
{
// Not supported: libstdc++ has to be built with _GLIBCXX_HAVE_SYS_STATVFS_H and statvfs function
// has to be defined
CHECK_THROWS(std::filesystem::space("/test"));
}
void test_permissions()
{
auto perm = std::filesystem::status("/test/file").permissions();
CHECK(perm == std::filesystem::perms::all);
std::filesystem::permissions("/test/file", std::filesystem::perms::owner_read, std::filesystem::perm_options::replace);
// setting permissions is not supported and has no effect
perm = std::filesystem::status("/test/file").permissions();
CHECK(perm == std::filesystem::perms::all);
}
// when adding a test method, don't forget to add it to the list below
};
METHOD_AS_TEST_CASE(OpsTest::test_create_remove, "Test create and remove directories");
METHOD_AS_TEST_CASE(OpsTest::test_directory_iterator, "Test directory iterator");
METHOD_AS_TEST_CASE(OpsTest::test_recursive_directory_iterator, "Test recursive directory iterator");
METHOD_AS_TEST_CASE(OpsTest::test_copy_remove_recursive_copy, "Test copy, remove and recursive copy");
METHOD_AS_TEST_CASE(OpsTest::test_create_directories, "Test create directories");
METHOD_AS_TEST_CASE(OpsTest::test_rename_file, "Test rename file");
METHOD_AS_TEST_CASE(OpsTest::test_file_size_resize, "Test file size and resize");
METHOD_AS_TEST_CASE(OpsTest::test_file_last_write_time, "Test file last write time");
METHOD_AS_TEST_CASE(OpsTest::test_space, "Test space");
METHOD_AS_TEST_CASE(OpsTest::test_permissions, "Test permissions");

View File

@@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <filesystem>
#include <catch2/catch_test_macros.hpp>
TEST_CASE("std::filesystem path, relative, proximate, absolute")
{
// In IDF, CWD is always in the the root directory
CHECK(std::filesystem::current_path() == "/");
// Create absolute path from relative path
std::filesystem::path rel_path("test/file.txt");
std::filesystem::path abs_path = std::filesystem::absolute(rel_path);
CHECK(abs_path == "/test/file.txt");
// Create relative path from absolute path
std::filesystem::path rel_path2 = std::filesystem::relative(abs_path);
CHECK(rel_path2 == "test/file.txt");
// Create relative path from absolute path with different base
std::filesystem::path rel_path3 = std::filesystem::relative(abs_path, "/test");
CHECK(rel_path3 == "file.txt");
std::filesystem::path prox_path = std::filesystem::proximate("/root1/file", "/root2");
CHECK(prox_path == "../root1/file");
}
TEST_CASE("std::filesystem weakly_canonical")
{
CHECK(std::filesystem::weakly_canonical("/a/b/c/./d/../e/f/../g") == "/a/b/c/e/g");
}
TEST_CASE("std::filesystem current_path")
{
// In IDF, CWD is always in the the root directory
CHECK(std::filesystem::current_path() == "/");
// Changing the current path in IDF is not supported
CHECK_THROWS(std::filesystem::current_path("/test"));
}

View File

@@ -0,0 +1,213 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <filesystem>
#include <cstring>
#include <sys/errno.h>
#include <sys/stat.h>
#include <sys/dirent.h>
#include "esp_vfs.h"
#include "esp_err.h"
#include <catch2/catch_test_macros.hpp>
/* Helper VFS driver to test std::filesystem */
typedef struct {
const char* cmp_path;
int ret_errno;
struct stat ret_stat;
DIR out_dir;
int n_dir_entries;
struct dirent* ret_dirent_array;
} test_vfs_ctx_t;
static int test_vfs_open(void* ctx, const char* path, int flags, int mode)
{
test_vfs_ctx_t* vfs_ctx = (test_vfs_ctx_t*)ctx;
if (strcmp(path, vfs_ctx->cmp_path) != 0) {
errno = vfs_ctx->ret_errno;
return -1;
}
return 0;
}
static int test_vfs_stat(void* ctx, const char* path, struct stat* st)
{
test_vfs_ctx_t* vfs_ctx = (test_vfs_ctx_t*)ctx;
if (strcmp(path, vfs_ctx->cmp_path) != 0) {
errno = vfs_ctx->ret_errno;
return -1;
}
*st = vfs_ctx->ret_stat;
return 0;
}
static DIR* test_vfs_opendir(void* ctx, const char* name)
{
test_vfs_ctx_t* vfs_ctx = (test_vfs_ctx_t*)ctx;
if (strcmp(name, vfs_ctx->cmp_path) != 0) {
errno = vfs_ctx->ret_errno;
return nullptr;
}
return &vfs_ctx->out_dir;
}
static struct dirent* test_vfs_readdir(void* ctx, DIR* pdir)
{
test_vfs_ctx_t* vfs_ctx = (test_vfs_ctx_t*)ctx;
if (vfs_ctx->ret_errno) {
errno = vfs_ctx->ret_errno;
return nullptr;
}
if (vfs_ctx->n_dir_entries == 0) {
return nullptr;
}
vfs_ctx->n_dir_entries--;
struct dirent* ret = &vfs_ctx->ret_dirent_array[0];
vfs_ctx->ret_dirent_array++;
return ret;
}
static int test_vfs_closedir(void* ctx, DIR* pdir)
{
return 0;
}
/* Actual test case starts here */
TEST_CASE("std::filesystem status functions")
{
test_vfs_ctx_t test_ctx = {};
esp_vfs_t desc = {};
desc.flags = ESP_VFS_FLAG_CONTEXT_PTR;
desc.open_p = test_vfs_open;
desc.stat_p = test_vfs_stat;
desc.opendir_p = test_vfs_opendir;
desc.readdir_p = test_vfs_readdir;
desc.closedir_p = test_vfs_closedir;
REQUIRE(esp_vfs_register("/test", &desc, &test_ctx) == ESP_OK);
SECTION("Test file exists") {
test_ctx.cmp_path = "/file.txt";
test_ctx.ret_stat = {};
test_ctx.ret_stat.st_mode = S_IFREG;
CHECK(std::filesystem::exists("/test/file.txt"));
CHECK(std::filesystem::is_regular_file("/test/file.txt"));
}
SECTION("Test directory exists") {
test_ctx.cmp_path = "/dir";
test_ctx.ret_stat = {};
test_ctx.ret_stat.st_mode = S_IFDIR;
CHECK(std::filesystem::exists("/test/dir"));
CHECK(std::filesystem::is_directory("/test/dir"));
}
SECTION("Test non-existent file") {
test_ctx.cmp_path = "";
test_ctx.ret_errno = ENOENT;
CHECK(!std::filesystem::exists("/test/nonexistent"));
}
SECTION("Test is_character_file") {
test_ctx.cmp_path = "/chardev";
test_ctx.ret_stat = {};
test_ctx.ret_stat.st_mode = S_IFCHR;
CHECK(std::filesystem::exists("/test/chardev"));
CHECK(std::filesystem::is_character_file("/test/chardev"));
}
SECTION("Test is_block_file") {
test_ctx.cmp_path = "/blockdev";
test_ctx.ret_stat = {};
test_ctx.ret_stat.st_mode = S_IFBLK;
CHECK(std::filesystem::exists("/test/blockdev"));
CHECK(std::filesystem::is_block_file("/test/blockdev"));
}
SECTION("Test is_fifo") {
test_ctx.cmp_path = "/fifo";
test_ctx.ret_stat = {};
test_ctx.ret_stat.st_mode = S_IFIFO;
CHECK(std::filesystem::exists("/test/fifo"));
CHECK(std::filesystem::is_fifo("/test/fifo"));
}
SECTION("Test is_socket") {
test_ctx.cmp_path = "/socket";
test_ctx.ret_stat = {};
test_ctx.ret_stat.st_mode = S_IFSOCK;
CHECK(std::filesystem::exists("/test/socket"));
CHECK(std::filesystem::is_socket("/test/socket"));
}
SECTION("Test is_symlink") {
test_ctx.cmp_path = "/symlink";
test_ctx.ret_stat = {};
test_ctx.ret_stat.st_mode = S_IFLNK;
CHECK(std::filesystem::exists("/test/symlink"));
CHECK(std::filesystem::is_symlink("/test/symlink"));
}
SECTION("Test is_empty with file") {
test_ctx.cmp_path = "/file.txt";
test_ctx.ret_stat = {};
test_ctx.ret_stat.st_mode = S_IFREG;
test_ctx.ret_stat.st_size = 10;
CHECK(!std::filesystem::is_empty("/test/file.txt"));
test_ctx.ret_stat.st_size = 0;
CHECK(std::filesystem::is_empty("/test/file.txt"));
}
SECTION("Test is_empty with directory") {
test_ctx.cmp_path = "/dir";
test_ctx.ret_stat = {};
test_ctx.ret_stat.st_mode = S_IFDIR;
CHECK(std::filesystem::is_empty("/test/dir"));
}
SECTION("Test is_empty with non-empty directory") {
test_ctx.cmp_path = "/dir";
test_ctx.ret_stat = {};
test_ctx.ret_stat.st_mode = S_IFDIR;
test_ctx.n_dir_entries = 2;
struct dirent entries[2] = {
{ .d_ino = 0, .d_type = DT_REG, .d_name = "foo" },
{ .d_ino = 0, .d_type = DT_REG, .d_name = "bar" },
};
test_ctx.ret_dirent_array = entries;
CHECK(!std::filesystem::is_empty("/test/dir"));
}
SECTION("directory_iterator, empty directory") {
test_ctx.cmp_path = "/dir";
test_ctx.ret_stat = {};
test_ctx.ret_stat.st_mode = S_IFDIR;
test_ctx.n_dir_entries = 0;
struct dirent entries[2] = {
{ .d_ino = 0, .d_type = DT_REG, .d_name = "." },
{ .d_ino = 0, .d_type = DT_REG, .d_name = ".." },
};
test_ctx.ret_dirent_array = entries;
CHECK(std::filesystem::directory_iterator("/test/dir") == std::filesystem::directory_iterator{});
}
CHECK(esp_vfs_unregister("/test") == ESP_OK);
}

View File

@@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <catch2/catch_session.hpp>
extern "C" void app_main(void)
{
const char *argv[] = {
"target_test_main",
"--durations",
"yes",
NULL
};
int argc = sizeof(argv)/sizeof(argv[0]) - 1;
auto result = Catch::Session().run(argc, argv);
if (result != 0) {
printf("Test failed with result %d\n", result);
} else {
printf("Test passed.\n");
}
}

View File

@@ -0,0 +1,3 @@
# Name, Type, SubType, Offset, Size, Flags
factory, app, factory, 0x10000, 1400k,
storage, data, fat, , 528k,
1 # Name Type SubType Offset Size Flags
2 factory app factory 0x10000 1400k
3 storage data fat 528k

View File

@@ -0,0 +1,12 @@
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
@pytest.mark.qemu
@pytest.mark.host_test
@pytest.mark.esp32
@pytest.mark.esp32c3
def test_std_filesystem(dut: Dut) -> None:
dut.expect_exact('All tests passed', timeout=200)

View File

@@ -0,0 +1,14 @@
CONFIG_COMPILER_CXX_EXCEPTIONS=y
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_FATFS_LFN_HEAP=y
CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=y
CONFIG_COMPILER_WARN_WRITE_STRINGS=y
CONFIG_COMPILER_DISABLE_DEFAULT_ERRORS=n
CONFIG_FREERTOS_USE_LIST_DATA_INTEGRITY_CHECK_BYTES=y
CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y
CONFIG_HEAP_POISONING_COMPREHENSIVE=y
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
CONFIG_LOG_DEFAULT_LEVEL_WARN=y
CONFIG_LOG_MAXIMUM_LEVEL_INFO=y