[Concept,19/21] test: fs: add C-based filesystem tests

Message ID 20251214175449.3799539-20-sjg@u-boot.org
State New
Headers
Series test: Add support for passing arguments to C unit tests |

Commit Message

Simon Glass Dec. 14, 2025, 5:54 p.m. UTC
  From: Simon Glass <simon.glass@canonical.com>

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>
---

 include/test/fs.h  |  39 ++++
 test/Makefile      |   1 +
 test/cmd_ut.c      |   2 +
 test/fs/Makefile   |   3 +
 test/fs/fs_basic.c | 492 +++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 537 insertions(+)
 create mode 100644 include/test/fs.h
 create mode 100644 test/fs/Makefile
 create mode 100644 test/fs/fs_basic.c
  

Patch

diff --git a/include/test/fs.h b/include/test/fs.h
new file mode 100644
index 00000000000..58fc105a94a
--- /dev/null
+++ b/include/test/fs.h
@@ -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 */
diff --git a/test/Makefile b/test/Makefile
index f7ab9a36b2a..1edd931f3e8 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -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/
diff --git a/test/cmd_ut.c b/test/cmd_ut.c
index 6358f27a64f..8d8a2d763a2 100644
--- a/test/cmd_ut.c
+++ b/test/cmd_ut.c
@@ -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"),
diff --git a/test/fs/Makefile b/test/fs/Makefile
new file mode 100644
index 00000000000..5899be8e667
--- /dev/null
+++ b/test/fs/Makefile
@@ -0,0 +1,3 @@ 
+# SPDX-License-Identifier: GPL-2.0+
+
+obj-y += fs_basic.o
diff --git a/test/fs/fs_basic.c b/test/fs/fs_basic.c
new file mode 100644
index 00000000000..d3b18275fac
--- /dev/null
+++ b/test/fs/fs_basic.c
@@ -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 });