test: fs: add C-based filesystem tests

Add C implementations of filesystem tests that can be called via
the 'ut fs' command. These tests use UTF_MANUAL flag since they require
external setup, i.e. creation of filesystem images.

This covers the existing TestFsBasic tests.

The tests use typed arguments (fs_type, fs_image, md5 values) passed
via the command line.

Add a few helpers to make the code easier to read.

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
This commit is contained in:
Simon Glass
2025-12-12 14:39:56 -07:00
parent 2ac3226758
commit 72aeb4a2e2
5 changed files with 537 additions and 0 deletions

39
include/test/fs.h Normal file
View File

@@ -0,0 +1,39 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Copyright 2025 Google LLC
*/
#ifndef __TEST_FS_H
#define __TEST_FS_H
#include <test/test.h>
#include <test/ut.h>
/**
* FS_TEST() - Define a new filesystem test
*
* @name: Name of test function
* @flags: Flags for the test (see enum ut_flags)
*/
#define FS_TEST(_name, _flags) UNIT_TEST(_name, UTF_DM | (_flags), fs)
/**
* FS_TEST_ARGS() - Define a filesystem test with inline arguments
*
* Like FS_TEST() but for tests that take arguments.
* The test can access arguments via uts->args[].
* The NULL terminator is added automatically.
*
* Example:
* FS_TEST_ARGS(my_test, UTF_MANUAL,
* { "fs_type", UT_ARG_STR },
* { "fs_image", UT_ARG_STR });
*
* @name: Name of test function
* @flags: Flags for the test (see enum ut_flags)
* @...: Argument definitions (struct ut_arg_def initializers)
*/
#define FS_TEST_ARGS(_name, _flags, ...) \
UNIT_TEST_ARGS(_name, UTF_DM | (_flags), fs, __VA_ARGS__)
#endif /* __TEST_FS_H */

View File

@@ -22,6 +22,7 @@ obj-y += boot/
obj-$(CONFIG_UNIT_TEST) += common/
obj-$(CONFIG_UT_ENV) += env/
obj-$(CONFIG_UT_FDT_OVERLAY) += fdt_overlay/
obj-$(CONFIG_SANDBOX) += fs/
obj-y += log/
else
obj-$(CONFIG_SPL_UT_LOAD) += image/

View File

