Files

464 lines
14 KiB
C

/*
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#include "esp_vfs.h"
#include "esp_vfs_ops.h"
#include "test_vfs_linux_dev.h"
#include "unity.h"
#include "unity_fixture.h"
TEST_GROUP(vfs_linux);
/* Forward declaration; defined together with the VFS registration regression
* tests below. Used by TEST_TEAR_DOWN to clean up leaked registrations. */
static void unregister_all_tracked(void);
TEST_SETUP(vfs_linux)
{
}
TEST_TEAR_DOWN(vfs_linux)
{
/* Unregister any VFS entries that a test registered but did not clean up
* itself (e.g. because a TEST_ASSERT_* aborted the test early). This keeps
* the VFS table clean for subsequent tests. */
unregister_all_tracked();
}
static void test_create_file_with_text(const char* name, const char* text)
{
int fd = -1;
fd = open(name, O_CREAT|O_RDWR, 0777);
TEST_ASSERT_NOT_EQUAL(-1, fd);
ssize_t sz = write(fd, text, strlen(text));
TEST_ASSERT_EQUAL(strlen(text), sz);
TEST_ASSERT_EQUAL(0, close(fd));
}
TEST(vfs_linux,test_linux_vfs_open)
{
linux_vfs_dev_register();
const char *test_str = "Espressif";
const char *filename = "/linux/test.txt";
test_create_file_with_text(filename, test_str);
int fd = open(filename, O_RDWR);
TEST_ASSERT_NOT_EQUAL(-1, fd);
int len = strlen(test_str);
char buf[10];
TEST_ASSERT_EQUAL(len, read(fd, buf, len));
TEST_ASSERT_EQUAL(0, strncmp(test_str, buf, len));
close(fd);
unlink(filename);
printf("Inside %s\n",__func__);
linux_vfs_dev_unregister();
}
static void test_lseek(void)
{
const char *test_str = "0123456789\n";
const char *filename = "/linux/lseek.txt";
test_create_file_with_text(filename, test_str);
int fd = open(filename, O_RDWR);
TEST_ASSERT_NOT_EQUAL(-1, fd);
off_t off = lseek(fd, 6, SEEK_CUR);
TEST_ASSERT_EQUAL(6, off);
off = lseek(fd, 3, SEEK_SET);
TEST_ASSERT_EQUAL(3, off);
off = lseek(fd, -9, SEEK_END);
TEST_ASSERT_EQUAL(2, off);
close(fd);
unlink(filename);
}
TEST(vfs_linux, test_lseek_via_vfs)
{
printf("Inside %s\n",__func__);
linux_vfs_dev_register();
printf("Inside %s\n",__func__);
test_lseek();
linux_vfs_dev_unregister();
}
static void test_pread_pwrite(void)
{
const char *test_str = "0123456789";
const char *filename = "/linux/pread_pwrite.txt";
test_create_file_with_text(filename, test_str);
int fd = open(filename, O_RDWR);
TEST_ASSERT_NOT_EQUAL(-1, fd);
// Test pwrite
const char *write_str = "ABCD";
ssize_t written = pwrite(fd, write_str, strlen(write_str), 2);
TEST_ASSERT_EQUAL(strlen(write_str), written);
// Test pread
char read_buf[10] = {0};
ssize_t bytes_read = pread(fd, read_buf, 4, 2);
TEST_ASSERT_EQUAL(4, bytes_read);
TEST_ASSERT_EQUAL(0, strncmp(write_str, read_buf, 4));
close(fd);
unlink(filename);
}
TEST(vfs_linux, test_pread_pwrite_via_vfs)
{
linux_vfs_dev_register();
test_pread_pwrite();
linux_vfs_dev_unregister();
}
static void test_unlink(void)
{
const char *filename = "/linux/unlink.txt";
test_create_file_with_text(filename, "test");
// Test that file exists
int fd = open(filename, O_RDWR);
TEST_ASSERT_NOT_EQUAL(-1, fd);
close(fd);
// Test unlink
TEST_ASSERT_EQUAL(0, unlink(filename));
// Test that file no longer exists
fd = open(filename, O_RDWR);
TEST_ASSERT_EQUAL(-1, fd);
TEST_ASSERT_EQUAL(ENOENT, errno);
}
TEST(vfs_linux, test_unlink_via_vfs)
{
linux_vfs_dev_register();
test_unlink();
linux_vfs_dev_unregister();
}
static void test_fstat(void)
{
const char *filename = "/linux/fstat.txt";
test_create_file_with_text(filename, "test");
int fd = open(filename, O_RDWR);
TEST_ASSERT_NOT_EQUAL(-1, fd);
struct stat st;
TEST_ASSERT_EQUAL(0, fstat(fd, &st));
TEST_ASSERT_TRUE(S_ISREG(st.st_mode)); // Check if it's a regular file
TEST_ASSERT_EQUAL(4, st.st_size); // Check file size
close(fd);
unlink(filename);
}
TEST(vfs_linux, test_fstat_via_vfs)
{
linux_vfs_dev_register();
test_fstat();
linux_vfs_dev_unregister();
}
static void test_fcntl(void)
{
const char *filename = "/linux/fcntl.txt";
test_create_file_with_text(filename, "test");
int fd = open(filename, O_RDWR);
TEST_ASSERT_NOT_EQUAL(-1, fd);
// Test F_GETFL
int flags = fcntl(fd, F_GETFL);
TEST_ASSERT_NOT_EQUAL(-1, flags);
TEST_ASSERT_EQUAL(O_RDWR, (flags & O_RDWR)); // Check if file is opened in read-write mode
close(fd);
unlink(filename);
}
TEST(vfs_linux, test_fcntl_via_vfs)
{
linux_vfs_dev_register();
test_fcntl();
linux_vfs_dev_unregister();
}
static void test_ftruncate(void)
{
const char *test_str = "0123456789\n";
const char *filename = "/linux/ftruncate.txt";
test_create_file_with_text(filename, test_str);
int fd = open(filename, O_RDWR);
TEST_ASSERT_NOT_EQUAL(-1, fd);
// Test truncating to a smaller size
const char truncated_1[] = "01234";
off_t truncated_len = strlen(truncated_1);
TEST_ASSERT_EQUAL(0, ftruncate(fd, truncated_len));
// Verify the file size after truncation
struct stat st;
TEST_ASSERT_EQUAL(0, fstat(fd, &st));
TEST_ASSERT_EQUAL(truncated_len, st.st_size);
// Read back the truncated content
char buf[32] = {0};
TEST_ASSERT_EQUAL(truncated_len, read(fd, buf, sizeof(buf)));
TEST_ASSERT_EQUAL(0, strncmp(truncated_1, buf, truncated_len));
// Test truncating to a larger size
off_t new_size = truncated_len + 5;
TEST_ASSERT_EQUAL(0, ftruncate(fd, new_size));
TEST_ASSERT_EQUAL(0, fstat(fd, &st));
TEST_ASSERT_EQUAL(new_size, st.st_size);
// Test truncating to zero
TEST_ASSERT_EQUAL(0, ftruncate(fd, 0));
TEST_ASSERT_EQUAL(0, fstat(fd, &st));
TEST_ASSERT_EQUAL(0, st.st_size);
// Test invalid length
TEST_ASSERT_EQUAL(-1, ftruncate(fd, -1));
TEST_ASSERT_EQUAL(EINVAL, errno);
close(fd);
unlink(filename);
}
TEST(vfs_linux, test_ftruncate_via_vfs)
{
linux_vfs_dev_register();
test_ftruncate();
linux_vfs_dev_unregister();
}
/* Regression tests for the slot-accounting bug in esp_vfs_register_fs_common().
*
* The registration code used to track an ever-increasing counter (s_vfs_count)
* which was incremented when a VFS was registered into a new top slot but was
* never decremented on unregister. The "is there room for another VFS?" check
* compared that counter against VFS_MAX_COUNT. Once the table had been filled
* once, unregistering and registering again could permanently fail with
* ESP_ERR_NO_MEM even though free slots were available.
*
* The registration code path (esp_vfs_register_fs -> esp_vfs_register_fs_common)
* is compiled and exercised on the Linux host target, so these tests reproduce
* the bug here. They are expected to fail on the pre-fix code and pass on the
* fixed code.
*/
/* Maximum number of VFS slots in this build (CONFIG_VFS_MAX_COUNT, default 8).
* The upper limit of the Kconfig range is 20, so a loop bound of 20 is always
* enough to fill the table regardless of configuration. */
#define TEST_VFS_REGISTER_LIMIT 20
/* A minimal VFS used only for registration in the tests below. We never open
* files through it, only register/unregister it to exercise the VFS slot
* bookkeeping, so the (context-pointer) open/close operations are never actually
* invoked. The modern ops API is used to avoid the deprecation warnings that the
* legacy esp_vfs_t fields carry. */
static int dummy_open(void *ctx, const char *path, int flags, int mode)
{
(void) ctx; (void) path; (void) flags; (void) mode;
errno = ENOENT;
return -1;
}
static int dummy_close(void *ctx, int fd)
{
(void) ctx; (void) fd;
return 0;
}
static const esp_vfs_fs_ops_t s_dummy_vfs = {
.open_p = &dummy_open,
.close_p = &dummy_close,
};
/* Track every mount point this test file registers, so that TEST_TEAR_DOWN can
* unregister all of them even if a TEST_ASSERT_* aborts the test early (Unity
* Fixture uses longjmp on failure, which would otherwise skip in-test cleanup
* and leave the VFS table in a corrupted state for the next test). */
static char s_tracked_paths[TEST_VFS_REGISTER_LIMIT][8];
static int s_tracked_count;
/* Register the dummy VFS at the given path and record it for cleanup. Returns
* the esp_vfs_register_fs() result; only successful registrations are tracked. */
static esp_err_t register_tracked(const char *path)
{
esp_err_t err = esp_vfs_register_fs(path, &s_dummy_vfs, ESP_VFS_FLAG_CONTEXT_PTR, NULL);
if (err == ESP_OK) {
snprintf(s_tracked_paths[s_tracked_count], sizeof(s_tracked_paths[0]), "%s", path);
s_tracked_count++;
}
return err;
}
/* Unregister all still-registered tracked paths. Safe to call repeatedly. */
static void unregister_all_tracked(void)
{
for (int i = 0; i < s_tracked_count; ++i) {
esp_vfs_unregister(s_tracked_paths[i]);
}
s_tracked_count = 0;
}
/* Build a short, unique mount point ("/t<idx>") for the test VFS entries. */
static void make_test_path(char *buf, size_t buf_len, int idx)
{
snprintf(buf, buf_len, "/t%d", idx);
}
/* Fill every free VFS slot with the dummy VFS. Other VFSes may already be
* registered, so we keep going until registration reports the table is full.
* Returns the number of entries this function registered; paths[] is filled
* with the corresponding mount points. */
static int fill_vfs_table(char paths[][8])
{
int registered = 0;
for (int i = 0; i < TEST_VFS_REGISTER_LIMIT; ++i) {
make_test_path(paths[i], 8, i);
esp_err_t err = register_tracked(paths[i]);
if (err == ESP_ERR_NO_MEM) {
break; // table is full
}
TEST_ASSERT_EQUAL(ESP_OK, err);
registered++;
}
return registered;
}
TEST(vfs_linux, test_register_after_table_full)
{
char paths[TEST_VFS_REGISTER_LIMIT][8];
int registered = fill_vfs_table(paths);
/* We must have registered at least one entry, and the table must be full
* now (the next registration must fail with ESP_ERR_NO_MEM). */
TEST_ASSERT_GREATER_THAN(0, registered);
TEST_ASSERT_EQUAL(ESP_ERR_NO_MEM,
esp_vfs_register_fs("/overflow", &s_dummy_vfs, ESP_VFS_FLAG_CONTEXT_PTR, NULL));
/* Free the topmost entry, then register again. On the buggy code the stale
* counter would still equal VFS_MAX_COUNT and this would fail with
* ESP_ERR_NO_MEM. */
int top = registered - 1;
TEST_ASSERT_EQUAL(ESP_OK, esp_vfs_unregister(paths[top]));
/* paths[top] is still tracked from fill_vfs_table(), so re-register with the
* raw API instead of register_tracked() to avoid a duplicate tracker entry. */
TEST_ASSERT_EQUAL(ESP_OK, esp_vfs_register_fs(paths[top], &s_dummy_vfs, ESP_VFS_FLAG_CONTEXT_PTR, NULL));
/* Cleanup of registered entries happens in TEST_TEAR_DOWN. */
}
TEST(vfs_linux, test_register_into_middle_hole)
{
char paths[TEST_VFS_REGISTER_LIMIT][8];
int registered = fill_vfs_table(paths);
/* Need at least 3 entries so there is a genuine middle entry that is neither
* the first nor the topmost slot. */
TEST_ASSERT_GREATER_OR_EQUAL_INT(3, registered);
/* The table must be full now. */
TEST_ASSERT_EQUAL(ESP_ERR_NO_MEM,
esp_vfs_register_fs("/overflow", &s_dummy_vfs, ESP_VFS_FLAG_CONTEXT_PTR, NULL));
/* Unregister an entry in the middle (not the first, not the topmost). This
* leaves a NULL hole below the upper bound. The path stays tracked, so it is
* cleaned up in TEST_TEAR_DOWN regardless of what happens below. */
int middle = registered / 2;
TEST_ASSERT_NOT_EQUAL(0, middle);
TEST_ASSERT_NOT_EQUAL(registered - 1, middle);
TEST_ASSERT_EQUAL(ESP_OK, esp_vfs_unregister(paths[middle]));
/* Register again: with only the middle slot free, the registration must
* reuse that hole and succeed. On the buggy code it failed with
* ESP_ERR_NO_MEM. (The path is already tracked from fill_vfs_table().) */
TEST_ASSERT_EQUAL(ESP_OK, esp_vfs_register_fs(paths[middle], &s_dummy_vfs, ESP_VFS_FLAG_CONTEXT_PTR, NULL));
/* The table must be full again - the hole was reused, not appended. */
TEST_ASSERT_EQUAL(ESP_ERR_NO_MEM,
esp_vfs_register_fs("/overflow", &s_dummy_vfs, ESP_VFS_FLAG_CONTEXT_PTR, NULL));
/* Cleanup of registered entries happens in TEST_TEAR_DOWN. */
}
TEST(vfs_linux, test_register_unregister_cycles)
{
/* Fill the whole VFS table and then drain it again, several times over.
*
* On the fixed code every round must be able to register exactly the same
* number of entries, because unregistering lowers the upper bound again.
* On the buggy code the stale counter never came back down, so the second
* and later rounds would be able to register fewer entries (and eventually
* none at all), which this test detects. */
char paths[TEST_VFS_REGISTER_LIMIT][8];
const int rounds = 5;
int first_round_count = -1;
for (int r = 0; r < rounds; ++r) {
/* Register until the table is full. */
int count = fill_vfs_table(paths);
if (first_round_count < 0) {
first_round_count = count;
TEST_ASSERT_GREATER_THAN(0, first_round_count);
} else {
/* The capacity must not shrink between rounds. */
TEST_ASSERT_EQUAL_INT(first_round_count, count);
}
/* Drain the table again. The paths stay tracked until they are actually
* unregistered here; TEST_TEAR_DOWN is only the safety net for an early
* abort. */
unregister_all_tracked();
}
}
TEST_GROUP_RUNNER(vfs_linux)
{
RUN_TEST_CASE(vfs_linux, test_linux_vfs_open);
RUN_TEST_CASE(vfs_linux, test_lseek_via_vfs);
RUN_TEST_CASE(vfs_linux, test_pread_pwrite_via_vfs);
RUN_TEST_CASE(vfs_linux, test_unlink_via_vfs);
RUN_TEST_CASE(vfs_linux, test_fstat_via_vfs);
RUN_TEST_CASE(vfs_linux, test_fcntl_via_vfs);
RUN_TEST_CASE(vfs_linux, test_ftruncate_via_vfs);
RUN_TEST_CASE(vfs_linux, test_register_after_table_full);
RUN_TEST_CASE(vfs_linux, test_register_into_middle_hole);
RUN_TEST_CASE(vfs_linux, test_register_unregister_cycles);
}
static void run_all_tests(void)
{
RUN_TEST_GROUP(vfs_linux);
}
int main(int argc, char **argv)
{
UNITY_MAIN_FUNC(run_all_tests);
return 0;
}