From: "Theodore Ts'o" <tytso@mit.edu>
To: Ext4 Developers List <linux-ext4@vger.kernel.org>
Cc: "Theodore Ts'o" <tytso@mit.edu>
Subject: [PATCH 21/27] ext4: add KUnit tests for fs/ext4/migrate.c
Date: Wed, 11 Nov 2026 23:06:32 -0500 [thread overview]
Message-ID: <20261112040638.1576836-22-tytso@mit.edu> (raw)
In-Reply-To: <20261112040638.1576836-1-tytso@mit.edu>
Add the ext4_migrate_test KUnit suite in fs/ext4/migrate-test.c to test
online inode format migration between indirect block maps and extent
trees in fs/ext4/migrate.c:
- ext4_ext_migrate() precondition guards, fast symlink no-op handling,
and temporary inode allocation error propagation
- direct and single-indirect physically contiguous block coalescing,
sparse logical hole preservation, and indirect metadata freeing
- double-indirect and triple-indirect tree traversal and multi-level
extent tree construction with proper i_csum_seed propagation
- error rollback freeing allocated temporary extent tree blocks while
preserving the original indirect inode layout
- ext4_ext_swap_inode_data() concurrent block allocation detection via
EXT4_STATE_EXT_MIGRATE (-EAGAIN)
- ext4_ind_migrate() conversion of empty and single-extent inodes back
to direct block maps and rejection of unsupported layouts
Export ext4_ext_migrate(), ext4_ind_migrate(), and a KUnit wrapper for
ext4_ext_swap_inode_data() via EXPORT_SYMBOL_FOR_EXT4_TEST.
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
fs/ext4/Makefile | 2 +-
fs/ext4/ext4.h | 4 +
fs/ext4/migrate-test.c | 1064 ++++++++++++++++++++++++++++++++++++++++
fs/ext4/migrate.c | 11 +
4 files changed, 1080 insertions(+), 1 deletion(-)
create mode 100644 fs/ext4/migrate-test.c
diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 6163d47f3627..fafd9600cacb 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -22,7 +22,7 @@ ext4-test-objs += inode-test.o mballoc-test.o \
balloc-test.o orphan-test.o \
block_validity-test.o super-test.o \
sysfs-test.o ioctl-test.o \
- resize-test.o
+ resize-test.o migrate-test.o
obj-$(CONFIG_EXT4_KUNIT_TESTS) += ext4-test.o
ext4-$(CONFIG_FS_VERITY) += verity.o
ext4-$(CONFIG_FS_ENCRYPTION) += crypto.o
diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h
index 3a735c69e2e3..91cc0e00aaa5 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3385,6 +3385,10 @@ bool ext4_dax_compatible_test(struct inode *inode, unsigned int oldflags,
/* migrate.c */
extern int ext4_ext_migrate(struct inode *);
extern int ext4_ind_migrate(struct inode *inode);
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+extern int ext4_ext_swap_inode_data_test(handle_t *handle, struct inode *inode,
+ struct inode *tmp_inode);
+#endif
/* namei.c */
extern int ext4_init_new_dir(handle_t *handle, struct inode *dir,
diff --git a/fs/ext4/migrate-test.c b/fs/ext4/migrate-test.c
new file mode 100644
index 000000000000..e83d208a5117
--- /dev/null
+++ b/fs/ext4/migrate-test.c
@@ -0,0 +1,1064 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for fs/ext4/migrate.c
+ */
+
+#include <kunit/static_stub.h>
+#include <kunit/test.h>
+#include <linux/blkdev.h>
+#include <linux/buffer_head.h>
+#include <linux/dcache.h>
+#include <linux/fs.h>
+#include <linux/fs_context.h>
+#include <linux/fscrypt.h>
+#include <linux/iversion.h>
+#include <linux/kernel.h>
+#include <linux/list.h>
+#include <linux/mm.h>
+#include <linux/module.h>
+#include <linux/part_stat.h>
+#include <linux/percpu.h>
+#include <linux/percpu-rwsem.h>
+#include <linux/percpu_counter.h>
+#include <linux/quotaops.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/timer.h>
+#include <linux/uuid.h>
+#include <linux/workqueue.h>
+
+#include "ext4.h"
+#include "ext4_extents.h"
+#include "ext4_jbd2.h"
+
+#define MIGRATE_TEST_BLK_SIZE 1024U
+#define MIGRATE_TEST_BLK_BITS 10U
+#define MIGRATE_TEST_BLOCKS_PER_GRP 1024U
+#define MIGRATE_TEST_NGROUPS 8U
+#define MIGRATE_TEST_FIRST_DATA_BLK 1U
+#define MIGRATE_TEST_TOTAL_BLKS (MIGRATE_TEST_FIRST_DATA_BLK + \
+ MIGRATE_TEST_NGROUPS * \
+ MIGRATE_TEST_BLOCKS_PER_GRP)
+#define MIGRATE_TEST_INODE_SIZE 256U
+#define MIGRATE_TEST_EXTRA_ISIZE 32U
+#define MIGRATE_TEST_INODES_PER_GRP 32U
+#define MIGRATE_MAX_FREED_REC 32U
+
+struct migrate_mock_blk {
+ struct list_head node;
+ ext4_fsblk_t blocknr;
+ struct buffer_head bh;
+ u8 *data;
+};
+
+struct migrate_test_ctx {
+ struct super_block *sb;
+ struct ext4_sb_info sbi;
+ struct ext4_super_block *es;
+ struct blockgroup_lock bgl;
+ struct gendisk bdev_disk;
+ struct block_device bdev;
+ struct request_queue bdev_queue;
+ struct inode bd_inode;
+ struct folio *bh_folio;
+ struct list_head blks;
+ ext4_fsblk_t device_blocks;
+ ext4_fsblk_t next_alloc_pblk;
+ int new_inode_err;
+ ext4_fsblk_t freed_blks[MIGRATE_MAX_FREED_REC];
+ unsigned int freed_cnt;
+ bool rwsem_inited;
+ bool es_shrinker_inited;
+ bool counters_inited;
+};
+
+static struct kunit *active_migrate_test;
+
+static struct inode *migrate_test_alloc_inode(struct super_block *sb)
+{
+ struct ext4_inode_info *ei;
+
+ ei = kzalloc_obj(*ei);
+ if (!ei)
+ return NULL;
+
+ init_rwsem(&ei->i_data_sem);
+ inode_init_once(&ei->vfs_inode);
+ inode_set_iversion(&ei->vfs_inode, 1);
+ ei->i_prealloc_node = RB_ROOT;
+ rwlock_init(&ei->i_prealloc_lock);
+ ext4_es_init_tree(&ei->i_es_tree);
+ rwlock_init(&ei->i_es_lock);
+ INIT_LIST_HEAD(&ei->i_es_list);
+ spin_lock_init(&ei->i_raw_lock);
+ spin_lock_init(&ei->i_block_reservation_lock);
+ ext4_init_pending_tree(&ei->i_pending_tree);
+ init_rwsem(&ei->xattr_sem);
+ INIT_LIST_HEAD(&ei->i_rsv_conversion_list);
+ spin_lock_init(&ei->i_completed_io_lock);
+ INIT_LIST_HEAD(&ei->i_orphan);
+ ext4_fc_init_inode(&ei->vfs_inode);
+ spin_lock_init(&ei->i_fc_lock);
+ ei->i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+ ei->i_extra_isize = MIGRATE_TEST_EXTRA_ISIZE;
+ ei->i_last_alloc_group = ~0U;
+
+ return &ei->vfs_inode;
+}
+
+static void migrate_test_free_inode(struct inode *inode)
+{
+ ext4_es_remove_extent(inode, 0, EXT_MAX_BLOCKS);
+ kfree(EXT4_I(inode));
+}
+
+static void migrate_test_evict_inode(struct inode *inode)
+{
+ truncate_inode_pages_final(&inode->i_data);
+ clear_inode(inode);
+}
+
+#ifdef CONFIG_QUOTA
+static struct dquot __rcu **migrate_test_get_dquots(struct inode *inode)
+{
+ return EXT4_I(inode)->i_dquot;
+}
+#endif
+
+static const struct super_operations migrate_test_sops = {
+ .alloc_inode = migrate_test_alloc_inode,
+ .free_inode = migrate_test_free_inode,
+ .evict_inode = migrate_test_evict_inode,
+#ifdef CONFIG_QUOTA
+ .get_dquots = migrate_test_get_dquots,
+#endif
+};
+
+static int migrate_test_init_fs_context(struct fs_context *fc)
+{
+ return 0;
+}
+
+static void migrate_test_sb_upd_work(struct work_struct *work)
+{
+}
+
+static void migrate_test_err_timer(struct timer_list *t)
+{
+}
+
+static struct file_system_type migrate_test_fs_type = {
+ .name = "ext4_migrate_kunit",
+ .init_fs_context = migrate_test_init_fs_context,
+ .kill_sb = kill_anon_super,
+};
+
+static void migrate_init_mock_bh(struct buffer_head *bh, struct folio *folio,
+ u8 *data, size_t size, sector_t blocknr)
+{
+ memset(bh, 0, sizeof(*bh));
+ bh->b_folio = folio;
+ bh->b_data = (char *)data;
+ bh->b_size = size;
+ bh->b_blocknr = blocknr;
+ atomic_set(&bh->b_count, 2);
+ set_buffer_uptodate(bh);
+ set_buffer_mapped(bh);
+}
+
+static struct buffer_head *migrate_lookup_bh(struct migrate_test_ctx *ctx,
+ ext4_fsblk_t blocknr)
+{
+ struct migrate_mock_blk *blk;
+
+ list_for_each_entry(blk, &ctx->blks, node) {
+ if (blk->blocknr == blocknr)
+ return &blk->bh;
+ }
+ return NULL;
+}
+
+static struct buffer_head *migrate_get_or_create_bh(struct migrate_test_ctx *ctx,
+ ext4_fsblk_t blocknr)
+{
+ struct buffer_head *bh;
+ struct migrate_mock_blk *blk;
+
+ bh = migrate_lookup_bh(ctx, blocknr);
+ if (bh) {
+ get_bh(bh);
+ return bh;
+ }
+
+ blk = kzalloc_obj(*blk);
+ if (!blk)
+ return NULL;
+ blk->data = kzalloc(MIGRATE_TEST_BLK_SIZE, GFP_KERNEL);
+ if (!blk->data) {
+ kfree(blk);
+ return NULL;
+ }
+ blk->blocknr = blocknr;
+ migrate_init_mock_bh(&blk->bh, ctx->bh_folio, blk->data,
+ MIGRATE_TEST_BLK_SIZE, blocknr);
+ list_add_tail(&blk->node, &ctx->blks);
+ get_bh(&blk->bh);
+ return &blk->bh;
+}
+
+static struct buffer_head *migrate_sb_bread_stub(struct super_block *sb,
+ sector_t block,
+ blk_opf_t op_flags)
+{
+ struct kunit *test = active_migrate_test;
+ struct migrate_test_ctx *ctx;
+ struct buffer_head *bh;
+
+ if (!test)
+ return ERR_PTR(-EIO);
+ ctx = test->priv;
+ if (!ctx || sb != ctx->sb)
+ return ERR_PTR(-EIO);
+ if ((ext4_fsblk_t)block >= ctx->device_blocks)
+ return ERR_PTR(-EIO);
+
+ bh = migrate_get_or_create_bh(ctx, block);
+ if (!bh)
+ return ERR_PTR(-ENOMEM);
+ return bh;
+}
+
+static struct buffer_head *migrate_ext_getblk_stub(struct inode *inode,
+ ext4_fsblk_t pblk,
+ gfp_t gfp_flags)
+{
+ struct kunit *test = active_migrate_test;
+ struct migrate_test_ctx *ctx;
+
+ if (!test)
+ return NULL;
+ ctx = test->priv;
+ if (!ctx)
+ return NULL;
+
+ return migrate_get_or_create_bh(ctx, pblk);
+}
+
+static int migrate_ext_dirty_stub(const char *where, unsigned int line,
+ handle_t *handle, struct inode *inode,
+ struct ext4_ext_path *path)
+{
+ if (path && path->p_bh) {
+ ext4_extent_block_csum_set_test(inode,
+ ext_block_hdr(path->p_bh));
+ set_buffer_verified(path->p_bh);
+ }
+ return 0;
+}
+
+static int migrate_handle_dirty_metadata_stub(const char *where,
+ unsigned int line,
+ handle_t *handle,
+ struct inode *inode,
+ struct buffer_head *bh)
+{
+ if (bh)
+ set_buffer_uptodate(bh);
+ return 0;
+}
+
+static int migrate_mark_inode_dirty_stub(handle_t *handle, struct inode *inode,
+ const char *func, unsigned int line)
+{
+ return 0;
+}
+
+static struct inode *migrate_new_inode_stub(struct mnt_idmap *idmap,
+ handle_t *handle,
+ struct inode *dir, umode_t mode,
+ const struct qstr *qstr,
+ __u32 goal, uid_t *owner,
+ __u32 i_flags, int handle_type,
+ unsigned int line_no, int nblocks)
+{
+ struct kunit *test = active_migrate_test;
+ struct migrate_test_ctx *ctx;
+ struct inode *inode;
+
+ if (!test)
+ return ERR_PTR(-EIO);
+ ctx = test->priv;
+ if (!ctx)
+ return ERR_PTR(-EIO);
+ if (ctx->new_inode_err)
+ return ERR_PTR(ctx->new_inode_err);
+
+ inode = new_inode(dir->i_sb);
+ if (!inode)
+ return ERR_PTR(-ENOMEM);
+
+ inode->i_ino = 99;
+ inode->i_mode = mode;
+ inode->i_blkbits = dir->i_sb->s_blocksize_bits;
+ inode->i_flags |= S_NOQUOTA;
+ EXT4_I(inode)->i_flags = i_flags;
+ EXT4_I(inode)->i_csum_seed = 0xabcdef01U;
+ if (owner) {
+ i_uid_write(inode, owner[0]);
+ i_gid_write(inode, owner[1]);
+ }
+ inode_state_set_raw(inode, I_NEW);
+ return inode;
+}
+
+static ext4_fsblk_t migrate_mb_new_blocks_stub(handle_t *handle,
+ struct ext4_allocation_request *ar,
+ int *errp)
+{
+ struct kunit *test = active_migrate_test;
+ struct migrate_test_ctx *ctx;
+ ext4_fsblk_t pblk;
+
+ if (!test) {
+ *errp = -EIO;
+ return 0;
+ }
+ ctx = test->priv;
+ pblk = ctx->next_alloc_pblk;
+ ctx->next_alloc_pblk += ar->len;
+ ar->inode->i_blocks += (blkcnt_t)ar->len <<
+ (ar->inode->i_blkbits - 9);
+ *errp = 0;
+ return pblk;
+}
+
+static void migrate_free_blocks_stub(handle_t *handle, struct inode *inode,
+ struct buffer_head *bh, ext4_fsblk_t block,
+ unsigned long count, int flags)
+{
+ struct kunit *test = active_migrate_test;
+ struct migrate_test_ctx *ctx;
+ blkcnt_t sectors;
+ unsigned long i;
+
+ if (!test)
+ return;
+ ctx = test->priv;
+ for (i = 0; i < count && ctx->freed_cnt < MIGRATE_MAX_FREED_REC; i++)
+ ctx->freed_blks[ctx->freed_cnt++] = block + i;
+
+ sectors = (blkcnt_t)count << (inode->i_blkbits - 9);
+ if (inode->i_blocks >= sectors)
+ inode->i_blocks -= sectors;
+ if (bh)
+ brelse(bh);
+}
+
+static bool migrate_was_freed(struct migrate_test_ctx *ctx, ext4_fsblk_t blk)
+{
+ unsigned int i;
+
+ for (i = 0; i < ctx->freed_cnt; i++) {
+ if (ctx->freed_blks[i] == blk)
+ return true;
+ }
+ return false;
+}
+
+static struct inode *migrate_make_test_inode(struct kunit *test, unsigned long ino,
+ umode_t mode)
+{
+ struct migrate_test_ctx *ctx = test->priv;
+ struct inode *inode;
+
+ inode = new_inode(ctx->sb);
+ KUNIT_ASSERT_NOT_NULL(test, inode);
+ inode->i_ino = ino;
+ inode->i_mode = mode;
+ inode->i_blkbits = MIGRATE_TEST_BLK_BITS;
+ inode->i_flags |= S_NOQUOTA;
+ EXT4_I(inode)->i_csum_seed = 0x55aa55aaU;
+ return inode;
+}
+
+static void migrate_test_cleanup(struct migrate_test_ctx *ctx)
+{
+ struct super_block *sb = ctx->sb;
+ struct ext4_sb_info *sbi = &ctx->sbi;
+ struct migrate_mock_blk *blk, *tmp;
+
+ if (sb) {
+ cancel_work_sync(&sbi->s_sb_upd_work);
+ timer_delete_sync(&sbi->s_err_report);
+ }
+
+ if (ctx->es_shrinker_inited) {
+ ext4_es_unregister_shrinker(sbi);
+ ctx->es_shrinker_inited = false;
+ }
+
+ if (ctx->counters_inited) {
+ percpu_counter_destroy(&sbi->s_freeclusters_counter);
+ percpu_counter_destroy(&sbi->s_dirtyclusters_counter);
+ percpu_counter_destroy(&sbi->s_freeinodes_counter);
+ ctx->counters_inited = false;
+ }
+
+ if (ctx->rwsem_inited) {
+ percpu_free_rwsem(&sbi->s_writepages_rwsem);
+ ctx->rwsem_inited = false;
+ }
+
+ if (sb) {
+ deactivate_super(sb);
+ ctx->sb = NULL;
+ }
+
+ list_for_each_entry_safe(blk, tmp, &ctx->blks, node) {
+ list_del(&blk->node);
+ kfree(blk->data);
+ kfree(blk);
+ }
+
+ if (ctx->bh_folio) {
+ folio_put(ctx->bh_folio);
+ ctx->bh_folio = NULL;
+ }
+
+ if (ctx->bdev.bd_stats) {
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ }
+
+ active_migrate_test = NULL;
+}
+
+KUNIT_DEFINE_ACTION_WRAPPER(migrate_test_cleanup_action, migrate_test_cleanup,
+ struct migrate_test_ctx *);
+
+static int migrate_test_set_super(struct super_block *sb, struct fs_context *fc)
+{
+ return set_anon_super_fc(sb, fc);
+}
+
+static int migrate_test_init(struct kunit *test)
+{
+ struct migrate_test_ctx *ctx;
+ struct fs_context *fc;
+ struct super_block *sb;
+ struct ext4_sb_info *sbi;
+ struct buffer_head *sbh;
+ struct inode *root_inode;
+ int ret;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+ INIT_LIST_HEAD(&ctx->blks);
+ ctx->device_blocks = MIGRATE_TEST_TOTAL_BLKS;
+ ctx->next_alloc_pblk = 900;
+ test->priv = ctx;
+
+ ctx->bdev.bd_stats = alloc_percpu(struct disk_stats);
+ KUNIT_ASSERT_NOT_NULL(test, ctx->bdev.bd_stats);
+
+ ctx->bh_folio = folio_alloc(GFP_KERNEL, 0);
+ if (!ctx->bh_folio) {
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ KUNIT_FAIL_AND_ABORT(test, "folio_alloc failed");
+ }
+
+ fc = fs_context_for_mount(&migrate_test_fs_type, 0);
+ if (IS_ERR(fc)) {
+ folio_put(ctx->bh_folio);
+ ctx->bh_folio = NULL;
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ KUNIT_FAIL_AND_ABORT(test, "fs_context_for_mount failed: %ld",
+ PTR_ERR(fc));
+ }
+ sb = sget_fc(fc, NULL, migrate_test_set_super);
+ put_fs_context(fc);
+ if (IS_ERR(sb)) {
+ folio_put(ctx->bh_folio);
+ ctx->bh_folio = NULL;
+ free_percpu(ctx->bdev.bd_stats);
+ ctx->bdev.bd_stats = NULL;
+ KUNIT_FAIL_AND_ABORT(test, "sget_fc failed: %ld", PTR_ERR(sb));
+ }
+ ctx->sb = sb;
+ up_write(&sb->s_umount);
+
+ ret = kunit_add_action_or_reset(test, migrate_test_cleanup_action, ctx);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+
+ sbi = &ctx->sbi;
+ sb->s_fs_info = sbi;
+ sb->s_op = &migrate_test_sops;
+ sb->s_flags |= SB_ACTIVE;
+ sb->s_blocksize = MIGRATE_TEST_BLK_SIZE;
+ sb->s_blocksize_bits = MIGRATE_TEST_BLK_BITS;
+ sb->s_magic = EXT4_SUPER_MAGIC;
+ sb->s_maxbytes = MAX_LFS_FILESIZE;
+ INIT_LIST_HEAD(&sb->s_inodes);
+#ifdef CONFIG_FS_ENCRYPTION
+ sb->s_cop = &ext4_cryptops;
+#endif
+
+ strscpy(sb->s_id, "ext4_migrate_kunit", sizeof(sb->s_id));
+ inode_init_once(&ctx->bd_inode);
+ ctx->bd_inode.i_sb = sb;
+ ctx->bd_inode.i_blkbits = MIGRATE_TEST_BLK_BITS;
+ ctx->bd_inode.i_mapping = &ctx->bd_inode.i_data;
+ ctx->bd_inode.i_data.host = &ctx->bd_inode;
+ ctx->bdev_disk.part0 = &ctx->bdev;
+ ctx->bdev_disk.queue = &ctx->bdev_queue;
+ ctx->bdev.bd_disk = &ctx->bdev_disk;
+ ctx->bdev.bd_queue = &ctx->bdev_queue;
+ ctx->bdev.bd_mapping = &ctx->bd_inode.i_data;
+ bdev_clear_flag(&ctx->bdev, BD_READ_ONLY);
+ sb->s_bdev = &ctx->bdev;
+
+ sbi->s_sb = sb;
+ sbi->s_blockgroup_lock = &ctx->bgl;
+ bgl_lock_init(&ctx->bgl);
+ INIT_WORK(&sbi->s_sb_upd_work, migrate_test_sb_upd_work);
+ timer_setup(&sbi->s_err_report, migrate_test_err_timer, 0);
+ spin_lock_init(&sbi->s_error_lock);
+ spin_lock_init(&sbi->s_md_lock);
+ spin_lock_init(&sbi->s_bdev_wb_lock);
+ spin_lock_init(&sbi->s_es_lock);
+ INIT_LIST_HEAD(&sbi->s_es_list);
+ mutex_init(&sbi->s_fc_lock);
+ INIT_LIST_HEAD(&sbi->s_fc_q[FC_Q_MAIN]);
+ INIT_LIST_HEAD(&sbi->s_fc_q[FC_Q_STAGING]);
+ INIT_LIST_HEAD(&sbi->s_fc_dentry_q[FC_Q_MAIN]);
+ INIT_LIST_HEAD(&sbi->s_fc_dentry_q[FC_Q_STAGING]);
+ ratelimit_state_init(&sbi->s_err_ratelimit_state, 5 * HZ, 10);
+ ratelimit_state_init(&sbi->s_warning_ratelimit_state, 5 * HZ, 10);
+ ratelimit_state_init(&sbi->s_msg_ratelimit_state, 5 * HZ, 10);
+
+ ret = percpu_init_rwsem(&sbi->s_writepages_rwsem);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ctx->rwsem_inited = true;
+
+ ret = percpu_counter_init(&sbi->s_freeclusters_counter, 4096,
+ GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = percpu_counter_init(&sbi->s_dirtyclusters_counter, 0, GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ret = percpu_counter_init(&sbi->s_freeinodes_counter, 200, GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ctx->counters_inited = true;
+
+ ret = ext4_es_register_shrinker(sbi);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ ctx->es_shrinker_inited = true;
+
+ sbi->s_blocks_per_group = MIGRATE_TEST_BLOCKS_PER_GRP;
+ sbi->s_clusters_per_group = MIGRATE_TEST_BLOCKS_PER_GRP;
+ sbi->s_cluster_ratio = 1;
+ sbi->s_cluster_bits = 0;
+ sbi->s_groups_count = MIGRATE_TEST_NGROUPS;
+ sbi->s_inodes_per_group = MIGRATE_TEST_INODES_PER_GRP;
+ sbi->s_inode_size = MIGRATE_TEST_INODE_SIZE;
+ sbi->s_inodes_per_block = MIGRATE_TEST_BLK_SIZE /
+ MIGRATE_TEST_INODE_SIZE;
+ sbi->s_first_ino = 11;
+ sbi->s_mount_state = EXT4_VALID_FS;
+ set_opt(sb, ERRORS_CONT);
+
+ sbh = migrate_get_or_create_bh(ctx, MIGRATE_TEST_FIRST_DATA_BLK);
+ KUNIT_ASSERT_NOT_NULL(test, sbh);
+ brelse(sbh);
+ sbi->s_sbh = sbh;
+ ctx->es = (struct ext4_super_block *)sbh->b_data;
+ sbi->s_es = ctx->es;
+
+ ctx->es->s_magic = cpu_to_le16(EXT4_SUPER_MAGIC);
+ ctx->es->s_rev_level = cpu_to_le32(EXT4_DYNAMIC_REV);
+ ctx->es->s_first_data_block = cpu_to_le32(MIGRATE_TEST_FIRST_DATA_BLK);
+ ctx->es->s_blocks_per_group = cpu_to_le32(MIGRATE_TEST_BLOCKS_PER_GRP);
+ ctx->es->s_clusters_per_group =
+ cpu_to_le32(MIGRATE_TEST_BLOCKS_PER_GRP);
+ ctx->es->s_inodes_per_group = cpu_to_le32(MIGRATE_TEST_INODES_PER_GRP);
+ ctx->es->s_inodes_count = cpu_to_le32(MIGRATE_TEST_NGROUPS *
+ MIGRATE_TEST_INODES_PER_GRP);
+ ctx->es->s_inode_size = cpu_to_le16(MIGRATE_TEST_INODE_SIZE);
+ ctx->es->s_checksum_type = EXT4_CRC32C_CHKSUM;
+ generate_random_uuid(ctx->es->s_uuid);
+ ext4_blocks_count_set(ctx->es, MIGRATE_TEST_TOTAL_BLKS);
+ ext4_set_feature_extents(sb);
+ ext4_set_feature_64bit(sb);
+ ext4_set_feature_metadata_csum(sb);
+ sbi->s_csum_seed = ext4_chksum(~0, ctx->es->s_uuid,
+ sizeof(ctx->es->s_uuid));
+ ext4_superblock_csum_set(sb);
+
+ root_inode = migrate_make_test_inode(test, EXT4_ROOT_INO,
+ S_IFDIR | 0755);
+ sb->s_root = d_make_root(root_inode);
+ KUNIT_ASSERT_NOT_NULL(test, sb->s_root);
+
+ active_migrate_test = test;
+ kunit_activate_static_stub(test, ext4_sb_bread, migrate_sb_bread_stub);
+ kunit_activate_static_stub(test, ext4_ext_getblk,
+ migrate_ext_getblk_stub);
+ kunit_activate_static_stub(test, __ext4_ext_dirty,
+ migrate_ext_dirty_stub);
+ kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+ migrate_handle_dirty_metadata_stub);
+ kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+ migrate_mark_inode_dirty_stub);
+ kunit_activate_static_stub(test, __ext4_new_inode,
+ migrate_new_inode_stub);
+ kunit_activate_static_stub(test, ext4_mb_new_blocks,
+ migrate_mb_new_blocks_stub);
+ kunit_activate_static_stub(test, ext4_free_blocks,
+ migrate_free_blocks_stub);
+
+ return 0;
+}
+
+/*
+ * Test 1: ext4_ext_migrate() precondition guards, fast symlink no-op, and
+ * temporary inode allocation failure handling.
+ */
+static void test_ext_migrate_precondition_checks_and_fast_symlink(struct kunit *test)
+{
+ struct migrate_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct inode *inode, *symlink;
+
+ inode = migrate_make_test_inode(test, 12, S_IFREG | 0644);
+
+ /* 1. Filesystem lacks extents feature -> -EINVAL */
+ ext4_clear_feature_extents(sb);
+ KUNIT_EXPECT_EQ(test, ext4_ext_migrate(inode), -EINVAL);
+ ext4_set_feature_extents(sb);
+
+ /* 2. Inode already uses extents -> -EINVAL */
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ KUNIT_EXPECT_EQ(test, ext4_ext_migrate(inode), -EINVAL);
+ ext4_clear_inode_flag(inode, EXT4_INODE_EXTENTS);
+
+ /* 3. Inline-data inode -> -EINVAL */
+ ext4_set_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ EXT4_I(inode)->i_inline_off = 164;
+ KUNIT_EXPECT_EQ(test, ext4_ext_migrate(inode), -EINVAL);
+ ext4_clear_inode_flag(inode, EXT4_INODE_INLINE_DATA);
+ EXT4_I(inode)->i_inline_off = 0;
+
+ /* 4. Fast symlink (S_IFLNK && i_blocks == 0) is a clean no-op */
+ symlink = migrate_make_test_inode(test, 13, S_IFLNK | 0777);
+ symlink->i_blocks = 0;
+ memcpy(EXT4_I(symlink)->i_data, "target/path", 11);
+ KUNIT_EXPECT_EQ(test, ext4_ext_migrate(symlink), 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_flag(symlink, EXT4_INODE_EXTENTS));
+ KUNIT_EXPECT_MEMEQ(test, EXT4_I(symlink)->i_data, "target/path", 11);
+ iput(symlink);
+
+ /* 5. Temporary inode allocation failure (-ENOSPC) propagates */
+ ctx->new_inode_err = -ENOSPC;
+ KUNIT_EXPECT_EQ(test, ext4_ext_migrate(inode), -ENOSPC);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ ctx->new_inode_err = 0;
+
+ iput(inode);
+}
+
+/*
+ * Test 2: ext4_ext_migrate() coalesces contiguous direct + single-indirect
+ * runs across the EXT4_NDIR_BLOCKS boundary, preserves sparse holes, and
+ * frees the indirect metadata block.
+ */
+static void test_ext_migrate_direct_and_ind_coalescing_and_holes(struct kunit *test)
+{
+ struct migrate_test_ctx *ctx = test->priv;
+ struct inode *inode;
+ struct buffer_head *ind_bh;
+ __le32 *ind_data;
+ struct ext4_extent_header *eh;
+ struct ext4_extent *ex;
+
+ inode = migrate_make_test_inode(test, 14, S_IFREG | 0644);
+ i_size_write(inode, 25 * MIGRATE_TEST_BLK_SIZE);
+ inode->i_blocks = 8 * (MIGRATE_TEST_BLK_SIZE >> 9);
+
+ /* Direct [0..2] -> 100..102; hole [3..10]; Direct [11] -> 200 */
+ EXT4_I(inode)->i_data[0] = cpu_to_le32(100);
+ EXT4_I(inode)->i_data[1] = cpu_to_le32(101);
+ EXT4_I(inode)->i_data[2] = cpu_to_le32(102);
+ EXT4_I(inode)->i_data[11] = cpu_to_le32(200);
+
+ /* IND block 500: [0..1] (lblks 12..13) -> 201..202; [10] (lblk 22) -> 350 */
+ EXT4_I(inode)->i_data[EXT4_IND_BLOCK] = cpu_to_le32(500);
+ ind_bh = migrate_get_or_create_bh(ctx, 500);
+ KUNIT_ASSERT_NOT_NULL(test, ind_bh);
+ ind_data = (__le32 *)ind_bh->b_data;
+ ind_data[0] = cpu_to_le32(201);
+ ind_data[1] = cpu_to_le32(202);
+ ind_data[10] = cpu_to_le32(350);
+ brelse(ind_bh);
+
+ KUNIT_EXPECT_EQ(test, ext4_ext_migrate(inode), 0);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+
+ eh = ext_inode_hdr(inode);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_magic), EXT4_EXT_MAGIC);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(eh->eh_depth), 0U);
+ KUNIT_ASSERT_EQ(test, le16_to_cpu(eh->eh_entries), 3U);
+
+ ex = EXT_FIRST_EXTENT(eh);
+ /* Extent 0: lblk [0..2] -> pblk 100 */
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[0].ee_block), 0U);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(ex[0].ee_len), 3U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex[0]), 100ULL);
+
+ /* Extent 1: lblk [11..13] coalesced across DIRECT/IND -> pblk 200 */
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[1].ee_block), 11U);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(ex[1].ee_len), 3U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex[1]), 200ULL);
+
+ /* Extent 2: lblk [22..22] -> pblk 350 */
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(ex[2].ee_block), 22U);
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(ex[2].ee_len), 1U);
+ KUNIT_EXPECT_EQ(test, ext4_ext_pblock(&ex[2]), 350ULL);
+
+ /* Indirect metadata block 500 was freed */
+ KUNIT_EXPECT_TRUE(test, migrate_was_freed(ctx, 500));
+
+ iput(inode);
+}
+
+/*
+ * Test 3: ext4_ext_migrate() across DIRECT, IND, DIND, and TIND sparse blocks
+ * (> 4 discontiguous extents, growing a depth-1 extent tree) and freeing all
+ * indirect metadata blocks at every level.
+ */
+static void test_ext_migrate_dind_tind_and_multilevel_tree(struct kunit *test)
+{
+ struct migrate_test_ctx *ctx = test->priv;
+ struct inode *inode;
+ struct buffer_head *bh;
+ struct ext4_ext_path *path;
+ static const struct {
+ ext4_lblk_t lblk;
+ ext4_fsblk_t pblk;
+ } expected[] = {
+ { 0, 100 },
+ { 5, 110 },
+ { 15, 120 },
+ { 780, 130 },
+ { 131341, 140 },
+ { 131345, 150 },
+ };
+ static const ext4_fsblk_t expected_freed_meta[] = {
+ 510, 520, 521, 530, 531, 532
+ };
+ unsigned int i;
+
+ inode = migrate_make_test_inode(test, 15, S_IFREG | 0644);
+ i_size_write(inode, 140000ULL * MIGRATE_TEST_BLK_SIZE);
+ inode->i_blocks = 12 * (MIGRATE_TEST_BLK_SIZE >> 9);
+
+ /* Direct: lblk 0 -> 100, lblk 5 -> 110 */
+ EXT4_I(inode)->i_data[0] = cpu_to_le32(100);
+ EXT4_I(inode)->i_data[5] = cpu_to_le32(110);
+
+ /* IND (510): entry 3 (lblk 12 + 3 = 15) -> 120 */
+ EXT4_I(inode)->i_data[EXT4_IND_BLOCK] = cpu_to_le32(510);
+ bh = migrate_get_or_create_bh(ctx, 510);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ ((__le32 *)bh->b_data)[3] = cpu_to_le32(120);
+ brelse(bh);
+
+ /* DIND (520) -> [2] = 521 -> [0] (lblk 12 + 256 + 512 = 780) -> 130 */
+ EXT4_I(inode)->i_data[EXT4_DIND_BLOCK] = cpu_to_le32(520);
+ bh = migrate_get_or_create_bh(ctx, 520);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ ((__le32 *)bh->b_data)[2] = cpu_to_le32(521);
+ brelse(bh);
+
+ bh = migrate_get_or_create_bh(ctx, 521);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ ((__le32 *)bh->b_data)[0] = cpu_to_le32(130);
+ brelse(bh);
+
+ /*
+ * TIND (530) -> [1] = 531 -> [0] = 532 -> [1] (lblk 12 + 256 + 65536 +
+ * 65536 + 1 = 131341 -> 140) and [5] (lblk 131345 -> 150).
+ */
+ EXT4_I(inode)->i_data[EXT4_TIND_BLOCK] = cpu_to_le32(530);
+ bh = migrate_get_or_create_bh(ctx, 530);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ ((__le32 *)bh->b_data)[1] = cpu_to_le32(531);
+ brelse(bh);
+
+ bh = migrate_get_or_create_bh(ctx, 531);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ ((__le32 *)bh->b_data)[0] = cpu_to_le32(532);
+ brelse(bh);
+
+ bh = migrate_get_or_create_bh(ctx, 532);
+ KUNIT_ASSERT_NOT_NULL(test, bh);
+ ((__le32 *)bh->b_data)[1] = cpu_to_le32(140);
+ ((__le32 *)bh->b_data)[5] = cpu_to_le32(150);
+ brelse(bh);
+
+ KUNIT_EXPECT_EQ(test, ext4_ext_migrate(inode), 0);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ KUNIT_EXPECT_EQ(test, le16_to_cpu(ext_inode_hdr(inode)->eh_depth), 1U);
+
+ for (i = 0; i < ARRAY_SIZE(expected); i++) {
+ path = ext4_find_extent(inode, expected[i].lblk, NULL, 0);
+ KUNIT_ASSERT_FALSE_MSG(test, IS_ERR(path),
+ "find_extent failed at idx %u", i);
+ KUNIT_ASSERT_NOT_NULL(test, path[path->p_depth].p_ext);
+ KUNIT_EXPECT_EQ_MSG(test,
+ le32_to_cpu(path[path->p_depth].p_ext->ee_block),
+ expected[i].lblk, "lblk idx %u", i);
+ KUNIT_EXPECT_EQ_MSG(test,
+ ext4_ext_pblock(path[path->p_depth].p_ext),
+ expected[i].pblk, "pblk idx %u", i);
+ ext4_free_ext_path(path);
+ }
+
+ for (i = 0; i < ARRAY_SIZE(expected_freed_meta); i++) {
+ KUNIT_EXPECT_TRUE_MSG(test,
+ migrate_was_freed(ctx,
+ expected_freed_meta[i]),
+ "expected meta block %llu freed",
+ expected_freed_meta[i]);
+ }
+
+ iput(inode);
+}
+
+/*
+ * Test 4: When reading an indirect block fails midway after tmp_inode has
+ * already grown a depth-1 extent tree, free_ext_block() / free_ext_idx()
+ * frees the allocated temporary extent metadata block and leaves the original
+ * inode untouched.
+ */
+static void test_ext_migrate_error_rollback_frees_tmp_extent_blocks(struct kunit *test)
+{
+ struct migrate_test_ctx *ctx = test->priv;
+ struct inode *inode;
+ ext4_fsblk_t expected_leaf_blk = ctx->next_alloc_pblk;
+
+ inode = migrate_make_test_inode(test, 16, S_IFREG | 0644);
+ inode->i_blocks = 12;
+
+ /*
+ * 6 discontiguous direct blocks flush 5 extents into tmp_inode
+ * (the 6th stays buffered in lb), growing tmp_inode past eh_max=4
+ * to a depth-1 extent leaf block before EXT4_IND_BLOCK is read.
+ */
+ EXT4_I(inode)->i_data[0] = cpu_to_le32(100);
+ EXT4_I(inode)->i_data[2] = cpu_to_le32(110);
+ EXT4_I(inode)->i_data[4] = cpu_to_le32(120);
+ EXT4_I(inode)->i_data[6] = cpu_to_le32(130);
+ EXT4_I(inode)->i_data[8] = cpu_to_le32(140);
+ EXT4_I(inode)->i_data[10] = cpu_to_le32(150);
+ /* Unreadable IND block beyond device_blocks triggers -EIO rollback */
+ EXT4_I(inode)->i_data[EXT4_IND_BLOCK] =
+ cpu_to_le32(MIGRATE_TEST_TOTAL_BLKS + 50);
+
+ (void)ext4_ext_migrate(inode);
+
+ /* Temporary extent leaf block allocated during build must be freed */
+ KUNIT_EXPECT_TRUE(test, migrate_was_freed(ctx, expected_leaf_blk));
+ /* Original inode remains indirect with intact direct pointers */
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT4_I(inode)->i_data[0]), 100U);
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT4_I(inode)->i_data[8]), 140U);
+
+ iput(inode);
+}
+
+/*
+ * Test 5: ext4_ext_swap_inode_data() returns -EAGAIN without modifying the
+ * target inode or freeing its indirect blocks if EXT4_STATE_EXT_MIGRATE was
+ * cleared by a concurrent allocation.
+ */
+static void test_ext_swap_inode_data_detects_concurrent_allocation(struct kunit *test)
+{
+ struct migrate_test_ctx *ctx = test->priv;
+ struct inode *inode, *tmp_inode;
+ handle_t *handle;
+ int ret;
+
+ inode = migrate_make_test_inode(test, 17, S_IFREG | 0644);
+ EXT4_I(inode)->i_data[0] = cpu_to_le32(210);
+ EXT4_I(inode)->i_data[EXT4_IND_BLOCK] = cpu_to_le32(550);
+
+ tmp_inode = migrate_make_test_inode(test, 18, S_IFREG | 0644);
+ handle = ext4_journal_start(inode, EXT4_HT_MIGRATE, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ext4_ext_tree_init(handle, tmp_inode);
+
+ /* EXT4_STATE_EXT_MIGRATE intentionally NOT set on inode */
+ ret = ext4_ext_swap_inode_data_test(handle, inode, tmp_inode);
+ ext4_journal_stop(handle);
+
+ KUNIT_EXPECT_EQ(test, ret, -EAGAIN);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT4_I(inode)->i_data[0]), 210U);
+ KUNIT_EXPECT_EQ(test,
+ le32_to_cpu(EXT4_I(inode)->i_data[EXT4_IND_BLOCK]),
+ 550U);
+ KUNIT_EXPECT_FALSE(test, migrate_was_freed(ctx, 550));
+
+ iput(tmp_inode);
+ iput(inode);
+}
+
+/*
+ * Test 6: ext4_ind_migrate() converts an empty 0-extent file and a 1-extent
+ * file with a leading sparse hole ([3..7] -> 200..204) to direct block mapping.
+ */
+static void test_ind_migrate_empty_and_single_direct_extent(struct kunit *test)
+{
+ struct inode *inode;
+ struct ext4_extent_header *eh;
+ struct ext4_extent *ex;
+ handle_t *handle;
+ unsigned int i;
+
+ /* 1. Empty 0-extent file */
+ inode = migrate_make_test_inode(test, 19, S_IFREG | 0644);
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ handle = ext4_journal_start(inode, EXT4_HT_MIGRATE, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ext4_ext_tree_init(handle, inode);
+ ext4_journal_stop(handle);
+
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ for (i = 0; i < EXT4_N_BLOCKS; i++)
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[i], 0);
+ iput(inode);
+
+ /* 2. Single extent covering logical blocks [3..7] -> physical 200..204 */
+ inode = migrate_make_test_inode(test, 20, S_IFREG | 0644);
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ handle = ext4_journal_start(inode, EXT4_HT_MIGRATE, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ext4_ext_tree_init(handle, inode);
+ ext4_journal_stop(handle);
+
+ eh = ext_inode_hdr(inode);
+ eh->eh_entries = cpu_to_le16(1);
+ ex = EXT_FIRST_EXTENT(eh);
+ ex->ee_block = cpu_to_le32(3);
+ ex->ee_len = cpu_to_le16(5);
+ ext4_ext_store_pblock(ex, 200);
+
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), 0);
+ KUNIT_EXPECT_FALSE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+ for (i = 0; i < 3; i++)
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[i], 0);
+ for (i = 3; i <= 7; i++)
+ KUNIT_EXPECT_EQ(test, le32_to_cpu(EXT4_I(inode)->i_data[i]),
+ 200U + (i - 3));
+ for (i = 8; i < EXT4_N_BLOCKS; i++)
+ KUNIT_EXPECT_EQ(test, EXT4_I(inode)->i_data[i], 0);
+
+ iput(inode);
+}
+
+/*
+ * Test 7: ext4_ind_migrate() rejects non-extents, bigalloc, multi-extent,
+ * non-zero depth, >= EXT4_NDIR_BLOCKS logical range, and >32-bit filesystems.
+ */
+static void test_ind_migrate_rejections(struct kunit *test)
+{
+ struct migrate_test_ctx *ctx = test->priv;
+ struct super_block *sb = ctx->sb;
+ struct inode *inode;
+ struct ext4_extent_header *eh;
+ struct ext4_extent *ex;
+ handle_t *handle;
+
+ inode = migrate_make_test_inode(test, 21, S_IFREG | 0644);
+
+ /* 1. Inode without EXT4_INODE_EXTENTS -> -EINVAL */
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), -EINVAL);
+
+ ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+ handle = ext4_journal_start(inode, EXT4_HT_MIGRATE, 1);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(handle));
+ ext4_ext_tree_init(handle, inode);
+ ext4_journal_stop(handle);
+
+ /* 2. Filesystem without extents feature -> -EINVAL */
+ ext4_clear_feature_extents(sb);
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), -EINVAL);
+ ext4_set_feature_extents(sb);
+
+ /* 3. bigalloc enabled -> -EOPNOTSUPP */
+ ext4_set_feature_bigalloc(sb);
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), -EOPNOTSUPP);
+ ext4_clear_feature_bigalloc(sb);
+
+ /* 4. Extent reaching EXT4_NDIR_BLOCKS (end = 10 + 3 - 1 = 12) */
+ eh = ext_inode_hdr(inode);
+ eh->eh_entries = cpu_to_le16(1);
+ ex = EXT_FIRST_EXTENT(eh);
+ ex[0].ee_block = cpu_to_le32(10);
+ ex[0].ee_len = cpu_to_le16(3);
+ ext4_ext_store_pblock(&ex[0], 200);
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), -EOPNOTSUPP);
+ KUNIT_EXPECT_TRUE(test,
+ ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS));
+
+ /* 5. More than 1 extent (eh_entries == 2) -> -EOPNOTSUPP */
+ ex[0].ee_block = cpu_to_le32(0);
+ ex[0].ee_len = cpu_to_le16(2);
+ ext4_ext_store_pblock(&ex[0], 200);
+ ex[1].ee_block = cpu_to_le32(4);
+ ex[1].ee_len = cpu_to_le16(2);
+ ext4_ext_store_pblock(&ex[1], 210);
+ eh->eh_entries = cpu_to_le16(2);
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), -EOPNOTSUPP);
+
+ /* 6. Filesystem > EXT4_MAX_BLOCK_FILE_PHYS -> -EOPNOTSUPP */
+ eh->eh_entries = cpu_to_le16(1);
+ ext4_blocks_count_set(ctx->es, (ext4_fsblk_t)EXT4_MAX_BLOCK_FILE_PHYS +
+ 100ULL);
+ KUNIT_EXPECT_EQ(test, ext4_ind_migrate(inode), -EOPNOTSUPP);
+ ext4_blocks_count_set(ctx->es, MIGRATE_TEST_TOTAL_BLKS);
+
+ iput(inode);
+}
+
+static struct kunit_case ext4_migrate_test_cases[] = {
+ KUNIT_CASE(test_ext_migrate_precondition_checks_and_fast_symlink),
+ KUNIT_CASE(test_ext_migrate_direct_and_ind_coalescing_and_holes),
+ KUNIT_CASE(test_ext_migrate_dind_tind_and_multilevel_tree),
+ KUNIT_CASE(test_ext_migrate_error_rollback_frees_tmp_extent_blocks),
+ KUNIT_CASE(test_ext_swap_inode_data_detects_concurrent_allocation),
+ KUNIT_CASE(test_ind_migrate_empty_and_single_direct_extent),
+ KUNIT_CASE(test_ind_migrate_rejections),
+ {}
+};
+
+static struct kunit_suite ext4_migrate_test_suite = {
+ .name = "ext4_migrate_test",
+ .init = migrate_test_init,
+ .test_cases = ext4_migrate_test_cases,
+};
+
+kunit_test_suites(&ext4_migrate_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for ext4 indirect/extent inode migration");
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/migrate.c b/fs/ext4/migrate.c
index 5d60ef10fe11..8d76042f0cae 100644
--- a/fs/ext4/migrate.c
+++ b/fs/ext4/migrate.c
@@ -597,6 +597,7 @@ int ext4_ext_migrate(struct inode *inode)
iput(tmp_inode);
return retval;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_migrate);
/*
* Migrate a simple extent-based inode to use the i_blocks[] array
@@ -683,3 +684,13 @@ int ext4_ind_migrate(struct inode *inode)
ext4_writepages_up_write(inode->i_sb, alloc_ctx);
return ret;
}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ind_migrate);
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+int ext4_ext_swap_inode_data_test(handle_t *handle, struct inode *inode,
+ struct inode *tmp_inode)
+{
+ return ext4_ext_swap_inode_data(handle, inode, tmp_inode);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_ext_swap_inode_data_test);
+#endif
--
2.56.0.542.g3ef89b7020-goog
next prev parent reply other threads:[~2026-11-12 4:07 UTC|newest]
Thread overview: 55+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-12 4:06 [PATCH 00/27] ext4: add lots of KUnit tests! Theodore Ts'o
2026-11-12 4:06 ` [PATCH 01/27] ext4: add KUinit tests for fs/ext4/extents_status.c Theodore Ts'o
2026-11-12 4:18 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 02/27] ext4: add KUnit tests for fs/ext4/extents.c Theodore Ts'o
2026-11-12 4:28 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 03/27] ext4: add KUnit tests for ext4_ext_split() leaf and index splitting Theodore Ts'o
2026-11-12 4:15 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 04/27] ext4: add KUnit tests for fs/ext4/xattr.c Theodore Ts'o
2026-11-12 4:26 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 05/27] ext4: add KUnit tests for fs/ext4/namei.c Theodore Ts'o
2026-11-12 4:21 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 06/27] ext4: expand KUnit tests for fs/ext4/mballoc.c Theodore Ts'o
2026-11-12 4:26 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 07/27] ext4: add KUnit tests for fs/ext4/inline.c Theodore Ts'o
2026-11-12 4:29 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 08/27] ext4: zero s_fc_replay_state in ext4_fc_replay_cleanup() Theodore Ts'o
2026-11-12 4:16 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 09/27] ext4: add KUnit tests for fs/ext4/fast_commit.c Theodore Ts'o
2026-11-12 4:28 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 10/27] ext4: expand KUnit tests for fs/ext4/inode.c Theodore Ts'o
2026-11-12 4:36 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 11/27] ext4: add KUnit tests for fs/ext4/ialloc.c Theodore Ts'o
2026-11-12 4:25 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 12/27] ext4: add KUnit tests for fs/ext4/balloc.c Theodore Ts'o
2026-11-12 4:16 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 13/27] ext4: add KUnit tests for fs/ext4/orphan.c Theodore Ts'o
2026-11-12 4:18 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 14/27] ext4: add KUnit tests for fs/ext4/block_validity.c Theodore Ts'o
2026-11-12 4:21 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 15/27] ext4: check multi-block overlap on left traversal in add_system_zone() Theodore Ts'o
2026-11-12 4:10 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 16/27] ext4: enable full feature coverage in fs/ext4/.kunitconfig Theodore Ts'o
2026-11-12 4:24 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 17/27] ext4: add KUnit tests for fs/ext4/super.c Theodore Ts'o
2026-11-12 4:27 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 18/27] ext4: add KUnit tests for fs/ext4/sysfs.c Theodore Ts'o
2026-11-12 4:32 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 19/27] ext4: add KUnit tests for fs/ext4/ioctl.c Theodore Ts'o
2026-11-12 4:35 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 20/27] ext4: add KUnit tests for fs/ext4/resize.c Theodore Ts'o
2026-11-12 4:34 ` sashiko-bot
2026-11-12 4:06 ` Theodore Ts'o [this message]
2026-11-12 4:37 ` [PATCH 21/27] ext4: add KUnit tests for fs/ext4/migrate.c sashiko-bot
2026-11-12 4:06 ` [PATCH 22/27] ext4: add KUnit tests for fs/ext4/indirect.c Theodore Ts'o
2026-11-12 4:46 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 23/27] ext4: expand KUnit tests in fs/ext4/extents-test.c for file range operations Theodore Ts'o
2026-11-12 4:34 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 24/27] ext4: add KUnit tests for fs/ext4/fsmap.c Theodore Ts'o
2026-11-12 4:30 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 25/27] ext4: add KUnit tests for fs/ext4/move_extent.c Theodore Ts'o
2026-11-12 4:36 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 26/27] ext4: add KUnit tests for fs/ext4/mmp.c Theodore Ts'o
2026-11-12 4:29 ` sashiko-bot
2026-11-12 4:06 ` [PATCH 27/27] ext4: add KUnit tests for fs/ext4/dir.c Theodore Ts'o
2026-11-12 4:41 ` sashiko-bot
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261112040638.1576836-22-tytso@mit.edu \
--to=tytso@mit.edu \
--cc=linux-ext4@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.