@@ -61,6 +61,7 @@ SUITE_DECL(exit);
SUITE_DECL(fdt);
SUITE_DECL(fdt_overlay);
SUITE_DECL(font);
SUITE_DECL(fs);
SUITE_DECL(hush);
SUITE_DECL(lib);
SUITE_DECL(loadm);
@@ -89,6 +90,7 @@ static struct suite suites[] = {
SUITE(fdt, "fdt command"),
SUITE(fdt_overlay, "device tree overlays"),
SUITE(font, "font command"),
SUITE(fs, "filesystem tests"),
SUITE(hush, "hush behaviour"),
SUITE(lib, "library functions"),
SUITE(loadm, "loadm command parameters and loading memory blob"),

3
test/fs/Makefile Normal file
View File

@@ -0,0 +1,3 @@
# SPDX-License-Identifier: GPL-2.0+
obj-y += fs_basic.o

492
test/fs/fs_basic.c Normal file
View File

@@ -0,0 +1,492 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Basic filesystem tests - C implementation for Python wrapper
*
* These tests are marked UTF_MANUAL and are intended to be called from
* test_basic.py which sets up the filesystem image and expected values.
*
* Copyright 2025 Google LLC
*/
#include <command.h>
#include <dm.h>
#include <env.h>
#include <fs.h>
#include <fs_legacy.h>
#include <hexdump.h>
#include <image.h>
#include <linux/sizes.h>
#include <mapmem.h>
#include <test/fs.h>
#include <test/test.h>
#include <test/ut.h>
#include <u-boot/md5.h>
/* Test constants matching fstest_defs.py */
#define ADDR 0x01000008
/*
* Common argument indices. Each test declares only the arguments it needs,
* so indices 2+ vary per test - see comments in each test.
*/
#define FS_ARG_TYPE 0 /* fs_type: ext4, fat, exfat, fs_generic */
#define FS_ARG_IMAGE 1 /* fs_image: path to filesystem image */
/* Common arguments for all filesystem tests (indices 0 and 1) */
#define COMMON_ARGS \
{ "fs_type", UT_ARG_STR }, \
{ "fs_image", UT_ARG_STR }
/**
* get_fs_type(uts) - Get filesystem type enum from test argument
*
* Reads the fs_type argument and returns the appropriate FS_TYPE_* enum value.
*
* Return: filesystem type enum
*/
static int get_fs_type(struct unit_test_state *uts)
{
const char *fs_type = ut_str(FS_ARG_TYPE);
if (!fs_type)
return FS_TYPE_ANY;
if (!strcmp(fs_type, "ext4"))
return FS_TYPE_EXT;
if (!strcmp(fs_type, "fat"))
return FS_TYPE_FAT;
if (!strcmp(fs_type, "exfat"))
return FS_TYPE_EXFAT;
/* fs_generic uses FS_TYPE_ANY */
return FS_TYPE_ANY;
}
/* Set up the host filesystem block device */
static int set_fs(struct unit_test_state *uts)
{
return fs_set_blk_dev("host", "0:0", get_fs_type(uts));
}
/* Build a path by prepending "/" to the leaf filename, with optional suffix */
static const char *getpath(struct unit_test_state *uts, const char *leaf,
const char *suffix)
{
snprintf(uts->priv, sizeof(uts->priv), "/%s%s", leaf, suffix ?: "");
return uts->priv;
}
/**
* prep_fs() - Prepare filesystem for testing
*
* Binds the fs_image argument as host device 0, sets up the block device,
* and optionally returns a zeroed buffer.
*
* @uts: Unit test state
* @len: Length of buffer to allocate and zero, or 0 for none
* @bufp: Returns pointer to zeroed buffer, or NULL if @len is 0
* Return: 0 on success, negative on error
*/
static int prep_fs(struct unit_test_state *uts, uint len, void **bufp)
{
const char *fs_image = ut_str(FS_ARG_IMAGE);
ut_assertnonnull(fs_image);
ut_assertok(run_commandf("host bind 0 %s", fs_image));
ut_assertok(set_fs(uts));
if (len) {
*bufp = map_sysmem(ADDR, len);
memset(*bufp, '\0', len);
}
return 0;
}
/**
* fs_write_supported(uts) - Check if write is supported for current fs type
*
* Reads the fs_type argument and checks if write support is enabled
* for that filesystem type.
*
* Return: true if write is supported, false otherwise
*/
static bool fs_write_supported(struct unit_test_state *uts)
{
const char *fs_type = ut_str(FS_ARG_TYPE);
if (!fs_type)
return false;
if (!strcmp(fs_type, "ext4"))
return IS_ENABLED(CONFIG_EXT4_WRITE);
if (!strcmp(fs_type, "fat"))
return IS_ENABLED(CONFIG_CMD_FAT_WRITE);
/* fs_generic and exfat use generic write which is always available */
return true;
}
/**
* verify_md5() - Calculate MD5 of buffer and verify against expected
*
* Uses arg 3 (md5val) as the expected MD5 hex string.
*
* @uts: Unit test state
* @buf: Buffer to calculate MD5 of
* @len: Length of buffer
*
* Return: 0 if MD5 matches, -EINVAL otherwise
*/
static int verify_md5(struct unit_test_state *uts, const void *buf, size_t len)
{
u8 digest[MD5_SUM_LEN], expected[MD5_SUM_LEN];
const char *expected_hex = ut_str(3);
ut_assertok(hex2bin(expected, expected_hex, MD5_SUM_LEN));
md5_wd(buf, len, digest, CHUNKSZ_MD5);
ut_asserteq_mem(expected, digest, MD5_SUM_LEN);
return 0;
}
/**
* Test Case 1 - ls command, listing root directory and invalid directory
*/
static int fs_test_ls_norun(struct unit_test_state *uts)
{
const char *small = ut_str(2);
const char *big = ut_str(3);
struct fs_dir_stream *dirs;
struct fs_dirent *dent;
int found_big = 0, found_small = 0, found_subdir = 0;
ut_assertok(prep_fs(uts, 0, NULL));
/* Test listing root directory */
dirs = fs_opendir("/");
ut_assertnonnull(dirs);
while ((dent = fs_readdir(dirs))) {
if (!strcmp(dent->name, big)) {
found_big = 1;
ut_asserteq(FS_DT_REG, dent->type);
} else if (!strcmp(dent->name, small)) {
found_small = 1;
ut_asserteq(FS_DT_REG, dent->type);
} else if (!strcmp(dent->name, "SUBDIR")) {
found_subdir = 1;
ut_asserteq(FS_DT_DIR, dent->type);
}
}
fs_closedir(dirs);
ut_asserteq(1, found_big);
ut_asserteq(1, found_small);
ut_asserteq(1, found_subdir);
/* Test invalid directory returns error */
ut_assertok(set_fs(uts));
dirs = fs_opendir("/invalid_d");
ut_assertnull(dirs);
/* Test file exists */
ut_assertok(set_fs(uts));
ut_asserteq(1, fs_exists(small));
/* Test non-existent file */
ut_assertok(set_fs(uts));
ut_asserteq(0, fs_exists("nonexistent.file"));
return 0;
}
FS_TEST_ARGS(fs_test_ls_norun, UTF_SCAN_FDT | UTF_CONSOLE | UTF_MANUAL,
COMMON_ARGS, { "small", UT_ARG_STR }, { "big", UT_ARG_STR });
/**
* Test Case 2 - size command for small file (1MB)
*/
static int fs_test_size_small_norun(struct unit_test_state *uts)
{
const char *small = ut_str(2);
loff_t size;
ut_assertok(prep_fs(uts, 0, NULL));
ut_assertok(fs_size(getpath(uts, small, NULL), &size));
ut_asserteq(SZ_1M, size);
/* Test size via path with '..' */
ut_assertok(set_fs(uts));
snprintf(uts->priv, sizeof(uts->priv), "/SUBDIR/../%s", small);
ut_assertok(fs_size(uts->priv, &size));
ut_asserteq(SZ_1M, size);
return 0;
}
FS_TEST_ARGS(fs_test_size_small_norun, UTF_SCAN_FDT | UTF_MANUAL,
COMMON_ARGS, { "small", UT_ARG_STR });
/**
* Test Case 3 - size command for large file (2500 MiB)
*/
static int fs_test_size_big_norun(struct unit_test_state *uts)
{
const char *big = ut_str(2);
loff_t size;
ut_assertok(prep_fs(uts, 0, NULL));
ut_assertok(fs_size(getpath(uts, big, NULL), &size));
ut_asserteq_64((loff_t)SZ_1M * 2500, size); /* 2500 MiB = 0x9c400000 */
return 0;
}
FS_TEST_ARGS(fs_test_size_big_norun, UTF_SCAN_FDT | UTF_MANUAL,
COMMON_ARGS, { "big", UT_ARG_STR });
/**
* Test Case 4 - load small file, verify MD5
*/
static int fs_test_load_small_norun(struct unit_test_state *uts)
{
const char *small = ut_str(2);
loff_t actread;
void *buf;
ut_assertok(prep_fs(uts, SZ_1M, &buf));
ut_assertok(fs_legacy_read(getpath(uts, small, NULL), ADDR, 0, 0,
&actread));
ut_asserteq(SZ_1M, actread);
ut_assertok(verify_md5(uts, buf, SZ_1M));
return 0;
}
FS_TEST_ARGS(fs_test_load_small_norun, UTF_SCAN_FDT | UTF_MANUAL,
COMMON_ARGS, { "small", UT_ARG_STR }, { "md5val", UT_ARG_STR });
/**
* Test Case 5 - load first 1MB of big file
*/
static int fs_test_load_big_first_norun(struct unit_test_state *uts)
{
const char *big = ut_str(2);
loff_t actread;
void *buf;
ut_assertok(prep_fs(uts, SZ_1M, &buf));
ut_assertok(fs_legacy_read(getpath(uts, big, NULL), ADDR, 0, SZ_1M,
&actread));
ut_asserteq(SZ_1M, actread);
ut_assertok(verify_md5(uts, buf, SZ_1M));
return 0;
}
FS_TEST_ARGS(fs_test_load_big_first_norun, UTF_SCAN_FDT | UTF_MANUAL,
COMMON_ARGS, { "big", UT_ARG_STR }, { "md5val", UT_ARG_STR });
/**
* Test Case 6 - load last 1MB of big file (offset 0x9c300000)
*/
static int fs_test_load_big_last_norun(struct unit_test_state *uts)
{
const char *big = ut_str(2);
loff_t actread;
void *buf;
ut_assertok(prep_fs(uts, SZ_1M, &buf));
ut_assertok(fs_legacy_read(getpath(uts, big, NULL), ADDR, 0x9c300000ULL,
SZ_1M, &actread));
ut_asserteq(SZ_1M, actread);
ut_assertok(verify_md5(uts, buf, SZ_1M));
return 0;
}
FS_TEST_ARGS(fs_test_load_big_last_norun, UTF_SCAN_FDT | UTF_MANUAL,
COMMON_ARGS, { "big", UT_ARG_STR }, { "md5val", UT_ARG_STR });
/**
* Test Case 7 - load 1MB from last 1MB chunk of 2GB (offset 0x7ff00000)
*/
static int fs_test_load_big_2g_last_norun(struct unit_test_state *uts)
{
const char *big = ut_str(2);
loff_t actread;
void *buf;
ut_assertok(prep_fs(uts, SZ_1M, &buf));
ut_assertok(fs_legacy_read(getpath(uts, big, NULL), ADDR, 0x7ff00000ULL,
SZ_1M, &actread));
ut_asserteq(SZ_1M, actread);
ut_assertok(verify_md5(uts, buf, SZ_1M));
return 0;
}
FS_TEST_ARGS(fs_test_load_big_2g_last_norun, UTF_SCAN_FDT | UTF_MANUAL,
COMMON_ARGS, { "big", UT_ARG_STR }, { "md5val", UT_ARG_STR });
/**
* Test Case 8 - load first 1MB in 2GB region (offset 0x80000000)
*/
static int fs_test_load_big_2g_first_norun(struct unit_test_state *uts)
{
const char *big = ut_str(2);
loff_t actread;
void *buf;
ut_assertok(prep_fs(uts, SZ_1M, &buf));
ut_assertok(fs_legacy_read(getpath(uts, big, NULL), ADDR, 0x80000000ULL,
SZ_1M, &actread));
ut_asserteq(SZ_1M, actread);
ut_assertok(verify_md5(uts, buf, SZ_1M));
return 0;
}
FS_TEST_ARGS(fs_test_load_big_2g_first_norun, UTF_SCAN_FDT | UTF_MANUAL,
COMMON_ARGS, { "big", UT_ARG_STR }, { "md5val", UT_ARG_STR });
/**
* Test Case 9 - load 1MB crossing 2GB boundary (offset 0x7ff80000)
*/
static int fs_test_load_big_2g_cross_norun(struct unit_test_state *uts)
{
const char *big = ut_str(2);
loff_t actread;
void *buf;
ut_assertok(prep_fs(uts, SZ_1M, &buf));
ut_assertok(fs_legacy_read(getpath(uts, big, NULL), ADDR, 0x7ff80000ULL,
SZ_1M, &actread));
ut_asserteq(SZ_1M, actread);
ut_assertok(verify_md5(uts, buf, SZ_1M));
return 0;
}
FS_TEST_ARGS(fs_test_load_big_2g_cross_norun, UTF_SCAN_FDT | UTF_MANUAL,
COMMON_ARGS, { "big", UT_ARG_STR }, { "md5val", UT_ARG_STR });
/**
* Test Case 10 - load beyond file end (2MB from offset where only 1MB remains)
*/
static int fs_test_load_beyond_norun(struct unit_test_state *uts)
{
const char *big = ut_str(2);
loff_t actread;
void *buf;
ut_assertok(prep_fs(uts, SZ_2M, &buf)); /* 2MB buffer */
/* Request 2MB starting at 1MB before EOF - should get 1MB */
ut_assertok(fs_legacy_read(getpath(uts, big, NULL), ADDR, 0x9c300000ULL,
SZ_2M, &actread));
ut_asserteq(SZ_1M, actread); /* Only 1MB available */
return 0;
}
FS_TEST_ARGS(fs_test_load_beyond_norun, UTF_SCAN_FDT | UTF_MANUAL,
COMMON_ARGS, { "big", UT_ARG_STR });
/**
* Test Case 11 - write file
*/
static int fs_test_write_norun(struct unit_test_state *uts)
{
const char *small = ut_str(2);
loff_t actread, actwrite;
void *buf;
if (!fs_write_supported(uts))
return -EAGAIN;
ut_assertok(prep_fs(uts, SZ_1M, &buf));
/* Read small file */
ut_assertok(fs_legacy_read(getpath(uts, small, NULL), ADDR, 0, 0,
&actread));
ut_asserteq(SZ_1M, actread);
/* Write it back with new name */
ut_assertok(set_fs(uts));
ut_assertok(fs_write(getpath(uts, small, ".w"), ADDR, 0, SZ_1M,
&actwrite));
ut_asserteq(SZ_1M, actwrite);
/* Read back and verify MD5 */
ut_assertok(set_fs(uts));
memset(buf, '\0', SZ_1M);
ut_assertok(fs_legacy_read(getpath(uts, small, ".w"), ADDR, 0, 0,
&actread));
ut_asserteq(SZ_1M, actread);
ut_assertok(verify_md5(uts, buf, SZ_1M));
return 0;
}
FS_TEST_ARGS(fs_test_write_norun, UTF_SCAN_FDT | UTF_MANUAL,
COMMON_ARGS, { "small", UT_ARG_STR }, { "md5val", UT_ARG_STR });
/**
* Test Case 12 - write to "." directory (should fail)
*/
static int fs_test_write_dot_norun(struct unit_test_state *uts)
{
loff_t actwrite;
if (!fs_write_supported(uts))
return -EAGAIN;
ut_assertok(prep_fs(uts, 0, NULL));
/* Writing to "." should fail */
ut_assert(fs_write("/.", ADDR, 0, 0x10, &actwrite));
return 0;
}
FS_TEST_ARGS(fs_test_write_dot_norun, UTF_SCAN_FDT | UTF_MANUAL,
COMMON_ARGS);
/**
* Test Case 13 - write via "./" path
*/
static int fs_test_write_dotpath_norun(struct unit_test_state *uts)
{
const char *small = ut_str(2);
loff_t actread, actwrite;
void *buf;
if (!fs_write_supported(uts))
return -EAGAIN;
ut_assertok(prep_fs(uts, SZ_1M, &buf));
/* Read small file */
ut_assertok(fs_legacy_read(getpath(uts, small, NULL), ADDR, 0, 0,
&actread));
ut_asserteq(SZ_1M, actread);
/* Write via "./" path */
ut_assertok(set_fs(uts));
snprintf(uts->priv, sizeof(uts->priv), "/./%s2", small);
ut_assertok(fs_write(uts->priv, ADDR, 0, SZ_1M, &actwrite));
ut_asserteq(SZ_1M, actwrite);
/* Read back via "./" path and verify */
ut_assertok(set_fs(uts));
memset(buf, '\0', SZ_1M);
ut_assertok(fs_legacy_read(uts->priv, ADDR, 0, 0, &actread));
ut_asserteq(SZ_1M, actread);
ut_assertok(verify_md5(uts, buf, SZ_1M));
/* Also verify via normal path */
ut_assertok(set_fs(uts));
memset(buf, '\0', SZ_1M);
ut_assertok(fs_legacy_read(getpath(uts, small, "2"), ADDR, 0, 0,
&actread));
ut_asserteq(SZ_1M, actread);
ut_assertok(verify_md5(uts, buf, SZ_1M));
return 0;
}
FS_TEST_ARGS(fs_test_write_dotpath_norun, UTF_SCAN_FDT | UTF_MANUAL,
COMMON_ARGS, { "small", UT_ARG_STR }, { "md5val", UT_ARG_STR });