Skip to content

Commit

Permalink
feat(storage): add a test app for std::filesystem features
Browse files Browse the repository at this point in the history
  • Loading branch information
igrr committed Dec 3, 2024
1 parent a1042c0 commit 2ca6d2d
Show file tree
Hide file tree
Showing 15 changed files with 610 additions and 0 deletions.
12 changes: 12 additions & 0 deletions tools/test_apps/storage/.build-test-rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions tools/test_apps/storage/std_filesystem/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
70 changes: 70 additions & 0 deletions tools/test_apps/storage/std_filesystem/README.md
Original file line number Diff line number Diff line change
@@ -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 | |
10 changes: 10 additions & 0 deletions tools/test_apps/storage/std_filesystem/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions tools/test_apps/storage/std_filesystem/main/idf_component.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dependencies:
espressif/catch2: "^3.7.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1234567890
Empty file.
Empty file.
196 changes: 196 additions & 0 deletions tools/test_apps/storage/std_filesystem/main/test_ops.cpp
Original file line number Diff line number Diff line change
@@ -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");
44 changes: 44 additions & 0 deletions tools/test_apps/storage/std_filesystem/main/test_paths.cpp
Original file line number Diff line number Diff line change
@@ -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"));
}
Loading

0 comments on commit 2ca6d2d

Please sign in to comment.