Linux EXT4 FS development
 help / color / mirror / Atom feed
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 09/27] ext4: add KUnit tests for fs/ext4/fast_commit.c
Date: Wed, 11 Nov 2026 23:06:20 -0500	[thread overview]
Message-ID: <20261112040638.1576836-10-tytso@mit.edu> (raw)
In-Reply-To: <20261112040638.1576836-1-tytso@mit.edu>

Add the ext4_fast_commit_test KUnit test suite in
fs/ext4/fast_commit-test.c to comprehensively exercise fast commit
tracking, snapshotting, TLV serialization, commit execution, and journal
replay in fs/ext4/fast_commit.c without requiring a mounted block
device:

- Fast commit initialization and eligibility helpers (ext4_fc_init,
  ext4_fc_init_inode, ext4_fc_disabled, ext4_fc_eligible,
  ext4_fc_set_snap_err, and ext4_fc_snap_stats_update_max).
- Ineligibility tracking (ext4_fc_mark_ineligible) with active handles,
  NULL handles with or without a running transaction, monotonic
  s_fc_ineligible_tid updates, and per-reason statistics counters.
- Inode and logical range tracking (ext4_fc_track_inode and
  ext4_fc_track_range), covering directory skips, data-journalling and
  inline-data ineligibility, special inodes (< EXT4_FIRST_INO), same-TID
  bounding-interval range merging, cross-TID resets,
  EXT4_STATE_FC_COMMITTING to EXT4_STATE_FC_REQUEUE transitions, and
  FC_Q_MAIN vs. FC_Q_STAGING queue routing.
- Dentry update tracking (ext4_fc_track_create, ext4_fc_track_link, and
  ext4_fc_track_unlink) and inode deletion cleanup (ext4_fc_del),
  including root dentry parent handling, EXT4_FC_TAG_CREAT removal, and
  concurrent wait-bit wakeups across EXT4_STATE_FC_FLUSHING_DATA and
  EXT4_STATE_FC_COMMITTING.
- Fast commit buffer space reservation and TLV serialization
  (ext4_fc_reserve_space, ext4_fc_add_tlv, and ext4_fc_write_tail),
  including oversize rejection, block boundary EXT4_FC_TAG_PAD insertion
  and buffer submission, and EXT4_FC_TAG_TAIL padding and CRC32C
  calculation.
- Inode and extent-status range snapshotting (ext4_fc_snapshot_inode,
  ext4_fc_count_snapshot_inodes, ext4_fc_alloc_snapshot_inodes,
  ext4_fc_snapshot_inodes, ext4_fc_write_inode, and
  ext4_fc_write_inode_data), including ext4_get_inode_loc_noio fallback,
  ES miss/delayed/unsupported status errors, inode and range capacity
  caps (-E2BIG), extent splitting past EXT_INIT_MAX_LEN (32768), and
  create-only dentry inode snapshotting.
- Fast and full commit cleanup (ext4_fc_cleanup), EWMA commit time stats
  (ext4_fc_update_stats), and procfs stats output (ext4_fc_info_show).
- Top-level commit execution and fallbacks (ext4_fc_commit) across
  already-committed TIDs, disabled fast commits, jbd2_fc_begin_commit
  failures, ineligible filesystems, data flush -EIO errors, snapshot
  -EAGAIN fallbacks, jbd2_fc_wait_bufs -EIO errors, and consecutive fast
  commits with external journal device cache flushes.
- Recovery region exclusion and scan validation (ext4_fc_record_regions,
  ext4_fc_replay_check_excluded, ext4_fc_record_modified_inode,
  ext4_fc_value_len_isvalid, tag2str, and PASS_SCAN validation of
  missing HEAD, unexpected offset, unsupported features, wrong TID,
  bad tail CRC, and partial multi-commit logs).
- Recovery replay of EXT4_FC_TAG_INODE (extent and inline-data inodes),
  EXT4_FC_TAG_CREAT (regular files and subdirectories), EXT4_FC_TAG_LINK
  (-EEXIST idempotency), and EXT4_FC_TAG_UNLINK (missing entries and
  rename-overwrite inode mismatches).
- Recovery replay of EXT4_FC_TAG_ADD_RANGE and EXT4_FC_TAG_DEL_RANGE,
  error handling, and an end-to-end ext4_fc_commit() -> PASS_SCAN ->
  PASS_REPLAY round-trip.

Export the tested fast commit entry points with
EXPORT_SYMBOL_FOR_EXT4_TEST and add KUNIT_STATIC_STUB_REDIRECT hooks for
ext4_fc_get_buf, ext4_fc_submit_bh, ext4_fc_issue_flush,
ext4_get_inode_loc_noio, ext4_get_fc_inode_loc, ext4_mark_inode_used,
ext4_mb_mark_bb, and ext4_es_lookup_extent.

Signed-off-by: Theodore Ts'o <tytso@mit.edu>
---
 fs/ext4/Makefile           |    3 +-
 fs/ext4/ext4.h             |   35 +
 fs/ext4/extents_status.c   |    3 +
 fs/ext4/fast_commit-test.c | 2656 ++++++++++++++++++++++++++++++++++++
 fs/ext4/fast_commit.c      |  180 ++-
 fs/ext4/ialloc.c           |    3 +
 fs/ext4/inode.c            |    6 +
 fs/ext4/mballoc.c          |    3 +-
 8 files changed, 2882 insertions(+), 7 deletions(-)
 create mode 100644 fs/ext4/fast_commit-test.c

diff --git a/fs/ext4/Makefile b/fs/ext4/Makefile
index 5a90bf5b28e3..7bd91839c3ce 100644
--- a/fs/ext4/Makefile
+++ b/fs/ext4/Makefile
@@ -17,7 +17,8 @@ ext4-$(CONFIG_EXT4_FS_SECURITY)		+= xattr_security.o
 ext4-test-objs				+= inode-test.o mballoc-test.o \
 					   extents-test.o hash-test.o \
 					   extents_status-test.o xattr-test.o \
-					   namei-test.o inline-test.o
+					   namei-test.o inline-test.o \
+					   fast_commit-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 bcb4eb10d363..afa2db3b6d57 100644
--- a/fs/ext4/ext4.h
+++ b/fs/ext4/ext4.h
@@ -3070,6 +3070,9 @@ extern int ext4_init_inode_table(struct super_block *sb,
 void ext4_end_bitmap_read(struct bio *bio);
 
 /* fast_commit.c */
+int ext4_fc_get_buf(journal_t *journal, struct buffer_head **bh);
+void ext4_fc_submit_bh(struct super_block *sb, bool is_tail);
+void ext4_fc_issue_flush(struct block_device *bdev);
 int ext4_fc_info_show(struct seq_file *seq, void *v);
 void ext4_fc_init(struct super_block *sb, journal_t *journal);
 void ext4_fc_init_inode(struct inode *inode);
@@ -3096,6 +3099,38 @@ void ext4_fc_destroy_dentry_cache(void);
 int ext4_fc_record_regions(struct super_block *sb, int ino,
 			   ext4_lblk_t lblk, ext4_fsblk_t pblk,
 			   int len, int replay);
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+void ext4_fc_set_snap_err_test(int *snap_err, int err);
+bool ext4_fc_disabled_test(struct super_block *sb);
+bool ext4_fc_eligible_test(struct super_block *sb);
+void ext4_fc_snap_stats_update_max_test(atomic64_t *stat, u64 value);
+void ext4_fc_wake_inode_state_test(struct inode *inode, int bit);
+u8 *ext4_fc_reserve_space_test(struct super_block *sb, int len, u32 *crc);
+int ext4_fc_write_tail_test(struct super_block *sb, u32 crc);
+bool ext4_fc_add_tlv_test(struct super_block *sb, u16 tag, u16 len, u8 *val,
+			  u32 *crc);
+int ext4_fc_write_inode_test(struct inode *inode, u32 *crc);
+int ext4_fc_write_inode_data_test(struct inode *inode, u32 *crc);
+void ext4_fc_free_inode_snap_test(struct inode *inode);
+int ext4_fc_snapshot_inode_test(struct inode *inode,
+				unsigned int nr_ranges_total,
+				unsigned int *nr_rangesp, int *snap_err);
+unsigned int ext4_fc_count_snapshot_inodes_test(struct super_block *sb);
+int ext4_fc_alloc_snapshot_inodes_test(struct super_block *sb,
+				       struct inode ***inodesp,
+				       unsigned int *nr_inodesp);
+int ext4_fc_snapshot_inodes_test(journal_t *journal, struct inode **inodes,
+				 unsigned int inodes_size,
+				 unsigned int *nr_inodesp,
+				 unsigned int *nr_rangesp, int *snap_err);
+void ext4_fc_update_stats_test(struct super_block *sb, int status,
+			       u64 commit_time, int nblks, tid_t commit_tid);
+void ext4_fc_cleanup_test(journal_t *journal, int full, tid_t tid);
+int ext4_fc_record_modified_inode_test(struct super_block *sb, int ino);
+bool ext4_fc_value_len_isvalid_test(struct ext4_sb_info *sbi, int tag, int len);
+int ext4_fc_replay_test(journal_t *journal, struct buffer_head *bh,
+			enum passtype pass, int off, tid_t expected_tid);
+#endif
 
 /* mballoc.c */
 extern const struct seq_operations ext4_mb_seq_groups_ops;
diff --git a/fs/ext4/extents_status.c b/fs/ext4/extents_status.c
index 64bac0c4c1fa..075b9efb6dd4 100644
--- a/fs/ext4/extents_status.c
+++ b/fs/ext4/extents_status.c
@@ -1099,6 +1099,9 @@ int ext4_es_lookup_extent(struct inode *inode, ext4_lblk_t lblk,
 	struct rb_node *node;
 	int found = 0;
 
+	KUNIT_STATIC_STUB_REDIRECT(ext4_es_lookup_extent, inode, lblk,
+				   next_lblk, es, pseq);
+
 	if (EXT4_SB(inode->i_sb)->s_mount_state & EXT4_FC_REPLAY)
 		return 0;
 
diff --git a/fs/ext4/fast_commit-test.c b/fs/ext4/fast_commit-test.c
new file mode 100644
index 000000000000..57745e51bea6
--- /dev/null
+++ b/fs/ext4/fast_commit-test.c
@@ -0,0 +1,2656 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for ext4 fast commit operations (fs/ext4/fast_commit.c)
+ */
+
+#include <kunit/test.h>
+#include <kunit/static_stub.h>
+#include <linux/backing-dev.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/iversion.h>
+#include <linux/jbd2.h>
+#include <linux/kthread.h>
+#include <linux/mm.h>
+#include <linux/pagemap.h>
+#include <linux/quotaops.h>
+#include <linux/seq_file.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "ext4.h"
+#include "ext4_jbd2.h"
+#include "ext4_extents.h"
+#include "extents_status.h"
+#include "fast_commit.h"
+#include "namei.h"
+
+#define TEST_BLK_SIZE		1024
+#define TEST_BLK_BITS		10
+#define TEST_INODE_SIZE		256
+#define TEST_EXTRA_ISIZE	32
+#define TEST_FIRST_PBLK		100
+#define TEST_MAX_BLOCKS		32
+#define TEST_MAX_LBLOCKS	16
+#define TEST_MAX_INODES		8
+#define TEST_MAX_FC_BUFS	16
+#define TEST_MAX_BB_MARKS	32
+
+#define TEST_ROOT_INO		EXT4_ROOT_INO
+#define TEST_DIR_INO		12
+#define TEST_FILE1_INO		13
+#define TEST_FILE2_INO		14
+#define TEST_SUBDIR_INO		15
+#define TEST_CSUM_SEED		0x12345678U
+
+struct fc_mock_block {
+	bool in_use;
+	struct buffer_head bh;
+	char data[TEST_BLK_SIZE] __aligned(8);
+};
+
+struct fc_mock_inode_slot {
+	bool allocated;
+	struct ext4_inode_info ei;
+	struct jbd2_inode jinode;
+	struct buffer_head iloc_bh;
+	char raw_buf[TEST_BLK_SIZE] __aligned(8);
+	ext4_fsblk_t lblk_to_pblk[TEST_MAX_LBLOCKS];
+};
+
+struct fc_bb_mark {
+	ext4_fsblk_t block;
+	int len;
+	bool state;
+};
+
+struct fc_test_ctx {
+	struct super_block *sb;
+	struct ext4_sb_info *sbi;
+	struct ext4_super_block es;
+	struct block_device bdev;
+	struct block_device ext_jdev;
+	struct address_space bd_mapping;
+	struct buffer_head sb_bh;
+
+	journal_t journal;
+	transaction_t running_txn;
+	handle_t handle;
+	struct buffer_head *fc_wbuf[TEST_MAX_FC_BUFS];
+	struct buffer_head fc_bhs[TEST_MAX_FC_BUFS];
+	char fc_buf_data[TEST_MAX_FC_BUFS][TEST_BLK_SIZE] __aligned(8);
+
+	struct fc_mock_inode_slot inodes[TEST_MAX_INODES];
+	struct fc_mock_block blocks[TEST_MAX_BLOCKS];
+
+	/* Shrinker callbacks saved during init */
+	unsigned long (*shrinker_count)(struct shrinker *shrink,
+					struct shrink_control *sc);
+	unsigned long (*shrinker_scan)(struct shrinker *shrink,
+				       struct shrink_control *sc);
+
+	/* Fault injection and call tracking */
+	int journal_mode;
+	int fail_get_inode_loc;
+	int fail_get_inode_loc_noio;
+	int fail_get_fc_inode_loc;
+	int fail_dirty_metadata;
+	int fail_dirty_metadata_on_call;
+	int dirty_metadata_calls;
+	bool fail_sync_dirty_buffer;
+	int fail_mark_inode_used;
+	int fail_iget;
+	int fail_iget_on_call;
+	unsigned long fail_iget_ino;
+	int iget_calls;
+	int fail_map_blocks;
+	int force_map_blocks_ret;
+	ext4_fsblk_t force_map_pblk;
+	unsigned int force_map_flags;
+	int fail_find_extent;
+	int fail_insert_extent;
+	int fail_fc_get_buf;
+	int fail_fc_get_buf_on_call;
+	int fc_get_buf_calls;
+	bool fail_fc_wait_bufs;
+	int submit_inode_data_err;
+
+	unsigned int fc_submit_bh_calls;
+	unsigned int fc_submit_tail_calls;
+	unsigned int fc_flush_calls;
+	struct block_device *last_flushed_bdev;
+	unsigned int mark_inode_used_calls;
+	int last_marked_ino;
+
+	struct fc_bb_mark bb_marks[TEST_MAX_BB_MARKS];
+	unsigned int nr_bb_marks;
+
+	/* Custom ES lookup stub override */
+	bool use_custom_es_lookup;
+	struct extent_status custom_es;
+	int custom_es_ret;
+};
+
+static const struct super_operations fc_test_sops = {};
+
+#ifdef CONFIG_QUOTA
+static qsize_t *fc_test_get_reserved_space(struct inode *inode)
+{
+	return &EXT4_I(inode)->i_reserved_quota;
+}
+
+static const struct dquot_operations fc_test_qops = {
+	.get_reserved_space = fc_test_get_reserved_space,
+};
+#endif
+
+static unsigned long fc_dummy_shrinker_count(struct shrinker *shrink,
+					     struct shrink_control *sc)
+{
+	return 0;
+}
+
+static unsigned long fc_dummy_shrinker_scan(struct shrinker *shrink,
+					    struct shrink_control *sc)
+{
+	return SHRINK_STOP;
+}
+
+static int fc_init_fs_context(struct fs_context *fc)
+{
+	return 0;
+}
+
+static int fc_set_anon(struct super_block *sb, struct fs_context *fc)
+{
+	return set_anon_super_fc(sb, fc);
+}
+
+static struct file_system_type fc_fs_type = {
+	.name		 = "fast_commit_test",
+	.init_fs_context = fc_init_fs_context,
+	.kill_sb	 = kill_anon_super,
+};
+
+/* --- Mock Inode, Block, and Dentry Helpers --- */
+
+static struct fc_mock_inode_slot *find_slot_by_ino(struct fc_test_ctx *ctx,
+						   unsigned long ino)
+{
+	int i;
+
+	for (i = 0; i < TEST_MAX_INODES; i++) {
+		if (ctx->inodes[i].allocated &&
+		    ctx->inodes[i].ei.vfs_inode.i_ino == ino)
+			return &ctx->inodes[i];
+	}
+	return NULL;
+}
+
+static struct fc_mock_inode_slot *find_slot(struct fc_test_ctx *ctx,
+					    struct inode *inode)
+{
+	return find_slot_by_ino(ctx, inode->i_ino);
+}
+
+static struct inode *get_mock_inode(struct fc_test_ctx *ctx, unsigned long ino)
+{
+	struct fc_mock_inode_slot *slot = find_slot_by_ino(ctx, ino);
+
+	return slot ? &slot->ei.vfs_inode : NULL;
+}
+
+static struct fc_mock_block *get_mock_block(struct fc_test_ctx *ctx,
+					    ext4_fsblk_t pblk)
+{
+	unsigned long idx;
+
+	if (pblk < TEST_FIRST_PBLK)
+		return NULL;
+	idx = pblk - TEST_FIRST_PBLK;
+	if (idx >= TEST_MAX_BLOCKS)
+		return NULL;
+	return &ctx->blocks[idx];
+}
+
+static ext4_fsblk_t alloc_mock_pblk(struct fc_test_ctx *ctx)
+{
+	int i;
+
+	for (i = 0; i < TEST_MAX_BLOCKS; i++) {
+		if (!ctx->blocks[i].in_use) {
+			ctx->blocks[i].in_use = true;
+			memset(ctx->blocks[i].data, 0, TEST_BLK_SIZE);
+			set_buffer_uptodate(&ctx->blocks[i].bh);
+			clear_buffer_verified(&ctx->blocks[i].bh);
+			return TEST_FIRST_PBLK + i;
+		}
+	}
+	return 0;
+}
+
+static void init_mock_inode_slot(struct fc_test_ctx *ctx,
+				 struct fc_mock_inode_slot *slot,
+				 unsigned long ino, umode_t mode)
+{
+	struct inode *inode = &slot->ei.vfs_inode;
+	struct ext4_inode *raw_inode;
+
+	memset(slot, 0, sizeof(*slot));
+	slot->allocated = true;
+
+	inode->i_sb = ctx->sb;
+	inode->i_ino = ino;
+	inode->i_mode = mode;
+	inode->i_blkbits = TEST_BLK_BITS;
+	inode->i_flags |= S_NOQUOTA;
+	inode->i_mapping = &inode->i_data;
+	set_nlink(inode, S_ISDIR(mode) ? 2 : 1);
+	atomic_set(&inode->i_count, 1);
+
+	spin_lock_init(&inode->i_lock);
+	init_rwsem(&inode->i_rwsem);
+	init_rwsem(&inode->i_data.invalidate_lock);
+	INIT_LIST_HEAD(&inode->i_io_list);
+	INIT_LIST_HEAD(&inode->i_wb_list);
+	INIT_LIST_HEAD(&inode->i_lru);
+	INIT_HLIST_NODE(&inode->i_hash);
+	inode_state_set_raw(inode, I_DIRTY);
+
+	init_rwsem(&slot->ei.xattr_sem);
+	init_rwsem(&slot->ei.i_data_sem);
+	INIT_LIST_HEAD(&slot->ei.i_orphan);
+	spin_lock_init(&slot->ei.i_fc_lock);
+	spin_lock_init(&slot->ei.i_raw_lock);
+	spin_lock_init(&slot->ei.i_block_reservation_lock);
+	ext4_es_init_tree(&slot->ei.i_es_tree);
+	ext4_init_pending_tree(&slot->ei.i_pending_tree);
+	rwlock_init(&slot->ei.i_es_lock);
+	INIT_LIST_HEAD(&slot->ei.i_es_list);
+	ext4_fc_init_inode(inode);
+
+	slot->ei.i_extra_isize = TEST_EXTRA_ISIZE;
+	slot->ei.i_csum_seed = TEST_CSUM_SEED ^ (u32)ino;
+	slot->ei.i_projid = make_kprojid(&init_user_ns, EXT4_DEF_PROJID);
+
+	jbd2_journal_init_jbd_inode(&slot->jinode, inode);
+	slot->ei.jinode = &slot->jinode;
+
+	slot->iloc_bh.b_data = slot->raw_buf;
+	slot->iloc_bh.b_size = TEST_BLK_SIZE;
+	slot->iloc_bh.b_blocknr = 10 + ino;
+	slot->iloc_bh.b_bdev = &ctx->bdev;
+	atomic_set(&slot->iloc_bh.b_count, 1);
+	set_buffer_uptodate(&slot->iloc_bh);
+	set_buffer_mapped(&slot->iloc_bh);
+
+	raw_inode = (struct ext4_inode *)slot->raw_buf;
+	raw_inode->i_mode = cpu_to_le16(mode);
+	raw_inode->i_links_count = cpu_to_le16(inode->i_nlink);
+	raw_inode->i_extra_isize = cpu_to_le16(TEST_EXTRA_ISIZE);
+	if (S_ISREG(mode)) {
+		ext4_set_inode_flag(inode, EXT4_INODE_EXTENTS);
+		ext4_ext_tree_init(NULL, inode);
+		raw_inode->i_flags = cpu_to_le32(EXT4_EXTENTS_FL);
+		memcpy(raw_inode->i_block, slot->ei.i_data,
+		       sizeof(raw_inode->i_block));
+	}
+}
+
+static void init_mock_dentry(struct dentry *dentry, struct dentry *parent,
+			     const char *name, struct inode *inode,
+			     struct super_block *sb)
+{
+	memset(dentry, 0, sizeof(*dentry));
+	spin_lock_init(&dentry->d_lock);
+	seqcount_spinlock_init(&dentry->d_seq, &dentry->d_lock);
+	INIT_HLIST_NODE(&dentry->d_sib);
+	INIT_HLIST_HEAD(&dentry->d_children);
+	INIT_HLIST_BL_NODE(&dentry->d_hash);
+	dentry->d_parent = parent ? parent : dentry;
+	dentry->d_sb = sb;
+	strscpy((char *)dentry->d_shortname.string, name,
+		sizeof(dentry->d_shortname.string));
+	dentry->__d_name.name = dentry->d_shortname.string;
+	dentry->__d_name.len = strlen(name);
+	if (inode) {
+		dentry->d_inode = inode;
+		dentry->d_flags = S_ISDIR(inode->i_mode) ?
+				  DCACHE_DIRECTORY_TYPE : DCACHE_REGULAR_TYPE;
+	} else {
+		dentry->d_flags = DCACHE_MISS_TYPE;
+	}
+}
+
+static void sync_inode_to_raw(struct fc_mock_inode_slot *slot)
+{
+	struct inode *inode = &slot->ei.vfs_inode;
+	struct ext4_inode *raw = (struct ext4_inode *)slot->raw_buf;
+
+	raw->i_mode = cpu_to_le16(inode->i_mode);
+	raw->i_links_count = cpu_to_le16(inode->i_nlink);
+	raw->i_size_lo = cpu_to_le32(lower_32_bits(inode->i_size));
+	raw->i_size_high = cpu_to_le32(upper_32_bits(inode->i_size));
+	raw->i_flags = cpu_to_le32(slot->ei.i_flags);
+	raw->i_generation = cpu_to_le32(inode->i_generation);
+	raw->i_extra_isize = cpu_to_le16(slot->ei.i_extra_isize);
+	memcpy(raw->i_block, slot->ei.i_data, sizeof(raw->i_block));
+}
+
+/* --- KUnit Static Stubs --- */
+
+static int stub_inode_journal_mode(struct inode *inode)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+
+	return ctx->journal_mode;
+}
+
+static void stub_da_update_reserve_space(struct inode *inode, int used,
+					 int quota_claim)
+{
+}
+
+static void stub_da_release_space(struct inode *inode, int to_free)
+{
+}
+
+static int stub_get_inode_loc(struct inode *inode, struct ext4_iloc *iloc)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+	struct fc_mock_inode_slot *slot;
+
+	if (ctx->fail_get_inode_loc)
+		return ctx->fail_get_inode_loc;
+
+	slot = find_slot(ctx, inode);
+	if (!slot)
+		return -ENOENT;
+
+	get_bh(&slot->iloc_bh);
+	iloc->bh = &slot->iloc_bh;
+	iloc->offset = 0;
+	iloc->block_group = 0;
+	return 0;
+}
+
+static int stub_get_inode_loc_noio(struct inode *inode, struct ext4_iloc *iloc)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+	struct fc_mock_inode_slot *slot;
+
+	if (ctx->fail_get_inode_loc_noio)
+		return ctx->fail_get_inode_loc_noio;
+
+	slot = find_slot(ctx, inode);
+	if (!slot)
+		return -EAGAIN;
+
+	get_bh(&slot->iloc_bh);
+	iloc->bh = &slot->iloc_bh;
+	iloc->offset = 0;
+	iloc->block_group = 0;
+	return 0;
+}
+
+static int stub_get_fc_inode_loc(struct super_block *sb, unsigned long ino,
+				 struct ext4_iloc *iloc)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+	struct fc_mock_inode_slot *slot;
+
+	if (ctx->fail_get_fc_inode_loc)
+		return ctx->fail_get_fc_inode_loc;
+
+	slot = find_slot_by_ino(ctx, ino);
+	if (!slot)
+		return -ENOENT;
+
+	get_bh(&slot->iloc_bh);
+	iloc->bh = &slot->iloc_bh;
+	iloc->offset = 0;
+	iloc->block_group = 0;
+	return 0;
+}
+
+static int stub_handle_dirty_metadata(const char *where, unsigned int line,
+				      handle_t *handle, struct inode *inode,
+				      struct buffer_head *bh)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+
+	ctx->dirty_metadata_calls++;
+	if (ctx->fail_dirty_metadata_on_call > 0 &&
+	    ctx->dirty_metadata_calls == ctx->fail_dirty_metadata_on_call)
+		return ctx->fail_dirty_metadata;
+	if (ctx->fail_dirty_metadata_on_call == 0 && ctx->fail_dirty_metadata)
+		return ctx->fail_dirty_metadata;
+
+	if (ctx->fail_sync_dirty_buffer) {
+		/*
+		 * __sync_dirty_buffer() checks test_clear_buffer_dirty(bh)
+		 * and returns -EIO without submitting a bio if !buffer_mapped.
+		 */
+		set_buffer_dirty(bh);
+		clear_buffer_mapped(bh);
+	} else {
+		clear_buffer_dirty(bh);
+		set_buffer_mapped(bh);
+	}
+	return 0;
+}
+
+static int stub_mark_iloc_dirty(handle_t *handle, struct inode *inode,
+				struct ext4_iloc *iloc)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+	struct fc_mock_inode_slot *slot = find_slot(ctx, inode);
+
+	if (slot)
+		sync_inode_to_raw(slot);
+	brelse(iloc->bh);
+	return 0;
+}
+
+static int stub_mark_inode_dirty(handle_t *handle, struct inode *inode,
+				 const char *func, unsigned int line)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+	struct fc_mock_inode_slot *slot = find_slot(ctx, inode);
+
+	if (slot)
+		sync_inode_to_raw(slot);
+	return 0;
+}
+
+static int stub_ext_dirty(const char *where, unsigned int line,
+			  handle_t *handle, struct inode *inode,
+			  struct ext4_ext_path *path)
+{
+	return 0;
+}
+
+static void stub_free_blocks(handle_t *handle, struct inode *inode,
+			     struct buffer_head *bh, ext4_fsblk_t block,
+			     unsigned long count, int flags)
+{
+}
+
+static int stub_orphan_add(handle_t *handle, struct inode *inode)
+{
+	return 0;
+}
+
+static int stub_orphan_del(handle_t *handle, struct inode *inode)
+{
+	return 0;
+}
+
+static int stub_mark_inode_used(struct super_block *sb, int ino)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+
+	ctx->mark_inode_used_calls++;
+	ctx->last_marked_ino = ino;
+	if (ctx->fail_mark_inode_used)
+		return ctx->fail_mark_inode_used;
+	return 0;
+}
+
+static void stub_mb_mark_bb(struct super_block *sb, ext4_fsblk_t block,
+			    int len, bool state)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+
+	if (ctx->nr_bb_marks < TEST_MAX_BB_MARKS) {
+		ctx->bb_marks[ctx->nr_bb_marks].block = block;
+		ctx->bb_marks[ctx->nr_bb_marks].len = len;
+		ctx->bb_marks[ctx->nr_bb_marks].state = state;
+	}
+	ctx->nr_bb_marks++;
+}
+
+static struct inode *stub_ext4_iget(struct super_block *sb, unsigned long ino,
+				    ext4_iget_flags flags,
+				    const char *function, unsigned int line)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+	struct fc_mock_inode_slot *slot;
+	struct inode *inode;
+	struct ext4_inode *raw;
+
+	ctx->iget_calls++;
+	if (ctx->fail_iget_on_call > 0 &&
+	    ctx->iget_calls == ctx->fail_iget_on_call)
+		return ERR_PTR(ctx->fail_iget ? ctx->fail_iget : -ENOENT);
+	if (ctx->fail_iget_ino && ino == ctx->fail_iget_ino)
+		return ERR_PTR(ctx->fail_iget ? ctx->fail_iget : -ENOENT);
+	if (ctx->fail_iget && !ctx->fail_iget_on_call && !ctx->fail_iget_ino)
+		return ERR_PTR(ctx->fail_iget);
+
+	slot = find_slot_by_ino(ctx, ino);
+	if (!slot)
+		return ERR_PTR(-ENOENT);
+
+	inode = &slot->ei.vfs_inode;
+	raw = (struct ext4_inode *)slot->raw_buf;
+	if (raw->i_mode) {
+		inode->i_mode = le16_to_cpu(raw->i_mode);
+		set_nlink(inode, le16_to_cpu(raw->i_links_count));
+		inode->i_generation = le32_to_cpu(raw->i_generation);
+		i_size_write(inode, ext4_isize(sb, raw));
+		slot->ei.i_flags = le32_to_cpu(raw->i_flags);
+		if (slot->ei.i_flags & EXT4_INLINE_DATA_FL) {
+			ext4_set_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA);
+			memcpy(slot->ei.i_data, raw->i_block,
+			       sizeof(raw->i_block));
+		} else if (slot->ei.i_flags & EXT4_EXTENTS_FL) {
+			struct ext4_extent_header *eh =
+				(struct ext4_extent_header *)raw->i_block;
+			struct ext4_extent_header *ieh =
+				ext_inode_hdr(&slot->ei.vfs_inode);
+
+			if (eh->eh_magic == EXT4_EXT_MAGIC &&
+			    ieh->eh_entries == 0)
+				memcpy(slot->ei.i_data, raw->i_block,
+				       sizeof(raw->i_block));
+		}
+	}
+
+	atomic_inc(&inode->i_count);
+	return inode;
+}
+
+static int stub_map_blocks(handle_t *handle, struct inode *inode,
+			   struct ext4_map_blocks *map, int flags)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+	struct fc_mock_inode_slot *slot;
+	ext4_fsblk_t pblk;
+
+	if (ctx->fail_map_blocks)
+		return ctx->fail_map_blocks;
+	if (ctx->force_map_blocks_ret) {
+		map->m_pblk = ctx->force_map_pblk;
+		map->m_flags = ctx->force_map_flags;
+		if (ctx->force_map_blocks_ret > 0)
+			map->m_len = ctx->force_map_blocks_ret;
+		return ctx->force_map_blocks_ret;
+	}
+
+	if (S_ISREG(inode->i_mode) &&
+	    ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS)) {
+		kunit_deactivate_static_stub(test, ext4_map_blocks);
+		int ret = ext4_ext_map_blocks(handle, inode, map, flags);
+
+		kunit_activate_static_stub(test, ext4_map_blocks,
+					   stub_map_blocks);
+		return ret;
+	}
+
+	slot = find_slot(ctx, inode);
+	if (!slot || map->m_lblk >= TEST_MAX_LBLOCKS)
+		return -EFSCORRUPTED;
+
+	pblk = slot->lblk_to_pblk[map->m_lblk];
+	if (pblk) {
+		map->m_pblk = pblk;
+		map->m_len = 1;
+		map->m_flags = EXT4_MAP_MAPPED;
+		return 1;
+	}
+
+	if (!(flags & EXT4_GET_BLOCKS_CREATE)) {
+		map->m_pblk = 0;
+		map->m_flags = 0;
+		return 0;
+	}
+
+	pblk = alloc_mock_pblk(ctx);
+	if (!pblk)
+		return -ENOSPC;
+
+	slot->lblk_to_pblk[map->m_lblk] = pblk;
+	map->m_pblk = pblk;
+	map->m_len = 1;
+	map->m_flags = EXT4_MAP_MAPPED | EXT4_MAP_NEW;
+	return 1;
+}
+
+static struct buffer_head *stub_ext4_getblk(handle_t *handle,
+					    struct inode *inode,
+					    ext4_lblk_t block, int map_flags)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+	struct fc_mock_inode_slot *slot;
+	struct fc_mock_block *blk;
+	ext4_fsblk_t pblk;
+
+	slot = find_slot(ctx, inode);
+	if (!slot || block >= TEST_MAX_LBLOCKS)
+		return ERR_PTR(-EFSCORRUPTED);
+
+	pblk = slot->lblk_to_pblk[block];
+	if (!pblk) {
+		if (!(map_flags & EXT4_GET_BLOCKS_CREATE))
+			return NULL;
+		pblk = alloc_mock_pblk(ctx);
+		if (!pblk)
+			return ERR_PTR(-ENOSPC);
+		slot->lblk_to_pblk[block] = pblk;
+	}
+
+	blk = get_mock_block(ctx, pblk);
+	if (!blk)
+		return ERR_PTR(-EFSCORRUPTED);
+
+	get_bh(&blk->bh);
+	return &blk->bh;
+}
+
+static struct ext4_ext_path *stub_find_extent(struct inode *inode,
+					      ext4_lblk_t block,
+					      struct ext4_ext_path *path,
+					      int flags)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+	struct ext4_ext_path *ret;
+
+	if (ctx->fail_find_extent)
+		return ERR_PTR(ctx->fail_find_extent);
+
+	kunit_deactivate_static_stub(test, ext4_find_extent);
+	ret = ext4_find_extent(inode, block, path, flags);
+	kunit_activate_static_stub(test, ext4_find_extent, stub_find_extent);
+	return ret;
+}
+
+static struct ext4_ext_path *stub_insert_extent(handle_t *handle,
+						struct inode *inode,
+						struct ext4_ext_path *path,
+						struct ext4_extent *newext,
+						int gb_flags)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+	struct ext4_ext_path *ret;
+
+	if (ctx->fail_insert_extent) {
+		ext4_free_ext_path(path);
+		return ERR_PTR(ctx->fail_insert_extent);
+	}
+
+	kunit_deactivate_static_stub(test, ext4_ext_insert_extent);
+	ret = ext4_ext_insert_extent(handle, inode, path, newext, gb_flags);
+	kunit_activate_static_stub(test, ext4_ext_insert_extent,
+				   stub_insert_extent);
+	return ret;
+}
+
+static int stub_es_lookup_extent(struct inode *inode, ext4_lblk_t lblk,
+				 ext4_lblk_t *next_lblk,
+				 struct extent_status *es, u64 *pseq)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+	int ret;
+
+	if (ctx->use_custom_es_lookup) {
+		*es = ctx->custom_es;
+		return ctx->custom_es_ret;
+	}
+
+	kunit_deactivate_static_stub(test, ext4_es_lookup_extent);
+	ret = ext4_es_lookup_extent(inode, lblk, next_lblk, es, pseq);
+	kunit_activate_static_stub(test, ext4_es_lookup_extent,
+				   stub_es_lookup_extent);
+	return ret;
+}
+
+static int stub_fc_get_buf(journal_t *journal, struct buffer_head **bh_out)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+	struct buffer_head *bh;
+	int fc_off;
+
+	*bh_out = NULL;
+	ctx->fc_get_buf_calls++;
+	if (ctx->fail_fc_get_buf_on_call > 0 &&
+	    ctx->fc_get_buf_calls == ctx->fail_fc_get_buf_on_call)
+		return ctx->fail_fc_get_buf ? ctx->fail_fc_get_buf : -ENOSPC;
+	if (ctx->fail_fc_get_buf && !ctx->fail_fc_get_buf_on_call)
+		return ctx->fail_fc_get_buf;
+
+	if (journal->j_fc_off + journal->j_fc_first >= journal->j_fc_last)
+		return -EINVAL;
+
+	fc_off = journal->j_fc_off;
+	if (fc_off >= TEST_MAX_FC_BUFS)
+		return -ENOSPC;
+
+	journal->j_fc_off++;
+	bh = &ctx->fc_bhs[fc_off];
+	memset(bh->b_data, 0, journal->j_blocksize);
+	set_buffer_uptodate(bh);
+	get_bh(bh);
+	journal->j_fc_wbuf[fc_off] = bh;
+	*bh_out = bh;
+	return 0;
+}
+
+static void stub_fc_submit_bh(struct super_block *sb, bool is_tail)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+	struct buffer_head *bh = EXT4_SB(sb)->s_fc_bh;
+
+	ctx->fc_submit_bh_calls++;
+	if (is_tail)
+		ctx->fc_submit_tail_calls++;
+	set_buffer_dirty(bh);
+	if (ctx->fail_fc_wait_bufs)
+		clear_buffer_uptodate(bh);
+	else
+		set_buffer_uptodate(bh);
+	EXT4_SB(sb)->s_fc_bh = NULL;
+}
+
+static void stub_fc_issue_flush(struct block_device *bdev)
+{
+	struct kunit *test = kunit_get_current_test();
+	struct fc_test_ctx *ctx = test->priv;
+
+	ctx->fc_flush_calls++;
+	ctx->last_flushed_bdev = bdev;
+}
+
+static int mock_submit_inode_data_buffers(struct jbd2_inode *jinode)
+{
+	struct inode *inode = jinode->i_vfs_inode;
+	struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
+	struct fc_test_ctx *ctx = container_of(sbi->s_es, struct fc_test_ctx, es);
+
+	return ctx->submit_inode_data_err;
+}
+
+/*
+ * Wrapper for journal->j_fc_cleanup_callback used during ext4_fc_commit() tests.
+ * Calls the real ext4_fc_cleanup() and, on fallback commits (tid != 0), marks
+ * the mock transaction as committed so jbd2_complete_transaction() returns
+ * immediately without waiting for a kjournald2 thread.
+ */
+static void test_j_fc_cleanup_cb(journal_t *journal, int full, tid_t tid)
+{
+	ext4_fc_cleanup_test(journal, full, tid);
+	if (!full && tid != 0) {
+		write_lock(&journal->j_state_lock);
+		journal->j_commit_request = tid;
+		journal->j_commit_sequence = tid;
+		journal->j_flags &= ~JBD2_FULL_COMMIT_ONGOING;
+		write_unlock(&journal->j_state_lock);
+	}
+}
+
+/* --- Suite Setup & Teardown --- */
+
+static void fc_cleanup_all(void *priv)
+{
+	struct fc_test_ctx *ctx = priv;
+	int i;
+
+	ctx->sbi->s_mount_state &= ~EXT4_FC_REPLAY;
+	ext4_fc_cleanup_test(&ctx->journal, 1, 999999);
+	ext4_fc_replay_cleanup(ctx->sb);
+
+	for (i = 0; i < TEST_MAX_INODES; i++) {
+		if (!ctx->inodes[i].allocated)
+			continue;
+		down_write(&ctx->inodes[i].ei.i_data_sem);
+		ext4_es_remove_extent(&ctx->inodes[i].ei.vfs_inode, 0,
+				      EXT_MAX_BLOCKS);
+		up_write(&ctx->inodes[i].ei.i_data_sem);
+		ext4_fc_free_inode_snap_test(&ctx->inodes[i].ei.vfs_inode);
+	}
+
+	ext4_es_unregister_shrinker(ctx->sbi);
+	deactivate_super(ctx->sb);
+}
+
+static int fast_commit_test_init(struct kunit *test)
+{
+	struct fc_test_ctx *ctx;
+	struct ext4_sb_info *sbi;
+	struct super_block *sb;
+	struct fs_context *fc;
+	int i, err;
+
+	ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, ctx);
+
+	sbi = kunit_kzalloc(test, sizeof(*sbi), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, sbi);
+
+	fc = fs_context_for_mount(&fc_fs_type, 0);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(fc));
+	sb = sget_fc(fc, NULL, fc_set_anon);
+	put_fs_context(fc);
+	KUNIT_ASSERT_FALSE(test, IS_ERR(sb));
+
+	ctx->sb = sb;
+	ctx->sbi = sbi;
+	test->priv = ctx;
+
+	/* Superblock and ext4_sb_info setup */
+	sb->s_blocksize = TEST_BLK_SIZE;
+	sb->s_blocksize_bits = TEST_BLK_BITS;
+	sb->s_maxbytes = MAX_LFS_FILESIZE;
+	sb->s_time_gran = 1;
+	sb->s_bdi = &noop_backing_dev_info;
+	sb->s_fs_info = sbi;
+	sb->s_op = &fc_test_sops;
+#ifdef CONFIG_QUOTA
+	sb->dq_op = &fc_test_qops;
+#endif
+	strscpy(sb->s_id, "ext4-fc-test", sizeof(sb->s_id));
+
+	ctx->bdev.bd_mapping = &ctx->bd_mapping;
+	sb->s_bdev = &ctx->bdev;
+
+	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_ino = cpu_to_le32(EXT4_GOOD_OLD_FIRST_INO);
+	ctx->es.s_inode_size = cpu_to_le16(TEST_INODE_SIZE);
+	ctx->es.s_inodes_count = cpu_to_le32(1024);
+	ctx->es.s_first_data_block = cpu_to_le32(1);
+	ctx->es.s_feature_compat =
+		cpu_to_le32(EXT4_FEATURE_COMPAT_DIR_INDEX |
+			    EXT4_FEATURE_COMPAT_FAST_COMMIT);
+	ctx->es.s_feature_incompat =
+		cpu_to_le32(EXT4_FEATURE_INCOMPAT_FILETYPE |
+			    EXT4_FEATURE_INCOMPAT_EXTENTS |
+			    EXT4_FEATURE_INCOMPAT_INLINE_DATA);
+	ctx->es.s_feature_ro_compat =
+		cpu_to_le32(EXT4_FEATURE_RO_COMPAT_METADATA_CSUM);
+
+	ctx->sb_bh.b_data = (char *)&ctx->es;
+	ctx->sb_bh.b_size = sizeof(ctx->es);
+	ctx->sb_bh.b_bdev = &ctx->bdev;
+	atomic_set(&ctx->sb_bh.b_count, 1);
+	set_buffer_uptodate(&ctx->sb_bh);
+
+	sbi->s_sb = sb;
+	sbi->s_es = &ctx->es;
+	sbi->s_sbh = &ctx->sb_bh;
+	sbi->s_first_ino = EXT4_GOOD_OLD_FIRST_INO;
+	sbi->s_inode_size = TEST_INODE_SIZE;
+	sbi->s_want_extra_isize = TEST_EXTRA_ISIZE;
+	sbi->s_desc_size = EXT4_MIN_DESC_SIZE;
+	sbi->s_blocks_per_group = 32768;
+	sbi->s_inodes_per_group = 1024;
+	sbi->s_inodes_per_block = TEST_BLK_SIZE / TEST_INODE_SIZE;
+	sbi->s_csum_seed = TEST_CSUM_SEED;
+	sbi->s_cluster_bits = 0;
+	sbi->s_cluster_ratio = 1;
+	set_opt(sb, ERRORS_CONT);
+	set_opt2(sb, JOURNAL_FAST_COMMIT);
+
+	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]);
+	atomic_set(&sbi->s_fc_subtid, 0);
+	INIT_LIST_HEAD(&sbi->s_orphan);
+	mutex_init(&sbi->s_orphan_lock);
+	spin_lock_init(&sbi->s_bdev_wb_lock);
+	spin_lock_init(&sbi->s_error_lock);
+	spin_lock_init(&sbi->s_md_lock);
+	ratelimit_state_init(&sbi->s_err_ratelimit_state,
+			     DEFAULT_RATELIMIT_INTERVAL, 0);
+	ratelimit_state_init(&sbi->s_warning_ratelimit_state,
+			     DEFAULT_RATELIMIT_INTERVAL, 0);
+	ratelimit_state_init(&sbi->s_msg_ratelimit_state,
+			     DEFAULT_RATELIMIT_INTERVAL, 0);
+	up_write(&sb->s_umount);
+
+	err = ext4_es_register_shrinker(sbi);
+	KUNIT_ASSERT_EQ(test, err, 0);
+	ctx->shrinker_count = sbi->s_es_shrinker->count_objects;
+	ctx->shrinker_scan = sbi->s_es_shrinker->scan_objects;
+	WRITE_ONCE(sbi->s_es_shrinker->count_objects, fc_dummy_shrinker_count);
+	WRITE_ONCE(sbi->s_es_shrinker->scan_objects, fc_dummy_shrinker_scan);
+
+	/* Mock journal, running transaction, and handle setup */
+	rwlock_init(&ctx->journal.j_state_lock);
+	spin_lock_init(&ctx->journal.j_list_lock);
+	mutex_init(&ctx->journal.j_barrier);
+	init_waitqueue_head(&ctx->journal.j_wait_transaction_locked);
+	init_waitqueue_head(&ctx->journal.j_wait_done_commit);
+	init_waitqueue_head(&ctx->journal.j_wait_commit);
+	init_waitqueue_head(&ctx->journal.j_wait_updates);
+	init_waitqueue_head(&ctx->journal.j_wait_reserved);
+	init_waitqueue_head(&ctx->journal.j_fc_wait);
+	atomic_set(&ctx->journal.j_reserved_credits, 0);
+	ctx->journal.j_private = sb;
+	ctx->journal.j_blocksize = TEST_BLK_SIZE;
+	ctx->journal.j_dev = &ctx->bdev;
+	ctx->journal.j_fs_dev = &ctx->bdev;
+	ctx->journal.j_task = current;
+	ctx->journal.j_fc_first = 100;
+	ctx->journal.j_fc_last = 100 + TEST_MAX_FC_BUFS;
+	ctx->journal.j_fc_off = 0;
+	ctx->journal.j_fc_wbuf = ctx->fc_wbuf;
+	ctx->journal.j_stats.ts_tid = 1;
+	ctx->journal.j_commit_sequence = 9;
+	ctx->journal.j_commit_request = 9;
+	ctx->journal.j_submit_inode_data_buffers =
+		mock_submit_inode_data_buffers;
+
+	ctx->running_txn.t_tid = 10;
+	ctx->running_txn.t_state = T_RUNNING;
+	atomic_set(&ctx->running_txn.t_updates, 0);
+	ctx->journal.j_running_transaction = &ctx->running_txn;
+	ctx->handle.h_transaction = &ctx->running_txn;
+
+	sbi->s_journal = &ctx->journal;
+	ext4_fc_init(sb, &ctx->journal);
+	ctx->journal.j_fc_cleanup_callback = test_j_fc_cleanup_cb;
+
+	for (i = 0; i < TEST_MAX_FC_BUFS; i++) {
+		struct buffer_head *bh = &ctx->fc_bhs[i];
+
+		bh->b_data = ctx->fc_buf_data[i];
+		bh->b_size = TEST_BLK_SIZE;
+		bh->b_blocknr = 500 + i;
+		bh->b_bdev = &ctx->bdev;
+		atomic_set(&bh->b_count, 1);
+		set_buffer_uptodate(bh);
+		set_buffer_mapped(bh);
+	}
+
+	for (i = 0; i < TEST_MAX_BLOCKS; i++) {
+		struct buffer_head *bh = &ctx->blocks[i].bh;
+
+		bh->b_data = ctx->blocks[i].data;
+		bh->b_size = TEST_BLK_SIZE;
+		bh->b_blocknr = TEST_FIRST_PBLK + i;
+		bh->b_bdev = &ctx->bdev;
+		atomic_set(&bh->b_count, 1);
+		set_buffer_uptodate(bh);
+		set_buffer_mapped(bh);
+	}
+
+	ctx->journal_mode = EXT4_INODE_ORDERED_DATA_MODE;
+
+	kunit_activate_static_stub(test, ext4_inode_journal_mode,
+				   stub_inode_journal_mode);
+	kunit_activate_static_stub(test, ext4_da_update_reserve_space,
+				   stub_da_update_reserve_space);
+	kunit_activate_static_stub(test, ext4_da_release_space,
+				   stub_da_release_space);
+	kunit_activate_static_stub(test, ext4_get_inode_loc,
+				   stub_get_inode_loc);
+	kunit_activate_static_stub(test, ext4_get_inode_loc_noio,
+				   stub_get_inode_loc_noio);
+	kunit_activate_static_stub(test, ext4_get_fc_inode_loc,
+				   stub_get_fc_inode_loc);
+	kunit_activate_static_stub(test, __ext4_handle_dirty_metadata,
+				   stub_handle_dirty_metadata);
+	kunit_activate_static_stub(test, ext4_mark_iloc_dirty,
+				   stub_mark_iloc_dirty);
+	kunit_activate_static_stub(test, __ext4_mark_inode_dirty,
+				   stub_mark_inode_dirty);
+	kunit_activate_static_stub(test, __ext4_ext_dirty,
+				   stub_ext_dirty);
+	kunit_activate_static_stub(test, ext4_free_blocks,
+				   stub_free_blocks);
+	kunit_activate_static_stub(test, ext4_orphan_add,
+				   stub_orphan_add);
+	kunit_activate_static_stub(test, ext4_orphan_del,
+				   stub_orphan_del);
+	kunit_activate_static_stub(test, ext4_mark_inode_used,
+				   stub_mark_inode_used);
+	kunit_activate_static_stub(test, ext4_mb_mark_bb,
+				   stub_mb_mark_bb);
+	kunit_activate_static_stub(test, __ext4_iget,
+				   stub_ext4_iget);
+	kunit_activate_static_stub(test, ext4_map_blocks,
+				   stub_map_blocks);
+	kunit_activate_static_stub(test, ext4_getblk,
+				   stub_ext4_getblk);
+	kunit_activate_static_stub(test, ext4_find_extent,
+				   stub_find_extent);
+	kunit_activate_static_stub(test, ext4_ext_insert_extent,
+				   stub_insert_extent);
+	kunit_activate_static_stub(test, ext4_es_lookup_extent,
+				   stub_es_lookup_extent);
+	kunit_activate_static_stub(test, ext4_fc_get_buf,
+				   stub_fc_get_buf);
+	kunit_activate_static_stub(test, ext4_fc_submit_bh,
+				   stub_fc_submit_bh);
+	kunit_activate_static_stub(test, ext4_fc_issue_flush,
+				   stub_fc_issue_flush);
+
+	init_mock_inode_slot(ctx, &ctx->inodes[0], TEST_ROOT_INO,
+			     S_IFDIR | 0755);
+	init_mock_inode_slot(ctx, &ctx->inodes[1], TEST_DIR_INO,
+			     S_IFDIR | 0755);
+	init_mock_inode_slot(ctx, &ctx->inodes[2], TEST_FILE1_INO,
+			     S_IFREG | 0644);
+	init_mock_inode_slot(ctx, &ctx->inodes[3], TEST_FILE2_INO,
+			     S_IFREG | 0644);
+	init_mock_inode_slot(ctx, &ctx->inodes[4], TEST_SUBDIR_INO,
+			     S_IFDIR | 0755);
+
+	err = kunit_add_action_or_reset(test, fc_cleanup_all, ctx);
+	KUNIT_ASSERT_EQ(test, err, 0);
+
+	return 0;
+}
+
+/* =========================================================================
+ * 1. Initialization, Eligibility, and Snapshot Stat Helpers
+ * =========================================================================
+ */
+
+static void test_fc_init_and_eligibility(struct kunit *test)
+{
+	struct fc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct inode *inode = get_mock_inode(ctx, TEST_FILE1_INO);
+	struct ext4_inode_info *ei = EXT4_I(inode);
+	journal_t tmp_journal = {};
+	atomic64_t max_stat = ATOMIC64_INIT(0);
+	int snap_err = EXT4_FC_SNAP_ERR_NONE;
+
+	/*
+	 * 1) ext4_fc_init sets j_fc_replay_callback unconditionally, but sets
+	 *    j_fc_cleanup_callback only when JOURNAL_FAST_COMMIT is enabled.
+	 */
+	clear_opt2(sb, JOURNAL_FAST_COMMIT);
+	ext4_fc_init(sb, &tmp_journal);
+	KUNIT_EXPECT_NOT_NULL(test, tmp_journal.j_fc_replay_callback);
+	KUNIT_EXPECT_NULL(test, tmp_journal.j_fc_cleanup_callback);
+
+	set_opt2(sb, JOURNAL_FAST_COMMIT);
+	ext4_fc_init(sb, &tmp_journal);
+	KUNIT_EXPECT_NOT_NULL(test, tmp_journal.j_fc_replay_callback);
+	KUNIT_EXPECT_NOT_NULL(test, tmp_journal.j_fc_cleanup_callback);
+
+	/* 2) ext4_fc_init_inode clears state and resets ranges */
+	ei->i_fc_lblk_start = 42;
+	ei->i_fc_lblk_len = 10;
+	ext4_set_inode_state(inode, EXT4_STATE_FC_COMMITTING);
+	ext4_set_inode_state(inode, EXT4_STATE_FC_REQUEUE);
+	ext4_fc_init_inode(inode);
+	KUNIT_EXPECT_EQ(test, ei->i_fc_lblk_start, 0U);
+	KUNIT_EXPECT_EQ(test, ei->i_fc_lblk_len, 0U);
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_inode_state(inode, EXT4_STATE_FC_COMMITTING));
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_inode_state(inode, EXT4_STATE_FC_REQUEUE));
+	KUNIT_EXPECT_TRUE(test, list_empty(&ei->i_fc_list));
+	KUNIT_EXPECT_TRUE(test, list_empty(&ei->i_fc_dilist));
+	KUNIT_EXPECT_NULL(test, ei->i_fc_snap);
+
+	/* 3) ext4_fc_disabled and ext4_fc_eligible transitions */
+	KUNIT_EXPECT_FALSE(test, ext4_fc_disabled_test(sb));
+	KUNIT_EXPECT_TRUE(test, ext4_fc_eligible_test(sb));
+
+	ctx->sbi->s_mount_state |= EXT4_FC_REPLAY;
+	KUNIT_EXPECT_TRUE(test, ext4_fc_disabled_test(sb));
+	KUNIT_EXPECT_FALSE(test, ext4_fc_eligible_test(sb));
+	ctx->sbi->s_mount_state &= ~EXT4_FC_REPLAY;
+
+	ext4_set_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+	KUNIT_EXPECT_FALSE(test, ext4_fc_disabled_test(sb));
+	KUNIT_EXPECT_FALSE(test, ext4_fc_eligible_test(sb));
+	ext4_clear_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+
+	clear_opt2(sb, JOURNAL_FAST_COMMIT);
+	KUNIT_EXPECT_TRUE(test, ext4_fc_disabled_test(sb));
+	KUNIT_EXPECT_FALSE(test, ext4_fc_eligible_test(sb));
+	set_opt2(sb, JOURNAL_FAST_COMMIT);
+
+	/* 4) ext4_fc_set_snap_err preserves first error */
+	ext4_fc_set_snap_err_test(&snap_err, EXT4_FC_SNAP_ERR_ES_MISS);
+	KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_ES_MISS);
+	ext4_fc_set_snap_err_test(&snap_err, EXT4_FC_SNAP_ERR_NOMEM);
+	KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_ES_MISS);
+
+	/* 5) ext4_fc_snap_stats_update_max monotonically updates max */
+	ext4_fc_snap_stats_update_max_test(&max_stat, 100);
+	KUNIT_EXPECT_EQ(test, atomic64_read(&max_stat), 100LL);
+	ext4_fc_snap_stats_update_max_test(&max_stat, 50);
+	KUNIT_EXPECT_EQ(test, atomic64_read(&max_stat), 100LL);
+	ext4_fc_snap_stats_update_max_test(&max_stat, 250);
+	KUNIT_EXPECT_EQ(test, atomic64_read(&max_stat), 250LL);
+}
+
+/* =========================================================================
+ * 2. Ineligibility Marking (ext4_fc_mark_ineligible)
+ * =========================================================================
+ */
+
+static void test_fc_mark_ineligible(struct kunit *test)
+{
+	struct fc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ext4_sb_info *sbi = ctx->sbi;
+
+	/* 1) No-op when fast commit is disabled */
+	clear_opt2(sb, JOURNAL_FAST_COMMIT);
+	ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_XATTR, &ctx->handle);
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+	KUNIT_EXPECT_EQ(test,
+			sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_XATTR],
+			0U);
+	set_opt2(sb, JOURNAL_FAST_COMMIT);
+
+	/* 2) Valid handle records t_tid and sets EXT4_MF_FC_INELIGIBLE */
+	ctx->running_txn.t_tid = 10;
+	ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_XATTR, &ctx->handle);
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_ineligible_tid, 10U);
+	KUNIT_EXPECT_EQ(test,
+			sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_XATTR],
+			1U);
+
+	/* 3) Higher TID advances s_fc_ineligible_tid; lower TID does not */
+	ctx->running_txn.t_tid = 15;
+	ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_RESIZE, &ctx->handle);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_ineligible_tid, 15U);
+	KUNIT_EXPECT_EQ(test,
+			sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_RESIZE],
+			1U);
+
+	ctx->running_txn.t_tid = 12;
+	ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_RESIZE, &ctx->handle);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_ineligible_tid, 15U);
+	KUNIT_EXPECT_EQ(test,
+			sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_RESIZE],
+			2U);
+
+	/* 4) NULL handle uses journal->j_running_transaction when present */
+	ctx->running_txn.t_tid = 20;
+	ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_NOMEM, NULL);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_ineligible_tid, 20U);
+	KUNIT_EXPECT_EQ(test,
+			sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_NOMEM],
+			1U);
+
+	/* 5) NULL handle with no running transaction sets flag without tid update */
+	ext4_clear_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+	sbi->s_fc_ineligible_tid = 0;
+	ctx->journal.j_running_transaction = NULL;
+	ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_RENAME_DIR, NULL);
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_ineligible_tid, 0U);
+	KUNIT_EXPECT_EQ(test,
+			sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_RENAME_DIR],
+			1U);
+
+	ctx->journal.j_running_transaction = &ctx->running_txn;
+	ctx->running_txn.t_tid = 10;
+	ext4_clear_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+}
+
+/* =========================================================================
+ * 3. Inode and Logical Range Tracking
+ * =========================================================================
+ */
+
+static void test_fc_track_inode_and_range(struct kunit *test)
+{
+	struct fc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ext4_sb_info *sbi = ctx->sbi;
+	struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+	struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+	struct inode *file2 = get_mock_inode(ctx, TEST_FILE2_INO);
+	struct ext4_inode_info *ei1 = EXT4_I(file1);
+	struct ext4_inode_info *ei2 = EXT4_I(file2);
+
+	/* 1) Directory inodes are ignored by track_inode and track_range */
+	ext4_fc_track_inode(&ctx->handle, dir);
+	ext4_fc_track_range(&ctx->handle, dir, 0, 5);
+	KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(dir)->i_fc_list));
+
+	/* 2) Data journalling inode marks fs ineligible */
+	ctx->journal_mode = EXT4_INODE_JOURNAL_DATA_MODE;
+	ext4_fc_track_inode(&ctx->handle, file1);
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+	KUNIT_EXPECT_EQ(test,
+			sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_INODE_JOURNAL_DATA],
+			1U);
+	KUNIT_EXPECT_TRUE(test, list_empty(&ei1->i_fc_list));
+	ctx->journal_mode = EXT4_INODE_ORDERED_DATA_MODE;
+	ext4_clear_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+	sbi->s_fc_ineligible_tid = 0;
+
+	/* 3) Inline data inode marks fs ineligible on track_range */
+	ext4_set_inode_flag(file1, EXT4_INODE_INLINE_DATA);
+	ext4_set_inode_state(file1, EXT4_STATE_MAY_INLINE_DATA);
+	ei1->i_inline_off = 164;
+	ext4_fc_track_range(&ctx->handle, file1, 0, 1);
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+	KUNIT_EXPECT_EQ(test,
+			sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_XATTR],
+			1U);
+	ext4_clear_inode_flag(file1, EXT4_INODE_INLINE_DATA);
+	ext4_clear_inode_state(file1, EXT4_STATE_MAY_INLINE_DATA);
+	ei1->i_inline_off = 0;
+	ext4_clear_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+	sbi->s_fc_ineligible_tid = 0;
+
+	/* 4) Special inode (< EXT4_FIRST_INO) does not record range */
+	file1->i_ino = EXT4_ROOT_INO;
+	ext4_fc_track_range(&ctx->handle, file1, 5, 10);
+	KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 0U);
+	list_del_init(&ei1->i_fc_list);
+	file1->i_ino = TEST_FILE1_INO;
+	ei1->i_sync_tid = 0;
+
+	/*
+	 * 5) Range tracking within the same TID merges disjoint and overlapping
+	 *    ranges into their bounding interval, and subsequent track_inode
+	 *    in the same TID preserves the range.
+	 */
+	ctx->running_txn.t_tid = 10;
+	ext4_fc_track_inode(&ctx->handle, file1);
+	KUNIT_EXPECT_EQ(test, ei1->i_sync_tid, 10U);
+	KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 0U);
+	KUNIT_EXPECT_FALSE(test, list_empty(&ei1->i_fc_list));
+
+	/* Initial range [10, 20] -> start=10, len=11 */
+	ext4_fc_track_range(&ctx->handle, file1, 10, 20);
+	KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_start, 10U);
+	KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 11U);
+
+	/* Expand right [25, 30] -> [10, 30], start=10, len=21 */
+	ext4_fc_track_range(&ctx->handle, file1, 25, 30);
+	KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_start, 10U);
+	KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 21U);
+
+	/* Expand left [4, 8] -> [4, 30], start=4, len=27 */
+	ext4_fc_track_range(&ctx->handle, file1, 4, 8);
+	KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_start, 4U);
+	KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 27U);
+
+	/* Contained sub-range [12, 18] -> unchanged [4, 30] */
+	ext4_fc_track_range(&ctx->handle, file1, 12, 18);
+	KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_start, 4U);
+	KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 27U);
+
+	/* Calling track_inode again in same TID keeps tracked range intact */
+	ext4_fc_track_inode(&ctx->handle, file1);
+	KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_start, 4U);
+	KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 27U);
+
+	/*
+	 * 6) Tracking while EXT4_STATE_FC_COMMITTING is set marks the inode
+	 *    with EXT4_STATE_FC_REQUEUE, and tracking in a new TID resets the
+	 *    previous range.
+	 */
+	ext4_set_inode_state(file1, EXT4_STATE_FC_COMMITTING);
+	ctx->running_txn.t_tid = 11;
+	ext4_fc_track_range(&ctx->handle, file1, 50, 55);
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_test_inode_state(file1, EXT4_STATE_FC_REQUEUE));
+	KUNIT_EXPECT_EQ(test, ei1->i_sync_tid, 11U);
+	KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_start, 50U);
+	KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 6U);
+	ext4_clear_inode_state(file1, EXT4_STATE_FC_COMMITTING);
+	ext4_clear_inode_state(file1, EXT4_STATE_FC_REQUEUE);
+
+	/*
+	 * 7) Tracking a new inode while JBD2_FAST_COMMIT_ONGOING is set
+	 *    enqueues it on FC_Q_STAGING rather than FC_Q_MAIN.
+	 */
+	ctx->journal.j_flags |= JBD2_FAST_COMMIT_ONGOING;
+	ext4_fc_track_inode(&ctx->handle, file2);
+	KUNIT_EXPECT_FALSE(test, list_empty(&sbi->s_fc_q[FC_Q_STAGING]));
+	KUNIT_EXPECT_PTR_EQ(test,
+			    list_first_entry(&sbi->s_fc_q[FC_Q_STAGING],
+					     struct ext4_inode_info, i_fc_list),
+			    ei2);
+	ctx->journal.j_flags &= ~JBD2_FAST_COMMIT_ONGOING;
+}
+
+/* =========================================================================
+ * 4. Dentry Update Tracking and Inode Deletion (ext4_fc_del)
+ * =========================================================================
+ */
+
+struct fc_del_wait_work {
+	struct work_struct work;
+	struct inode *inode;
+};
+
+static void fc_del_unblock_worker(struct work_struct *work)
+{
+	struct fc_del_wait_work *w =
+		container_of(work, struct fc_del_wait_work, work);
+	struct inode *inode = w->inode;
+	int alloc_ctx;
+
+	/*
+	 * Simulate the commit thread finishing data flush and transitioning
+	 * FLUSHING_DATA -> COMMITTING -> done while ext4_fc_del() is waiting.
+	 */
+	alloc_ctx = ext4_fc_lock(inode->i_sb);
+	ext4_clear_inode_state(inode, EXT4_STATE_FC_FLUSHING_DATA);
+	ext4_set_inode_state(inode, EXT4_STATE_FC_COMMITTING);
+	ext4_fc_wake_inode_state_test(inode, EXT4_STATE_FC_FLUSHING_DATA);
+	ext4_fc_unlock(inode->i_sb, alloc_ctx);
+
+	alloc_ctx = ext4_fc_lock(inode->i_sb);
+	ext4_clear_inode_state(inode, EXT4_STATE_FC_COMMITTING);
+	ext4_fc_wake_inode_state_test(inode, EXT4_STATE_FC_COMMITTING);
+	ext4_fc_unlock(inode->i_sb, alloc_ctx);
+}
+
+static void test_fc_track_dentry_and_del(struct kunit *test)
+{
+	struct fc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ext4_sb_info *sbi = ctx->sbi;
+	struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+	struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+	struct inode *file2 = get_mock_inode(ctx, TEST_FILE2_INO);
+	struct ext4_inode_info *ei1 = EXT4_I(file1);
+	struct dentry d_parent, d_f1, d_f1_link, d_f2;
+	struct ext4_fc_dentry_update *fcd;
+	struct fc_del_wait_work wait_work;
+
+	init_mock_dentry(&d_parent, NULL, "dir", dir, sb);
+	init_mock_dentry(&d_f1, &d_parent, "file1", file1, sb);
+	init_mock_dentry(&d_f1_link, &d_parent, "file1_ln", file1, sb);
+	init_mock_dentry(&d_f2, &d_parent, "file2", file2, sb);
+
+	/* 1) Encrypted parent directory marks fs ineligible */
+	dir->i_flags |= S_ENCRYPTED;
+	ext4_fc_track_create(&ctx->handle, &d_f1);
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+	KUNIT_EXPECT_EQ(test,
+			sbi->s_fc_stats.fc_ineligible_reason_count[EXT4_FC_REASON_ENCRYPTED_FILENAME],
+			1U);
+	KUNIT_EXPECT_TRUE(test, list_empty(&sbi->s_fc_dentry_q[FC_Q_MAIN]));
+	dir->i_flags &= ~S_ENCRYPTED;
+	ext4_clear_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+	sbi->s_fc_ineligible_tid = 0;
+
+	/* 2) Track create, link, and unlink onto MAIN and STAGING queues */
+	ext4_fc_track_create(&ctx->handle, &d_f1);
+	KUNIT_EXPECT_FALSE(test, list_empty(&sbi->s_fc_dentry_q[FC_Q_MAIN]));
+	KUNIT_EXPECT_FALSE(test, list_empty(&ei1->i_fc_dilist));
+
+	fcd = list_first_entry(&sbi->s_fc_dentry_q[FC_Q_MAIN],
+			       struct ext4_fc_dentry_update, fcd_list);
+	KUNIT_EXPECT_EQ(test, fcd->fcd_op, EXT4_FC_TAG_CREAT);
+	KUNIT_EXPECT_EQ(test, fcd->fcd_parent, (int)TEST_DIR_INO);
+	KUNIT_EXPECT_EQ(test, fcd->fcd_ino, (int)TEST_FILE1_INO);
+	KUNIT_EXPECT_STREQ(test, (const char *)fcd->fcd_name.name.name, "file1");
+
+	ext4_fc_track_link(&ctx->handle, file1, &d_f1_link);
+	ext4_fc_track_unlink(&ctx->handle, &d_f1_link);
+
+	ctx->journal.j_flags |= JBD2_FULL_COMMIT_ONGOING;
+	ext4_fc_track_create(&ctx->handle, &d_f2);
+	KUNIT_EXPECT_FALSE(test, list_empty(&sbi->s_fc_dentry_q[FC_Q_STAGING]));
+	ctx->journal.j_flags &= ~JBD2_FULL_COMMIT_ONGOING;
+
+	/*
+	 * 3) ext4_fc_del removes file2's CREAT entry from s_fc_dentry_q and
+	 *    i_fc_dilist even when file2 is not on s_fc_q.
+	 */
+	ext4_fc_del(file2);
+	KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(file2)->i_fc_dilist));
+	KUNIT_EXPECT_TRUE(test, list_empty(&sbi->s_fc_dentry_q[FC_Q_STAGING]));
+
+	/*
+	 * 4) ext4_fc_del waits for EXT4_STATE_FC_FLUSHING_DATA and
+	 *    EXT4_STATE_FC_COMMITTING to clear before deleting file1.
+	 */
+	ext4_fc_track_inode(&ctx->handle, file1);
+	ext4_set_inode_state(file1, EXT4_STATE_FC_FLUSHING_DATA);
+	wait_work.inode = file1;
+	INIT_WORK_ONSTACK(&wait_work.work, fc_del_unblock_worker);
+	schedule_work(&wait_work.work);
+
+	ext4_fc_del(file1);
+	flush_work(&wait_work.work);
+	destroy_work_on_stack(&wait_work.work);
+
+	KUNIT_EXPECT_TRUE(test, list_empty(&ei1->i_fc_list));
+	KUNIT_EXPECT_TRUE(test, list_empty(&ei1->i_fc_dilist));
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_inode_state(file1, EXT4_STATE_FC_FLUSHING_DATA));
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_inode_state(file1, EXT4_STATE_FC_COMMITTING));
+}
+
+/* =========================================================================
+ * 5. Space Reservation, Padding, and TLV Writing
+ * =========================================================================
+ */
+
+static void test_fc_reserve_space_and_tlv_writing(struct kunit *test)
+{
+	struct fc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ext4_sb_info *sbi = ctx->sbi;
+	struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+	struct ext4_fc_tl tl;
+	struct ext4_fc_tail tail;
+	u8 payload[600];
+	u8 *dst;
+	u32 crc = 0;
+
+	memset(payload, 0xAB, sizeof(payload));
+
+	/* 1) Request exceeding bsize - EXT4_FC_TAG_BASE_LEN fails immediately */
+	dst = ext4_fc_reserve_space_test(sb,
+					 TEST_BLK_SIZE - EXT4_FC_TAG_BASE_LEN + 1,
+					 &crc);
+	KUNIT_EXPECT_NULL(test, dst);
+	KUNIT_EXPECT_EQ(test, ctx->fc_get_buf_calls, 0);
+
+	/* 2) ext4_fc_get_buf failure on initial block returns NULL */
+	ctx->fail_fc_get_buf = -ENOSPC;
+	dst = ext4_fc_reserve_space_test(sb, 32, &crc);
+	KUNIT_EXPECT_NULL(test, dst);
+	ctx->fail_fc_get_buf = 0;
+
+	/*
+	 * 3) Add a 600-byte TLV into block 0 (takes 604 bytes), then add
+	 *    another 600-byte TLV. The second TLV does not fit in the
+	 *    remaining 420 bytes of block 0, so ext4_fc_reserve_space writes
+	 *    an EXT4_FC_TAG_PAD TLV of length 416 (420 - 4), submits block 0,
+	 *    and allocates block 1.
+	 */
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_HEAD,
+					       sizeof(payload), payload, &crc));
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_bytes,
+			(unsigned int)(EXT4_FC_TAG_BASE_LEN + sizeof(payload)));
+	KUNIT_EXPECT_EQ(test, ctx->fc_submit_bh_calls, 0U);
+
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_HEAD,
+					       sizeof(payload), payload, &crc));
+	KUNIT_EXPECT_EQ(test, ctx->fc_submit_bh_calls, 1U);
+	KUNIT_EXPECT_EQ(test, ctx->fc_submit_tail_calls, 0U);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_bytes,
+			(unsigned int)(TEST_BLK_SIZE + EXT4_FC_TAG_BASE_LEN +
+				       sizeof(payload)));
+
+	/* Verify PAD TLV was written at offset 604 of block 0 */
+	memcpy(&tl, ctx->fc_buf_data[0] + EXT4_FC_TAG_BASE_LEN + sizeof(payload),
+	       EXT4_FC_TAG_BASE_LEN);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(tl.fc_tag), (u16)EXT4_FC_TAG_PAD);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(tl.fc_len),
+			(u16)(TEST_BLK_SIZE - 2 * EXT4_FC_TAG_BASE_LEN -
+			      sizeof(payload)));
+
+	/* 4) Write tail tag into block 1 and verify rounding + tail submission */
+	KUNIT_EXPECT_EQ(test, ext4_fc_write_tail_test(sb, crc), 0);
+	KUNIT_EXPECT_EQ(test, ctx->fc_submit_bh_calls, 2U);
+	KUNIT_EXPECT_EQ(test, ctx->fc_submit_tail_calls, 1U);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_bytes, (unsigned int)(2 * TEST_BLK_SIZE));
+
+	memcpy(&tl, ctx->fc_buf_data[1] + EXT4_FC_TAG_BASE_LEN + sizeof(payload),
+	       EXT4_FC_TAG_BASE_LEN);
+	memcpy(&tail,
+	       ctx->fc_buf_data[1] + 2 * EXT4_FC_TAG_BASE_LEN + sizeof(payload),
+	       sizeof(tail));
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(tl.fc_tag), (u16)EXT4_FC_TAG_TAIL);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(tail.fc_tid), 10U);
+
+	/* 5) Missing inode snapshot increments snap_fail_no_snap */
+	KUNIT_EXPECT_EQ(test, ext4_fc_write_inode_test(file1, &crc),
+			-ECANCELED);
+	KUNIT_EXPECT_EQ(test, ext4_fc_write_inode_data_test(file1, &crc),
+			-ECANCELED);
+	KUNIT_EXPECT_EQ(test,
+			atomic64_read(&sbi->s_fc_snap_stats.snap_fail_no_snap),
+			2LL);
+}
+
+/* =========================================================================
+ * 6. Inode and Extent Status Range Snapshotting
+ * =========================================================================
+ */
+
+static void test_fc_snapshot_inode_and_ranges(struct kunit *test)
+{
+	struct fc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ext4_sb_info *sbi = ctx->sbi;
+	struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+	struct inode *file2 = get_mock_inode(ctx, TEST_FILE2_INO);
+	struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+	struct ext4_inode_info *ei1 = EXT4_I(file1);
+	struct dentry d_parent, d_f2;
+	struct inode **inodes = NULL;
+	unsigned int nr_inodes = 0, nr_ranges = 0;
+	int snap_err = EXT4_FC_SNAP_ERR_NONE;
+	u32 crc = 0;
+
+	/* 1) ext4_get_inode_loc_noio failure records EXT4_FC_SNAP_ERR_INODE_LOC */
+	ctx->fail_get_inode_loc_noio = -EAGAIN;
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_snapshot_inode_test(file1, 0, &nr_ranges,
+						    &snap_err),
+			-EAGAIN);
+	KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_INODE_LOC);
+	KUNIT_EXPECT_EQ(test,
+			atomic64_read(&sbi->s_fc_snap_stats.snap_fail_inode_loc),
+			1LL);
+	ctx->fail_get_inode_loc_noio = 0;
+
+	/* 2) ES miss on tracked range records EXT4_FC_SNAP_ERR_ES_MISS */
+	snap_err = EXT4_FC_SNAP_ERR_NONE;
+	ext4_fc_track_range(&ctx->handle, file1, 0, 3);
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_snapshot_inode_test(file1, 0, &nr_ranges,
+						    &snap_err),
+			-EAGAIN);
+	KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_ES_MISS);
+	KUNIT_EXPECT_EQ(test,
+			atomic64_read(&sbi->s_fc_snap_stats.snap_fail_es_miss),
+			1LL);
+
+	/* 3) Delayed extent in ES tree records EXT4_FC_SNAP_ERR_ES_DELAYED */
+	snap_err = EXT4_FC_SNAP_ERR_NONE;
+	down_write(&ei1->i_data_sem);
+	ext4_es_insert_delayed_extent(file1, 0, 4, false, false);
+	up_write(&ei1->i_data_sem);
+	ext4_fc_track_range(&ctx->handle, file1, 0, 3);
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_snapshot_inode_test(file1, 0, &nr_ranges,
+						    &snap_err),
+			-EAGAIN);
+	KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_ES_DELAYED);
+	KUNIT_EXPECT_EQ(test,
+			atomic64_read(&sbi->s_fc_snap_stats.snap_fail_es_delayed),
+			1LL);
+	down_write(&ei1->i_data_sem);
+	ext4_es_remove_extent(file1, 0, EXT_MAX_BLOCKS);
+	up_write(&ei1->i_data_sem);
+
+	/* 4) Unsupported ES status records EXT4_FC_SNAP_ERR_ES_OTHER */
+	snap_err = EXT4_FC_SNAP_ERR_NONE;
+	ctx->use_custom_es_lookup = true;
+	ctx->custom_es_ret = 1;
+	ctx->custom_es.es_lblk = 0;
+	ctx->custom_es.es_len = 4;
+	ctx->custom_es.es_pblk = 0;
+	ext4_es_set_referenced(&ctx->custom_es);
+	ext4_fc_track_range(&ctx->handle, file1, 0, 3);
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_snapshot_inode_test(file1, 0, &nr_ranges,
+						    &snap_err),
+			-EAGAIN);
+	KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_ES_OTHER);
+	KUNIT_EXPECT_EQ(test,
+			atomic64_read(&sbi->s_fc_snap_stats.snap_fail_es_other),
+			1LL);
+	ctx->use_custom_es_lookup = false;
+
+	/* 5) Exceeding EXT4_FC_SNAPSHOT_MAX_RANGES returns -E2BIG */
+	snap_err = EXT4_FC_SNAP_ERR_NONE;
+	down_write(&ei1->i_data_sem);
+	ext4_es_insert_extent(file1, 0, 4, 200, EXTENT_STATUS_WRITTEN, false);
+	up_write(&ei1->i_data_sem);
+	ext4_fc_track_range(&ctx->handle, file1, 0, 3);
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_snapshot_inode_test(file1, 2048, &nr_ranges,
+						    &snap_err),
+			-E2BIG);
+	KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_RANGES_CAP);
+	KUNIT_EXPECT_EQ(test,
+			atomic64_read(&sbi->s_fc_snap_stats.snap_fail_ranges_cap),
+			1LL);
+
+	/*
+	 * 6) Snapshot mixed written, hole, unwritten, and large (>32768)
+	 *    extents, verifying automatic splitting at EXT_INIT_MAX_LEN (32768)
+	 *    and EXT_UNWRITTEN_MAX_LEN (32767).
+	 */
+	down_write(&ei1->i_data_sem);
+	ext4_es_remove_extent(file1, 0, EXT_MAX_BLOCKS);
+	/* [0, 3]: written at pblk 200 */
+	ext4_es_insert_extent(file1, 0, 4, 200, EXTENT_STATUS_WRITTEN, false);
+	/* [4, 5]: hole */
+	ext4_es_insert_extent(file1, 4, 2, 0, EXTENT_STATUS_HOLE, false);
+	/* [6, 40005]: 40000 unwritten blocks at pblk 1000 (splits into 32767 + 7233) */
+	ext4_es_insert_extent(file1, 6, 40000, 1000, EXTENT_STATUS_UNWRITTEN,
+			      false);
+	up_write(&ei1->i_data_sem);
+
+	snap_err = EXT4_FC_SNAP_ERR_NONE;
+	ext4_fc_track_range(&ctx->handle, file1, 1, 40005);
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_snapshot_inode_test(file1, 0, &nr_ranges,
+						    &snap_err),
+			0);
+	/* 1 written [1..3] + 1 hole [4..5] + 2 split unwritten [6..32772, 32773..40005] = 4 */
+	KUNIT_EXPECT_EQ(test, nr_ranges, 4U);
+	KUNIT_EXPECT_NOT_NULL(test, ei1->i_fc_snap);
+
+	/* Serialize the snapshotted inode and ranges into fast commit buffer */
+	sbi->s_fc_bytes = 0;
+	ctx->journal.j_fc_off = 0;
+	KUNIT_EXPECT_EQ(test, ext4_fc_write_inode_data_test(file1, &crc), 0);
+	KUNIT_EXPECT_EQ(test, ext4_fc_write_inode_test(file1, &crc), 0);
+
+	/*
+	 * 7) Count and snapshot both s_fc_q[MAIN] inodes and create-only
+	 *    inodes from s_fc_dentry_q[MAIN].
+	 */
+	init_mock_dentry(&d_parent, NULL, "dir", dir, sb);
+	init_mock_dentry(&d_f2, &d_parent, "file2", file2, sb);
+	ext4_fc_track_create(&ctx->handle, &d_f2);
+	KUNIT_EXPECT_TRUE(test, list_empty(&EXT4_I(file2)->i_fc_list));
+	KUNIT_EXPECT_EQ(test, ext4_fc_count_snapshot_inodes_test(sb), 2U);
+
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_alloc_snapshot_inodes_test(sb, &inodes,
+							   &nr_inodes),
+			0);
+	KUNIT_EXPECT_EQ(test, nr_inodes, 2U);
+	KUNIT_ASSERT_NOT_NULL(test, inodes);
+
+	/* Capacity smaller than queued inode count returns -E2BIG */
+	snap_err = EXT4_FC_SNAP_ERR_NONE;
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_snapshot_inodes_test(&ctx->journal, inodes, 1,
+						     &nr_inodes, &nr_ranges,
+						     &snap_err),
+			-E2BIG);
+	KUNIT_EXPECT_EQ(test, snap_err, (int)EXT4_FC_SNAP_ERR_INODES_CAP);
+
+	snap_err = EXT4_FC_SNAP_ERR_NONE;
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_snapshot_inodes_test(&ctx->journal, inodes, 2,
+						     &nr_inodes, &nr_ranges,
+						     &snap_err),
+			0);
+	KUNIT_EXPECT_EQ(test, nr_inodes, 2U);
+	/* Create-only inode (file2) is marked EXT4_STATE_FC_COMMITTING */
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_test_inode_state(file2, EXT4_STATE_FC_COMMITTING));
+	KUNIT_EXPECT_NOT_NULL(test, EXT4_I(file2)->i_fc_snap);
+	kvfree(inodes);
+}
+
+/* =========================================================================
+ * 7. Fast Commit Cleanup, Stats, and Procfs Output (ext4_fc_info_show)
+ * =========================================================================
+ */
+
+static void test_fc_cleanup_and_stats(struct kunit *test)
+{
+	struct fc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ext4_sb_info *sbi = ctx->sbi;
+	struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+	struct inode *file2 = get_mock_inode(ctx, TEST_FILE2_INO);
+	struct ext4_inode_info *ei1 = EXT4_I(file1);
+	struct ext4_inode_info *ei2 = EXT4_I(file2);
+	struct seq_file seq = {};
+	char *buf;
+
+	/*
+	 * 1) Fast commit cleanup (full = 0):
+	 *    - file1 (no FC_REQUEUE) is removed from FC_Q_MAIN and reset.
+	 *    - file2 (FC_REQUEUE set) is requeued and preserves its range.
+	 */
+	ctx->running_txn.t_tid = 10;
+	ext4_fc_track_range(&ctx->handle, file1, 2, 5);
+	ext4_fc_track_range(&ctx->handle, file2, 8, 12);
+	ext4_set_inode_state(file1, EXT4_STATE_FC_COMMITTING);
+	ext4_set_inode_state(file2, EXT4_STATE_FC_COMMITTING);
+	ext4_set_inode_state(file2, EXT4_STATE_FC_REQUEUE);
+	sbi->s_fc_bytes = 512;
+
+	ext4_fc_cleanup_test(&ctx->journal, 0, 10);
+	KUNIT_EXPECT_TRUE(test, list_empty(&ei1->i_fc_list));
+	KUNIT_EXPECT_EQ(test, ei1->i_fc_lblk_len, 0U);
+	KUNIT_EXPECT_FALSE(test, list_empty(&ei2->i_fc_list));
+	KUNIT_EXPECT_EQ(test, ei2->i_fc_lblk_start, 8U);
+	KUNIT_EXPECT_EQ(test, ei2->i_fc_lblk_len, 5U);
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_inode_state(file2, EXT4_STATE_FC_REQUEUE));
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_inode_state(file2, EXT4_STATE_FC_COMMITTING));
+	/* Fast commit cleanup does not reset s_fc_bytes */
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_bytes, 512U);
+
+	/*
+	 * 2) Full commit cleanup (full = 1):
+	 *    - Inodes with i_sync_tid > tid are requeued; inodes with
+	 *      i_sync_tid <= tid are removed and reset.
+	 *    - s_fc_ineligible_tid is cleared only when tid >= s_fc_ineligible_tid.
+	 *    - s_fc_bytes is reset to 0.
+	 */
+	ctx->running_txn.t_tid = 15;
+	ext4_fc_track_range(&ctx->handle, file1, 1, 3);
+	ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_XATTR, &ctx->handle);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_ineligible_tid, 15U);
+
+	/* Full commit of TID 10: file2 (tid 10) removed, file1 (tid 15) kept */
+	ext4_fc_cleanup_test(&ctx->journal, 1, 10);
+	KUNIT_EXPECT_TRUE(test, list_empty(&ei2->i_fc_list));
+	KUNIT_EXPECT_FALSE(test, list_empty(&ei1->i_fc_list));
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_bytes, 0U);
+
+	/* Full commit of TID 15: file1 removed and ineligibility cleared */
+	ext4_fc_cleanup_test(&ctx->journal, 1, 15);
+	KUNIT_EXPECT_TRUE(test, list_empty(&ei1->i_fc_list));
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_mount_flag(sb, EXT4_MF_FC_INELIGIBLE));
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_ineligible_tid, 0U);
+
+	/*
+	 * 3) ext4_fc_update_stats and ext4_fc_info_show formatting
+	 */
+	memset(&sbi->s_fc_stats, 0, sizeof(sbi->s_fc_stats));
+	ext4_fc_update_stats_test(sb, EXT4_FC_STATUS_OK, 40000, 2, 10);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_num_commits, 1UL);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_numblks, 2UL);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.s_fc_avg_commit_time, 40000ULL);
+
+	/* EWMA update: (80000 + 40000 * 3) / 4 = 50000 */
+	ext4_fc_update_stats_test(sb, EXT4_FC_STATUS_OK, 80000, 3, 10);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_num_commits, 2UL);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_numblks, 5UL);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.s_fc_avg_commit_time, 50000ULL);
+
+	ext4_fc_update_stats_test(sb, EXT4_FC_STATUS_INELIGIBLE, 0, 0, 10);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_ineligible_commits, 1UL);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_failed_commits, 0UL);
+
+	ext4_fc_update_stats_test(sb, EXT4_FC_STATUS_FAILED, 0, 0, 10);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_ineligible_commits, 2UL);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_failed_commits, 1UL);
+
+	ext4_fc_update_stats_test(sb, EXT4_FC_STATUS_SKIPPED, 0, 0, 10);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_skipped_commits, 1UL);
+
+	buf = kunit_kzalloc(test, PAGE_SIZE, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, buf);
+	seq.buf = buf;
+	seq.size = PAGE_SIZE;
+	seq.private = sb;
+
+	/* Non-start token produces no output */
+	KUNIT_EXPECT_EQ(test, ext4_fc_info_show(&seq, NULL), 0);
+	KUNIT_EXPECT_EQ(test, seq.count, 0UL);
+
+	/* SEQ_START_TOKEN outputs full stats table */
+	KUNIT_EXPECT_EQ(test, ext4_fc_info_show(&seq, SEQ_START_TOKEN), 0);
+	KUNIT_EXPECT_GT(test, seq.count, 0UL);
+	KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "fc stats:\n2 commits\n2 ineligible\n5 numblks\n50us avg_commit_time"));
+	KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "Ineligible reasons:"));
+	KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "Snapshot stats:"));
+	KUNIT_EXPECT_NOT_NULL(test, strstr(buf, "Snapshot failures:"));
+}
+
+/* =========================================================================
+ * 8. Top-Level Commit Paths and Fallbacks (ext4_fc_commit)
+ * =========================================================================
+ */
+
+static void test_fc_commit_paths(struct kunit *test)
+{
+	struct fc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ext4_sb_info *sbi = ctx->sbi;
+	struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+	struct ext4_inode_info *ei1 = EXT4_I(file1);
+	struct ext4_fc_tl tl;
+
+	/* 1) Already-committed TID is skipped (-EALREADY) */
+	ctx->journal.j_commit_sequence = 10;
+	KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_skipped_commits, 1UL);
+
+	/* 1b) JOURNAL_FAST_COMMIT disabled delegates to jbd2_complete_transaction */
+	clear_opt2(sb, JOURNAL_FAST_COMMIT);
+	KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_skipped_commits, 1UL);
+	set_opt2(sb, JOURNAL_FAST_COMMIT);
+
+	/* 1c) jbd2_fc_begin_commit failure (ts_tid == 0 -> -EINVAL) records FAILED */
+	ctx->journal.j_commit_sequence = 9;
+	ctx->journal.j_commit_request = 10;
+	ctx->journal.j_stats.ts_tid = 0;
+	ctx->journal.j_running_transaction = NULL;
+	KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_failed_commits, 1UL);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_ineligible_commits, 1UL);
+	ctx->journal.j_stats.ts_tid = 1;
+	ctx->journal.j_running_transaction = &ctx->running_txn;
+	sbi->s_fc_stats.fc_failed_commits = 0;
+	sbi->s_fc_stats.fc_ineligible_commits = 0;
+
+	/* 2) Ineligible filesystem falls back to full commit */
+	ctx->journal.j_commit_sequence = 9;
+	ctx->journal.j_commit_request = 9;
+	ctx->running_txn.t_tid = 10;
+	ext4_fc_mark_ineligible(sb, EXT4_FC_REASON_XATTR, &ctx->handle);
+	KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_ineligible_commits, 1UL);
+	ctx->journal.j_flags &= ~JBD2_FULL_COMMIT_ONGOING;
+	ext4_clear_mount_flag(sb, EXT4_MF_FC_INELIGIBLE);
+	sbi->s_fc_ineligible_tid = 0;
+
+	/* 3) Data flush failure (-EIO) clears FLUSHING_DATA and falls back */
+	ctx->journal.j_commit_sequence = 9;
+	ctx->journal.j_commit_request = 9;
+	ctx->running_txn.t_tid = 10;
+	ext4_fc_track_inode(&ctx->handle, file1);
+	WRITE_ONCE(ei1->jinode->i_flags, JI_WRITE_DATA);
+	ctx->submit_inode_data_err = -EIO;
+	KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_test_inode_state(file1, EXT4_STATE_FC_FLUSHING_DATA));
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_failed_commits, 1UL);
+	ctx->journal.j_flags &= ~JBD2_FULL_COMMIT_ONGOING;
+	WRITE_ONCE(ei1->jinode->i_flags, 0);
+	ctx->submit_inode_data_err = 0;
+
+	/* 4) Snapshot ES miss (-EAGAIN) records INELIGIBLE status and falls back */
+	ctx->journal.j_commit_sequence = 9;
+	ctx->journal.j_commit_request = 9;
+	ctx->running_txn.t_tid = 10;
+	ext4_fc_track_range(&ctx->handle, file1, 0, 1);
+	KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_ineligible_commits, 3UL);
+	/* Failed commits count does not increment on -EAGAIN */
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_failed_commits, 1UL);
+	ctx->journal.j_flags &= ~JBD2_FULL_COMMIT_ONGOING;
+
+	/* 5) jbd2_fc_wait_bufs I/O failure (-EIO) records FAILED status */
+	ctx->journal.j_commit_sequence = 9;
+	ctx->journal.j_commit_request = 9;
+	ctx->running_txn.t_tid = 10;
+	ctx->journal.j_fc_off = 0;
+	sbi->s_fc_bytes = 0;
+	ext4_fc_track_inode(&ctx->handle, file1);
+	ctx->fail_fc_wait_bufs = true;
+	KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_stats.fc_failed_commits, 2UL);
+	ctx->journal.j_flags &= ~JBD2_FULL_COMMIT_ONGOING;
+	ctx->fail_fc_wait_bufs = false;
+
+	/*
+	 * 6) Two consecutive successful fast commits in the same TID, with an
+	 *    external journal device (j_fs_dev != j_dev) triggering cache flush.
+	 */
+	ctx->journal.j_commit_sequence = 9;
+	ctx->journal.j_commit_request = 9;
+	ctx->running_txn.t_tid = 10;
+	ctx->journal.j_fc_off = 0;
+	sbi->s_fc_bytes = 0;
+	atomic_set(&sbi->s_fc_subtid, 0);
+	ctx->journal.j_fs_dev = &ctx->ext_jdev;
+	ctx->fc_flush_calls = 0;
+
+	down_write(&ei1->i_data_sem);
+	ext4_es_insert_extent(file1, 0, 2, 200, EXTENT_STATUS_WRITTEN, false);
+	up_write(&ei1->i_data_sem);
+	ext4_fc_track_range(&ctx->handle, file1, 0, 1);
+
+	KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+	KUNIT_EXPECT_EQ(test, atomic_read(&sbi->s_fc_subtid), 1);
+	KUNIT_EXPECT_EQ(test, ctx->fc_flush_calls, 1U);
+	KUNIT_EXPECT_PTR_EQ(test, ctx->last_flushed_bdev, &ctx->ext_jdev);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_bytes, (unsigned int)TEST_BLK_SIZE);
+
+	/* Block 0 starts with EXT4_FC_TAG_HEAD */
+	memcpy(&tl, ctx->fc_buf_data[0], EXT4_FC_TAG_BASE_LEN);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(tl.fc_tag), (u16)EXT4_FC_TAG_HEAD);
+
+	/* Second fast commit in the same TID (block 1 starts with INODE, not HEAD) */
+	ext4_fc_track_inode(&ctx->handle, file1);
+	KUNIT_EXPECT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+	KUNIT_EXPECT_EQ(test, atomic_read(&sbi->s_fc_subtid), 2);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_bytes, (unsigned int)(2 * TEST_BLK_SIZE));
+	memcpy(&tl, ctx->fc_buf_data[1], EXT4_FC_TAG_BASE_LEN);
+	KUNIT_EXPECT_EQ(test, le16_to_cpu(tl.fc_tag), (u16)EXT4_FC_TAG_INODE);
+
+	ctx->journal.j_fs_dev = &ctx->bdev;
+}
+
+/* =========================================================================
+ * 9. Replay Regions, Modified Inode Tracking, and Scan Validation
+ * =========================================================================
+ */
+
+static void test_fc_replay_regions_and_scan(struct kunit *test)
+{
+	struct fc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ext4_sb_info *sbi = ctx->sbi;
+	struct ext4_fc_replay_state *state = &sbi->s_fc_replay_state;
+	struct ext4_fc_tl tl;
+	struct ext4_fc_head head;
+	int i;
+
+	/*
+	 * 1) ext4_fc_record_regions and ext4_fc_replay_check_excluded across
+	 *    dynamic krealloc (> EXT4_FC_REPLAY_REALLOC_INCREMENT = 4)
+	 */
+	for (i = 0; i < 6; i++) {
+		KUNIT_EXPECT_EQ(test,
+				ext4_fc_record_regions(sb, 12 + i, i * 10,
+						       100 + i * 10, 5, 1),
+				0);
+	}
+	KUNIT_EXPECT_EQ(test, state->fc_regions_used, 6);
+	KUNIT_EXPECT_EQ(test, state->fc_regions_valid, 6);
+	KUNIT_EXPECT_GE(test, state->fc_regions_size, 6);
+
+	KUNIT_EXPECT_TRUE(test, ext4_fc_replay_check_excluded(sb, 100));
+	KUNIT_EXPECT_TRUE(test, ext4_fc_replay_check_excluded(sb, 104));
+	KUNIT_EXPECT_FALSE(test, ext4_fc_replay_check_excluded(sb, 105));
+	KUNIT_EXPECT_TRUE(test, ext4_fc_replay_check_excluded(sb, 152));
+
+	/* Unvalidated scan region (replay = 0) is not excluded until validated */
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_record_regions(sb, 99, 0, 500, 10, 0),
+			0);
+	KUNIT_EXPECT_EQ(test, state->fc_regions_used, 7);
+	KUNIT_EXPECT_EQ(test, state->fc_regions_valid, 6);
+	KUNIT_EXPECT_FALSE(test, ext4_fc_replay_check_excluded(sb, 502));
+
+	/* Next replay=1 addition resyncs fc_regions_used to fc_regions_valid */
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_record_regions(sb, 100, 0, 600, 4, 1),
+			0);
+	KUNIT_EXPECT_EQ(test, state->fc_regions_used, 7);
+	KUNIT_EXPECT_EQ(test, state->fc_regions_valid, 7);
+	KUNIT_EXPECT_TRUE(test, ext4_fc_replay_check_excluded(sb, 601));
+
+	/* 2) ext4_fc_record_modified_inode deduplication and krealloc growth */
+	for (i = 0; i < 6; i++) {
+		KUNIT_EXPECT_EQ(test,
+				ext4_fc_record_modified_inode_test(sb, 20 + i),
+				0);
+		/* Duplicate ino is ignored */
+		KUNIT_EXPECT_EQ(test,
+				ext4_fc_record_modified_inode_test(sb, 20 + i),
+				0);
+	}
+	KUNIT_EXPECT_EQ(test, state->fc_modified_inodes_used, 6);
+
+	ext4_fc_replay_cleanup(sb);
+	memset(state, 0, sizeof(*state));
+
+	/* 3) ext4_fc_value_len_isvalid_test and tag2str */
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_ADD_RANGE,
+							 sizeof(struct ext4_fc_add_range)));
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_ADD_RANGE, 1));
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_DEL_RANGE,
+							 sizeof(struct ext4_fc_del_range)));
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_CREAT,
+							 sizeof(struct ext4_fc_dentry_info) + 1));
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_CREAT,
+							  sizeof(struct ext4_fc_dentry_info)));
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_LINK,
+							  sizeof(struct ext4_fc_dentry_info) +
+							  EXT4_NAME_LEN + 1));
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_INODE,
+							 sizeof(struct ext4_fc_inode) +
+							 EXT4_GOOD_OLD_INODE_SIZE));
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_INODE,
+							  sizeof(struct ext4_fc_inode) +
+							  TEST_INODE_SIZE + 1));
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_PAD, 0));
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_TAIL,
+							 sizeof(struct ext4_fc_tail)));
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_TAIL,
+							  sizeof(struct ext4_fc_tail) - 1));
+	KUNIT_EXPECT_TRUE(test,
+			  ext4_fc_value_len_isvalid_test(sbi, EXT4_FC_TAG_HEAD,
+							 sizeof(struct ext4_fc_head)));
+	KUNIT_EXPECT_FALSE(test,
+			   ext4_fc_value_len_isvalid_test(sbi, 0x9999, 8));
+	KUNIT_EXPECT_STREQ(test, tag2str(EXT4_FC_TAG_HEAD), "HEAD");
+	KUNIT_EXPECT_STREQ(test, tag2str(0x9999), "ERROR");
+
+	/* 4) Scan error cases */
+	/* Block 0 not starting with HEAD returns 0 (early stop) */
+	memset(ctx->fc_buf_data[0], 0, TEST_BLK_SIZE);
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+					    PASS_SCAN, 0, 10),
+			0);
+
+	/* Unexpected block offset returns -EFSCORRUPTED */
+	state->fc_replay_expected_off = 2;
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+					    PASS_SCAN, 1, 10),
+			-EFSCORRUPTED);
+
+	/* HEAD with unsupported features returns -EOPNOTSUPP */
+	memset(state, 0, sizeof(*state));
+	memset(ctx->fc_buf_data[0], 0, TEST_BLK_SIZE);
+	tl.fc_tag = cpu_to_le16(EXT4_FC_TAG_HEAD);
+	tl.fc_len = cpu_to_le16(sizeof(head));
+	head.fc_features = cpu_to_le32(0x1);
+	head.fc_tid = cpu_to_le32(10);
+	memcpy(ctx->fc_buf_data[0], &tl, EXT4_FC_TAG_BASE_LEN);
+	memcpy(ctx->fc_buf_data[0] + EXT4_FC_TAG_BASE_LEN, &head, sizeof(head));
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+					    PASS_SCAN, 0, 10),
+			-EOPNOTSUPP);
+
+	/* HEAD with wrong TID returns JBD2_FC_REPLAY_STOP */
+	memset(state, 0, sizeof(*state));
+	head.fc_features = 0;
+	head.fc_tid = cpu_to_le32(99);
+	memcpy(ctx->fc_buf_data[0] + EXT4_FC_TAG_BASE_LEN, &head, sizeof(head));
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+					    PASS_SCAN, 0, 10),
+			JBD2_FC_REPLAY_STOP);
+
+	/* Corrupted TAIL CRC on first commit returns -EFSBADCRC */
+	memset(state, 0, sizeof(*state));
+	sbi->s_fc_bytes = 0;
+	ctx->journal.j_fc_off = 0;
+	ctx->running_txn.t_tid = 10;
+	head.fc_tid = cpu_to_le32(10);
+	{
+		u32 crc = 0;
+
+		ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_HEAD, sizeof(head),
+				     (u8 *)&head, &crc);
+		ext4_fc_write_tail_test(sb, crc);
+	}
+	/* Corrupt last byte of tail.fc_crc */
+	ctx->fc_buf_data[0][2 * EXT4_FC_TAG_BASE_LEN + sizeof(head) +
+			    offsetof(struct ext4_fc_tail, fc_crc)] ^= 0xFF;
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+					    PASS_SCAN, 0, 10),
+			-EFSBADCRC);
+
+	/*
+	 * 5) Valid first commit in block 0 followed by a corrupted second
+	 *    commit in block 1 stops cleanly with JBD2_FC_REPLAY_STOP and
+	 *    keeps fc_replay_num_tags from block 0.
+	 */
+	memset(state, 0, sizeof(*state));
+	sbi->s_fc_bytes = 0;
+	ctx->journal.j_fc_off = 0;
+	{
+		u32 crc = 0;
+
+		ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_HEAD, sizeof(head),
+				     (u8 *)&head, &crc);
+		ext4_fc_write_tail_test(sb, crc);
+	}
+	/* Block 1 has an invalid TLV (tag=0) */
+	memset(ctx->fc_buf_data[1], 0, TEST_BLK_SIZE);
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+					    PASS_SCAN, 0, 10),
+			JBD2_FC_REPLAY_CONTINUE);
+	KUNIT_EXPECT_EQ(test, state->fc_replay_num_tags, 2);
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[1],
+					    PASS_SCAN, 1, 10),
+			JBD2_FC_REPLAY_STOP);
+	KUNIT_EXPECT_EQ(test, state->fc_replay_num_tags, 2);
+	ext4_fc_replay_cleanup(sb);
+	memset(state, 0, sizeof(*state));
+}
+
+/* =========================================================================
+ * 10. Replay of Inode, Create, Link, and Unlink Tags
+ * =========================================================================
+ */
+
+static void test_fc_replay_inode_and_dentries(struct kunit *test)
+{
+	struct fc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ext4_sb_info *sbi = ctx->sbi;
+	struct inode *root = get_mock_inode(ctx, TEST_ROOT_INO);
+	struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+	struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+	struct inode *subdir = get_mock_inode(ctx, TEST_SUBDIR_INO);
+	struct fc_mock_inode_slot *f1_slot = find_slot(ctx, file1);
+	struct dentry d_dir, d_f1, d_f1_link, d_subdir;
+	struct ext4_dir_entry_2 *de = NULL;
+	struct buffer_head *bh;
+	struct qstr q_f1 = QSTR_INIT("file1", 5);
+	struct qstr q_f1_ln = QSTR_INIT("file1_ln", 8);
+	struct qstr q_subdir = QSTR_INIT("subdir", 6);
+	struct ext4_fc_head head = {
+		.fc_features = 0,
+		.fc_tid = cpu_to_le32(10),
+	};
+	u32 crc = 0;
+	int snap_err = EXT4_FC_SNAP_ERR_NONE;
+
+	/* Initialize parent directory on-disk block */
+	KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root, dir), 0);
+
+	init_mock_dentry(&d_dir, NULL, "dir", dir, sb);
+	init_mock_dentry(&d_f1, &d_dir, "file1", file1, sb);
+	init_mock_dentry(&d_f1_link, &d_dir, "file1_ln", file1, sb);
+	init_mock_dentry(&d_subdir, &d_dir, "subdir", subdir, sb);
+
+	/* Prepare file1 and subdir snapshots */
+	file1->i_generation = 0x4242;
+	i_size_write(file1, 2048);
+	sync_inode_to_raw(f1_slot);
+	sync_inode_to_raw(find_slot(ctx, subdir));
+	KUNIT_ASSERT_EQ(test,
+			ext4_fc_snapshot_inode_test(file1, 0, NULL, &snap_err),
+			0);
+	KUNIT_ASSERT_EQ(test,
+			ext4_fc_snapshot_inode_test(subdir, 0, NULL, &snap_err),
+			0);
+
+	/*
+	 * Build a fast commit block containing:
+	 *   HEAD -> INODE(file1) -> CREAT(file1) -> LINK(file1_ln) ->
+	 *   INODE(subdir) -> CREAT(subdir) -> UNLINK(file1) -> TAIL
+	 */
+	sbi->s_fc_bytes = 0;
+	ctx->journal.j_fc_off = 0;
+	ctx->running_txn.t_tid = 10;
+
+	ext4_fc_track_create(&ctx->handle, &d_f1);
+	ext4_fc_track_link(&ctx->handle, file1, &d_f1_link);
+	ext4_fc_track_create(&ctx->handle, &d_subdir);
+	ext4_fc_track_unlink(&ctx->handle, &d_f1);
+
+	KUNIT_ASSERT_TRUE(test,
+			  ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_HEAD,
+					       sizeof(head), (u8 *)&head, &crc));
+	/* Commit all queued dentry updates into block 0 */
+	{
+		struct ext4_fc_dentry_update *fcd;
+
+		list_for_each_entry(fcd, &sbi->s_fc_dentry_q[FC_Q_MAIN], fcd_list) {
+			struct ext4_fc_dentry_info fcd_info = {
+				.fc_parent_ino = cpu_to_le32(fcd->fcd_parent),
+				.fc_ino = cpu_to_le32(fcd->fcd_ino),
+			};
+			u8 d_buf[64];
+			int dlen = fcd->fcd_name.name.len;
+
+			if (fcd->fcd_op == EXT4_FC_TAG_CREAT) {
+				struct inode *c_ino =
+					get_mock_inode(ctx, fcd->fcd_ino);
+				KUNIT_ASSERT_EQ(test,
+						ext4_fc_write_inode_test(c_ino, &crc),
+						0);
+			}
+			memcpy(d_buf, &fcd_info, sizeof(fcd_info));
+			memcpy(d_buf + sizeof(fcd_info),
+			       fcd->fcd_name.name.name, dlen);
+			KUNIT_ASSERT_TRUE(test,
+					  ext4_fc_add_tlv_test(sb, fcd->fcd_op,
+							       sizeof(fcd_info) + dlen,
+							       d_buf, &crc));
+		}
+	}
+	KUNIT_ASSERT_EQ(test, ext4_fc_write_tail_test(sb, crc), 0);
+
+	/* Clear file1 generation in raw_buf to verify replay restores it */
+	((struct ext4_inode *)f1_slot->raw_buf)->i_generation = 0;
+	file1->i_generation = 0;
+
+	/* Scan and replay block 0 */
+	memset(&sbi->s_fc_replay_state, 0, sizeof(sbi->s_fc_replay_state));
+	KUNIT_ASSERT_EQ(test,
+			ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+					    PASS_SCAN, 0, 10),
+			JBD2_FC_REPLAY_CONTINUE);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_replay_state.fc_replay_num_tags, 8);
+
+	/* Error injection: ext4_get_fc_inode_loc failure aborts INODE replay */
+	ctx->fail_get_fc_inode_loc = -EIO;
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+					    PASS_REPLAY, 0, 10),
+			-EIO);
+	ctx->fail_get_fc_inode_loc = 0;
+
+	/* Error injection: sync_dirty_buffer failure aborts INODE replay */
+	sbi->s_fc_replay_state.fc_replay_num_tags = 8;
+	ctx->fail_sync_dirty_buffer = true;
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+					    PASS_REPLAY, 0, 10),
+			-EIO);
+	ctx->fail_sync_dirty_buffer = false;
+
+	/* Successful full replay of block 0 */
+	sbi->s_fc_replay_state.fc_replay_num_tags = 8;
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+					    PASS_REPLAY, 0, 10),
+			JBD2_FC_REPLAY_CONTINUE);
+	KUNIT_EXPECT_EQ(test, sbi->s_fc_replay_state.fc_replay_num_tags, 0);
+
+	/* Calling PASS_REPLAY once fc_replay_num_tags == 0 runs bitmaps & stops */
+	KUNIT_EXPECT_EQ(test,
+			ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+					    PASS_REPLAY, 1, 10),
+			0);
+
+	/* Verify replayed state: file1 generation restored, file1 unlinked, file1_ln & subdir present */
+	KUNIT_EXPECT_EQ(test, file1->i_generation, 0x4242U);
+
+	bh = ext4_find_entry_test(dir, &q_f1, &de, NULL);
+	KUNIT_EXPECT_NULL(test, bh);
+
+	bh = ext4_find_entry_test(dir, &q_f1_ln, &de, NULL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), (u32)TEST_FILE1_INO);
+	brelse(bh);
+
+	bh = ext4_find_entry_test(dir, &q_subdir, &de, NULL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), (u32)TEST_SUBDIR_INO);
+	brelse(bh);
+
+	/*
+	 * Exercise additional replay branches:
+	 *   - LINK on an already-existing entry ("file1_ln") ignores -EEXIST
+	 *   - UNLINK on an already-removed entry ("file1") returns 0
+	 *   - UNLINK when dentry points to a different inode (simulating
+	 *     rename-overwrite) leaves the dentry intact
+	 *   - INODE replay of an inline-data inode copies raw_fc_inode->i_block
+	 */
+	{
+		struct inode *file2 = get_mock_inode(ctx, TEST_FILE2_INO);
+		struct fc_mock_inode_slot *f2_slot = find_slot(ctx, file2);
+		struct dentry d_f1_overwrite;
+		struct ext4_inode *raw_f2 = (struct ext4_inode *)f2_slot->raw_buf;
+		struct ext4_fc_dentry_info fcd_info;
+		u8 d_buf[64];
+
+		/* Prepare inline-data file2 snapshot */
+		ext4_clear_inode_flag(file2, EXT4_INODE_EXTENTS);
+		ext4_set_inode_flag(file2, EXT4_INODE_INLINE_DATA);
+		sync_inode_to_raw(f2_slot);
+		memcpy(raw_f2->i_block, "inline-test-payload", 20);
+		KUNIT_ASSERT_EQ(test,
+				ext4_fc_snapshot_inode_test(file2, 0, NULL,
+							    &snap_err),
+				0);
+		memset(raw_f2->i_block, 0, sizeof(raw_f2->i_block));
+
+		/* Build block: HEAD -> LINK(file1_ln) -> UNLINK(file1) -> INODE(file2) -> TAIL */
+		sbi->s_fc_bytes = 0;
+		ctx->journal.j_fc_off = 0;
+		crc = 0;
+		KUNIT_ASSERT_TRUE(test,
+				  ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_HEAD,
+						       sizeof(head),
+						       (u8 *)&head, &crc));
+
+		/* LINK("file1_ln") when "file1_ln" already exists */
+		fcd_info.fc_parent_ino = cpu_to_le32(TEST_DIR_INO);
+		fcd_info.fc_ino = cpu_to_le32(TEST_FILE1_INO);
+		memcpy(d_buf, &fcd_info, sizeof(fcd_info));
+		memcpy(d_buf + sizeof(fcd_info), "file1_ln", 8);
+		KUNIT_ASSERT_TRUE(test,
+				  ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_LINK,
+						       sizeof(fcd_info) + 8,
+						       d_buf, &crc));
+
+		/* UNLINK("file1", ino=TEST_FILE1_INO) */
+		memcpy(d_buf, &fcd_info, sizeof(fcd_info));
+		memcpy(d_buf + sizeof(fcd_info), "file1", 5);
+		KUNIT_ASSERT_TRUE(test,
+				  ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_UNLINK,
+						       sizeof(fcd_info) + 5,
+						       d_buf, &crc));
+
+		/* INODE(file2) with inline data */
+		KUNIT_ASSERT_EQ(test, ext4_fc_write_inode_test(file2, &crc), 0);
+		KUNIT_ASSERT_EQ(test, ext4_fc_write_tail_test(sb, crc), 0);
+
+		/* 1) Replay when "file1" does not exist (UNLINK no-op) */
+		sbi->s_fc_replay_state.fc_replay_num_tags = 5;
+		KUNIT_EXPECT_EQ(test,
+				ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+						    PASS_REPLAY, 0, 10),
+				JBD2_FC_REPLAY_CONTINUE);
+		KUNIT_EXPECT_EQ(test,
+				memcmp(raw_f2->i_block, "inline-test-payload", 20),
+				0);
+
+		/* 2) Add "file1" pointing to file2, then replay UNLINK(file1, ino=file1) */
+		init_mock_dentry(&d_f1_overwrite, &d_dir, "file1", file2, sb);
+		KUNIT_ASSERT_EQ(test,
+				__ext4_link(dir, file2, &d_f1_overwrite.d_name,
+					    &d_f1_overwrite),
+				0);
+
+		sbi->s_fc_replay_state.fc_replay_num_tags = 5;
+		KUNIT_EXPECT_EQ(test,
+				ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+						    PASS_REPLAY, 0, 10),
+				JBD2_FC_REPLAY_CONTINUE);
+
+		/* Verify "file1" still points to file2 after UNLINK(file1, ino=file1) */
+		bh = ext4_find_entry_test(dir, &q_f1, &de, NULL);
+		KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+		KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode),
+				(u32)TEST_FILE2_INO);
+		brelse(bh);
+	}
+
+	ext4_fc_replay_cleanup(sb);
+}
+
+/* =========================================================================
+ * 11. Replay of Extent Ranges and End-to-End Commit-to-Replay Round-Trip
+ * =========================================================================
+ */
+
+static void test_fc_replay_ranges_and_end_to_end(struct kunit *test)
+{
+	struct fc_test_ctx *ctx = test->priv;
+	struct super_block *sb = ctx->sb;
+	struct ext4_sb_info *sbi = ctx->sbi;
+	struct inode *root = get_mock_inode(ctx, TEST_ROOT_INO);
+	struct inode *dir = get_mock_inode(ctx, TEST_DIR_INO);
+	struct inode *file1 = get_mock_inode(ctx, TEST_FILE1_INO);
+	struct ext4_inode_info *ei1 = EXT4_I(file1);
+	struct fc_mock_inode_slot *f1_slot = find_slot(ctx, file1);
+	struct dentry d_dir, d_f1;
+	struct ext4_map_blocks map;
+	struct ext4_dir_entry_2 *de = NULL;
+	struct buffer_head *bh;
+	struct qstr q_f1 = QSTR_INIT("e2e_file", 8);
+
+	KUNIT_ASSERT_EQ(test, ext4_init_new_dir(NULL, root, dir), 0);
+	init_mock_dentry(&d_dir, NULL, "dir", dir, sb);
+	init_mock_dentry(&d_f1, &d_dir, "e2e_file", file1, sb);
+
+	/*
+	 * Set up file1 modifications for a real ext4_fc_commit():
+	 *   - [0, 3]: written extent at pblk 300
+	 *   - [4, 5]: hole (DEL_RANGE)
+	 *   - [6, 9]: unwritten extent at pblk 310
+	 */
+	ctx->journal.j_commit_sequence = 9;
+	ctx->running_txn.t_tid = 10;
+	ctx->journal.j_fc_off = 0;
+	sbi->s_fc_bytes = 0;
+
+	i_size_write(file1, 10 * TEST_BLK_SIZE);
+	file1->i_generation = 0x7777;
+	sync_inode_to_raw(f1_slot);
+
+	down_write(&ei1->i_data_sem);
+	ext4_es_insert_extent(file1, 0, 4, 300, EXTENT_STATUS_WRITTEN, false);
+	ext4_es_insert_extent(file1, 4, 2, 0, EXTENT_STATUS_HOLE, false);
+	ext4_es_insert_extent(file1, 6, 4, 310, EXTENT_STATUS_UNWRITTEN, false);
+	up_write(&ei1->i_data_sem);
+
+	ext4_fc_track_create(&ctx->handle, &d_f1);
+	ext4_fc_track_range(&ctx->handle, file1, 0, 9);
+
+	/* Execute real ext4_fc_commit() */
+	KUNIT_ASSERT_EQ(test, ext4_fc_commit(&ctx->journal, 10), 0);
+	KUNIT_EXPECT_EQ(test, ctx->journal.j_fc_off, 1U);
+
+	/*
+	 * Simulate pre-crash on-disk state before replay:
+	 *   - [0, 3] was mapped to old pblk 250 (to exercise pblk remap &
+	 *     old block bitmap freeing in ext4_fc_replay_add_range)
+	 *   - [4, 5] was mapped to pblk 260 (to exercise DEL_RANGE removal)
+	 *   - [6, 9] was mapped to pblk 310 as written (to exercise state toggle)
+	 */
+	ext4_ext_tree_init(NULL, file1);
+	{
+		struct ext4_ext_path *path;
+		struct ext4_extent ex;
+
+		memset(&ex, 0, sizeof(ex));
+		ex.ee_block = cpu_to_le32(0);
+		ex.ee_len = cpu_to_le16(4);
+		ext4_ext_store_pblock(&ex, 250);
+		path = ext4_find_extent(file1, 0, NULL, 0);
+		KUNIT_ASSERT_FALSE(test, IS_ERR(path));
+		down_write(&ei1->i_data_sem);
+		path = ext4_ext_insert_extent(NULL, file1, path, &ex, 0);
+		up_write(&ei1->i_data_sem);
+		KUNIT_ASSERT_FALSE(test, IS_ERR(path));
+		ext4_free_ext_path(path);
+
+		memset(&ex, 0, sizeof(ex));
+		ex.ee_block = cpu_to_le32(4);
+		ex.ee_len = cpu_to_le16(2);
+		ext4_ext_store_pblock(&ex, 260);
+		path = ext4_find_extent(file1, 4, NULL, 0);
+		KUNIT_ASSERT_FALSE(test, IS_ERR(path));
+		down_write(&ei1->i_data_sem);
+		path = ext4_ext_insert_extent(NULL, file1, path, &ex, 0);
+		up_write(&ei1->i_data_sem);
+		KUNIT_ASSERT_FALSE(test, IS_ERR(path));
+		ext4_free_ext_path(path);
+
+		memset(&ex, 0, sizeof(ex));
+		ex.ee_block = cpu_to_le32(6);
+		ex.ee_len = cpu_to_le16(4);
+		ext4_ext_store_pblock(&ex, 310);
+		path = ext4_find_extent(file1, 6, NULL, 0);
+		KUNIT_ASSERT_FALSE(test, IS_ERR(path));
+		down_write(&ei1->i_data_sem);
+		path = ext4_ext_insert_extent(NULL, file1, path, &ex, 0);
+		up_write(&ei1->i_data_sem);
+		KUNIT_ASSERT_FALSE(test, IS_ERR(path));
+		ext4_free_ext_path(path);
+	}
+
+	/* Run PASS_SCAN followed by PASS_REPLAY on the committed block */
+	ctx->nr_bb_marks = 0;
+	memset(&sbi->s_fc_replay_state, 0, sizeof(sbi->s_fc_replay_state));
+	KUNIT_ASSERT_EQ(test,
+			ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+					    PASS_SCAN, 0, 10),
+			JBD2_FC_REPLAY_CONTINUE);
+	KUNIT_EXPECT_GT(test, sbi->s_fc_replay_state.fc_replay_num_tags, 0);
+	/* Both ADD_RANGE extents ([0,3]->300 and [6,9]->310) are excluded */
+	KUNIT_EXPECT_TRUE(test, ext4_fc_replay_check_excluded(sb, 300));
+	KUNIT_EXPECT_TRUE(test, ext4_fc_replay_check_excluded(sb, 312));
+
+	KUNIT_ASSERT_EQ(test,
+			ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+					    PASS_REPLAY, 0, 10),
+			JBD2_FC_REPLAY_CONTINUE);
+	KUNIT_ASSERT_EQ(test,
+			ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+					    PASS_REPLAY, 1, 10),
+			0);
+
+	/* 1) Verify directory entry "e2e_file" was created in dir */
+	bh = ext4_find_entry_test(dir, &q_f1, &de, NULL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bh);
+	KUNIT_EXPECT_EQ(test, le32_to_cpu(de->inode), (u32)TEST_FILE1_INO);
+	brelse(bh);
+
+	/* 2) Verify [0, 3] is mapped to new pblk 300 (written) */
+	map.m_lblk = 0;
+	map.m_len = 4;
+	KUNIT_EXPECT_EQ(test, ext4_ext_map_blocks(NULL, file1, &map, 0), 4);
+	KUNIT_EXPECT_EQ(test, map.m_pblk, 300ULL);
+	KUNIT_EXPECT_TRUE(test, (map.m_flags & EXT4_MAP_MAPPED) != 0);
+
+	/* 3) Verify [4, 5] hole was punched by DEL_RANGE */
+	map.m_lblk = 4;
+	map.m_len = 2;
+	KUNIT_EXPECT_EQ(test, ext4_ext_map_blocks(NULL, file1, &map, 0), 0);
+
+	/* 4) Verify [6, 9] is mapped to pblk 310 as unwritten */
+	map.m_lblk = 6;
+	map.m_len = 4;
+	KUNIT_EXPECT_EQ(test, ext4_ext_map_blocks(NULL, file1, &map, 0), 4);
+	KUNIT_EXPECT_EQ(test, map.m_pblk, 310ULL);
+	KUNIT_EXPECT_TRUE(test, (map.m_flags & EXT4_MAP_UNWRITTEN) != 0);
+
+	/* 5) Verify ext4_mb_mark_bb was invoked to free old blocks & mark new ones */
+	KUNIT_EXPECT_GT(test, ctx->nr_bb_marks, 0U);
+
+	/*
+	 * 6) Error handling in ADD_RANGE and DEL_RANGE replay:
+	 *    - DEL_RANGE when ext4_map_blocks fails propagates error
+	 *    - ADD_RANGE when ext4_map_blocks, ext4_find_extent, or
+	 *      ext4_ext_insert_extent fails propagates error
+	 *    - ADD_RANGE / DEL_RANGE when ext4_iget fails returns 0
+	 */
+	{
+		struct ext4_fc_del_range del_arg;
+		struct ext4_fc_add_range add_arg;
+		struct ext4_extent *ex = (struct ext4_extent *)add_arg.fc_ex;
+		u32 crc = 0;
+
+		/* DEL_RANGE: ext4_iget failure returns 0; ext4_map_blocks failure returns -EIO */
+		sbi->s_fc_bh = NULL;
+		sbi->s_fc_bytes = 0;
+		ctx->journal.j_fc_off = 0;
+		del_arg.fc_ino = cpu_to_le32(TEST_FILE1_INO);
+		del_arg.fc_lblk = cpu_to_le32(0);
+		del_arg.fc_len = cpu_to_le32(2);
+		KUNIT_ASSERT_TRUE(test,
+				  ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_DEL_RANGE,
+						       sizeof(del_arg),
+						       (u8 *)&del_arg, &crc));
+
+		sbi->s_fc_replay_state.fc_replay_num_tags = 1;
+		ctx->fail_iget = -ENOENT;
+		KUNIT_EXPECT_EQ(test,
+				ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+						    PASS_REPLAY, 0, 10),
+				0);
+		ctx->fail_iget = 0;
+
+		sbi->s_fc_replay_state.fc_replay_num_tags = 1;
+		ctx->fail_map_blocks = -EIO;
+		KUNIT_EXPECT_EQ(test,
+				ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+						    PASS_REPLAY, 0, 10),
+				-EIO);
+		ctx->fail_map_blocks = 0;
+
+		/* ADD_RANGE on unmapped range [20, 21]: test map_blocks, find_extent, insert_extent failures */
+		sbi->s_fc_bh = NULL;
+		sbi->s_fc_bytes = 0;
+		ctx->journal.j_fc_off = 0;
+		memset(&add_arg, 0, sizeof(add_arg));
+		add_arg.fc_ino = cpu_to_le32(TEST_FILE1_INO);
+		ex->ee_block = cpu_to_le32(20);
+		ex->ee_len = cpu_to_le16(2);
+		ext4_ext_store_pblock(ex, 400);
+		KUNIT_ASSERT_TRUE(test,
+				  ext4_fc_add_tlv_test(sb, EXT4_FC_TAG_ADD_RANGE,
+						       sizeof(add_arg),
+						       (u8 *)&add_arg, &crc));
+
+		sbi->s_fc_replay_state.fc_replay_num_tags = 1;
+		ctx->fail_map_blocks = -EIO;
+		KUNIT_EXPECT_EQ(test,
+				ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+						    PASS_REPLAY, 0, 10),
+				-EIO);
+		ctx->fail_map_blocks = 0;
+
+		sbi->s_fc_replay_state.fc_replay_num_tags = 1;
+		ctx->fail_find_extent = -EIO;
+		KUNIT_EXPECT_EQ(test,
+				ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+						    PASS_REPLAY, 0, 10),
+				-EIO);
+		ctx->fail_find_extent = 0;
+
+		sbi->s_fc_replay_state.fc_replay_num_tags = 1;
+		ctx->fail_insert_extent = -ENOSPC;
+		KUNIT_EXPECT_EQ(test,
+				ext4_fc_replay_test(&ctx->journal, &ctx->fc_bhs[0],
+						    PASS_REPLAY, 0, 10),
+				-ENOSPC);
+		ctx->fail_insert_extent = 0;
+	}
+
+	ext4_fc_replay_cleanup(sb);
+	KUNIT_EXPECT_FALSE(test, (sbi->s_mount_state & EXT4_FC_REPLAY) != 0);
+}
+
+static struct kunit_case fast_commit_test_cases[] = {
+	KUNIT_CASE(test_fc_init_and_eligibility),
+	KUNIT_CASE(test_fc_mark_ineligible),
+	KUNIT_CASE(test_fc_track_inode_and_range),
+	KUNIT_CASE(test_fc_track_dentry_and_del),
+	KUNIT_CASE(test_fc_reserve_space_and_tlv_writing),
+	KUNIT_CASE(test_fc_snapshot_inode_and_ranges),
+	KUNIT_CASE(test_fc_cleanup_and_stats),
+	KUNIT_CASE(test_fc_commit_paths),
+	KUNIT_CASE(test_fc_replay_regions_and_scan),
+	KUNIT_CASE(test_fc_replay_inode_and_dentries),
+	KUNIT_CASE(test_fc_replay_ranges_and_end_to_end),
+	{}
+};
+
+static struct kunit_suite fast_commit_test_suite = {
+	.name = "ext4_fast_commit_test",
+	.init = fast_commit_test_init,
+	.test_cases = fast_commit_test_cases,
+};
+
+kunit_test_suites(&fast_commit_test_suite);
+
+MODULE_LICENSE("GPL");
diff --git a/fs/ext4/fast_commit.c b/fs/ext4/fast_commit.c
index 15bd040c2329..57d78cb3b55d 100644
--- a/fs/ext4/fast_commit.c
+++ b/fs/ext4/fast_commit.c
@@ -14,6 +14,7 @@
 
 #include <linux/lockdep.h>
 #include <linux/wait_bit.h>
+#include <kunit/static_stub.h>
 /*
  * Ext4 Fast Commits
  * -----------------
@@ -661,11 +662,25 @@ void ext4_fc_track_range(handle_t *handle, struct inode *inode, ext4_lblk_t star
 	trace_ext4_fc_track_range(handle, inode, start, end, ret);
 }
 
-static void ext4_fc_submit_bh(struct super_block *sb, bool is_tail)
+int ext4_fc_get_buf(journal_t *journal, struct buffer_head **bh)
+{
+	KUNIT_STATIC_STUB_REDIRECT(ext4_fc_get_buf, journal, bh);
+	return jbd2_fc_get_buf(journal, bh);
+}
+
+void ext4_fc_issue_flush(struct block_device *bdev)
+{
+	KUNIT_STATIC_STUB_REDIRECT(ext4_fc_issue_flush, bdev);
+	blkdev_issue_flush(bdev);
+}
+
+void ext4_fc_submit_bh(struct super_block *sb, bool is_tail)
 {
 	blk_opf_t write_flags = JBD2_JOURNAL_REQ_FLAGS;
 	struct buffer_head *bh = EXT4_SB(sb)->s_fc_bh;
 
+	KUNIT_STATIC_STUB_REDIRECT(ext4_fc_submit_bh, sb, is_tail);
+
 	/* Add REQ_FUA | REQ_PREFLUSH only its tail */
 	if (test_opt(sb, BARRIER) && is_tail)
 		write_flags |= REQ_FUA | REQ_PREFLUSH;
@@ -707,7 +722,7 @@ static u8 *ext4_fc_reserve_space(struct super_block *sb, int len, u32 *crc)
 		return NULL;
 
 	if (!sbi->s_fc_bh) {
-		ret = jbd2_fc_get_buf(EXT4_SB(sb)->s_journal, &bh);
+		ret = ext4_fc_get_buf(EXT4_SB(sb)->s_journal, &bh);
 		if (ret)
 			return NULL;
 		sbi->s_fc_bh = bh;
@@ -737,7 +752,7 @@ static u8 *ext4_fc_reserve_space(struct super_block *sb, int len, u32 *crc)
 
 	ext4_fc_submit_bh(sb, false);
 
-	ret = jbd2_fc_get_buf(EXT4_SB(sb)->s_journal, &bh);
+	ret = ext4_fc_get_buf(EXT4_SB(sb)->s_journal, &bh);
 	if (ret)
 		return NULL;
 	sbi->s_fc_bh = bh;
@@ -1426,7 +1441,7 @@ static int ext4_fc_perform_commit(journal_t *journal, tid_t commit_tid)
 	 * issue a cache flush before we start writing fast commit blocks.
 	 */
 	if (journal->j_fs_dev != journal->j_dev)
-		blkdev_issue_flush(journal->j_fs_dev);
+		ext4_fc_issue_flush(journal->j_fs_dev);
 
 	blk_start_plug(&plug);
 	alloc_ctx = ext4_fc_lock(sb);
@@ -2016,7 +2031,7 @@ static int ext4_fc_replay_inode(struct super_block *sb,
 out:
 	iput(inode);
 	if (!ret)
-		blkdev_issue_flush(sb->s_bdev);
+		ext4_fc_issue_flush(sb->s_bdev);
 
 	return ret;
 }
@@ -2765,3 +2780,158 @@ void ext4_fc_destroy_dentry_cache(void)
 	kmem_cache_destroy(ext4_fc_range_cachep);
 	kmem_cache_destroy(ext4_fc_dentry_cachep);
 }
+
+#if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+void ext4_fc_set_snap_err_test(int *snap_err, int err)
+{
+	ext4_fc_set_snap_err(snap_err, err);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_set_snap_err_test);
+
+bool ext4_fc_disabled_test(struct super_block *sb)
+{
+	return ext4_fc_disabled(sb);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_disabled_test);
+
+bool ext4_fc_eligible_test(struct super_block *sb)
+{
+	return ext4_fc_eligible(sb);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_eligible_test);
+
+void ext4_fc_snap_stats_update_max_test(atomic64_t *stat, u64 value)
+{
+	ext4_fc_snap_stats_update_max(stat, value);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_snap_stats_update_max_test);
+
+void ext4_fc_wake_inode_state_test(struct inode *inode, int bit)
+{
+	ext4_fc_wake_inode_state(inode, bit);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_wake_inode_state_test);
+
+u8 *ext4_fc_reserve_space_test(struct super_block *sb, int len, u32 *crc)
+{
+	return ext4_fc_reserve_space(sb, len, crc);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_reserve_space_test);
+
+int ext4_fc_write_tail_test(struct super_block *sb, u32 crc)
+{
+	return ext4_fc_write_tail(sb, crc);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_write_tail_test);
+
+bool ext4_fc_add_tlv_test(struct super_block *sb, u16 tag, u16 len, u8 *val,
+			  u32 *crc)
+{
+	return ext4_fc_add_tlv(sb, tag, len, val, crc);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_add_tlv_test);
+
+int ext4_fc_write_inode_test(struct inode *inode, u32 *crc)
+{
+	return ext4_fc_write_inode(inode, crc);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_write_inode_test);
+
+int ext4_fc_write_inode_data_test(struct inode *inode, u32 *crc)
+{
+	return ext4_fc_write_inode_data(inode, crc);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_write_inode_data_test);
+
+void ext4_fc_free_inode_snap_test(struct inode *inode)
+{
+	ext4_fc_free_inode_snap(inode);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_free_inode_snap_test);
+
+int ext4_fc_snapshot_inode_test(struct inode *inode,
+				unsigned int nr_ranges_total,
+				unsigned int *nr_rangesp, int *snap_err)
+{
+	return ext4_fc_snapshot_inode(inode, nr_ranges_total, nr_rangesp,
+				      snap_err);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_snapshot_inode_test);
+
+unsigned int ext4_fc_count_snapshot_inodes_test(struct super_block *sb)
+{
+	return ext4_fc_count_snapshot_inodes(sb);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_count_snapshot_inodes_test);
+
+int ext4_fc_alloc_snapshot_inodes_test(struct super_block *sb,
+				       struct inode ***inodesp,
+				       unsigned int *nr_inodesp)
+{
+	return ext4_fc_alloc_snapshot_inodes(sb, inodesp, nr_inodesp);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_alloc_snapshot_inodes_test);
+
+int ext4_fc_snapshot_inodes_test(journal_t *journal, struct inode **inodes,
+				 unsigned int inodes_size,
+				 unsigned int *nr_inodesp,
+				 unsigned int *nr_rangesp, int *snap_err)
+{
+	return ext4_fc_snapshot_inodes(journal, inodes, inodes_size,
+				       nr_inodesp, nr_rangesp, snap_err);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_snapshot_inodes_test);
+
+void ext4_fc_update_stats_test(struct super_block *sb, int status,
+			       u64 commit_time, int nblks, tid_t commit_tid)
+{
+	ext4_fc_update_stats(sb, status, commit_time, nblks, commit_tid);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_update_stats_test);
+
+void ext4_fc_cleanup_test(journal_t *journal, int full, tid_t tid)
+{
+	ext4_fc_cleanup(journal, full, tid);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_cleanup_test);
+
+int ext4_fc_record_modified_inode_test(struct super_block *sb, int ino)
+{
+	return ext4_fc_record_modified_inode(sb, ino);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_record_modified_inode_test);
+
+bool ext4_fc_value_len_isvalid_test(struct ext4_sb_info *sbi, int tag, int len)
+{
+	return ext4_fc_value_len_isvalid(sbi, tag, len);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_value_len_isvalid_test);
+
+int ext4_fc_replay_test(journal_t *journal, struct buffer_head *bh,
+			enum passtype pass, int off, tid_t expected_tid)
+{
+	return ext4_fc_replay(journal, bh, pass, off, expected_tid);
+}
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_replay_test);
+
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_get_buf);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_submit_bh);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_issue_flush);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_init_inode);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_del);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_mark_ineligible);
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_fc_track_unlink);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_track_unlink);
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_fc_track_link);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_track_link);
+EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_fc_track_create);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_track_create);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_track_inode);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_track_range);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_commit);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_record_regions);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_replay_check_excluded);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_replay_cleanup);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_init);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_info_show);
+#endif
diff --git a/fs/ext4/ialloc.c b/fs/ext4/ialloc.c
index 02f3ef05acaf..25b387f3ce85 100644
--- a/fs/ext4/ialloc.c
+++ b/fs/ext4/ialloc.c
@@ -768,6 +768,8 @@ int ext4_mark_inode_used(struct super_block *sb, int ino)
 	int bit;
 	int err;
 
+	KUNIT_STATIC_STUB_REDIRECT(ext4_mark_inode_used, sb, ino);
+
 	if (ino < EXT4_FIRST_INO(sb) || ino > max_ino)
 		return -EFSCORRUPTED;
 
@@ -1638,4 +1640,5 @@ int ext4_init_inode_table(struct super_block *sb, ext4_group_t group,
 
 #if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
 EXPORT_SYMBOL_FOR_EXT4_TEST(__ext4_new_inode);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mark_inode_used);
 #endif
diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c
index 105c731b95b1..67d37b39db93 100644
--- a/fs/ext4/inode.c
+++ b/fs/ext4/inode.c
@@ -5088,6 +5088,8 @@ int ext4_get_inode_loc_noio(struct inode *inode, struct ext4_iloc *iloc)
 	int inodes_per_block, inode_offset;
 	unsigned long ino = inode->i_ino;
 
+	KUNIT_STATIC_STUB_REDIRECT(ext4_get_inode_loc_noio, inode, iloc);
+
 	iloc->bh = NULL;
 	if (ino < EXT4_ROOT_INO ||
 	    ino > le32_to_cpu(EXT4_SB(sb)->s_es->s_inodes_count))
@@ -5129,6 +5131,8 @@ int ext4_get_inode_loc_noio(struct inode *inode, struct ext4_iloc *iloc)
 int ext4_get_fc_inode_loc(struct super_block *sb, unsigned long ino,
 			  struct ext4_iloc *iloc)
 {
+	KUNIT_STATIC_STUB_REDIRECT(ext4_get_fc_inode_loc, sb, ino, iloc);
+
 	return __ext4_get_inode_loc(sb, ino, NULL, iloc, NULL);
 }
 
@@ -7002,6 +7006,8 @@ vm_fault_t ext4_page_mkwrite(struct vm_fault *vmf)
 }
 
 #if IS_ENABLED(CONFIG_EXT4_KUNIT_TESTS)
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_inode_loc_noio);
+EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_fc_inode_loc);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_get_block_prep);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_update_reserve_space);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_da_release_space);
diff --git a/fs/ext4/mballoc.c b/fs/ext4/mballoc.c
index 46ba07e017a3..3f06b0996063 100644
--- a/fs/ext4/mballoc.c
+++ b/fs/ext4/mballoc.c
@@ -4269,6 +4269,8 @@ void ext4_mb_mark_bb(struct super_block *sb, ext4_fsblk_t block,
 	int err = 0;
 	unsigned int clen, thisgrp_len;
 
+	KUNIT_STATIC_STUB_REDIRECT(ext4_mb_mark_bb, sb, block, len, state);
+
 	while (len > 0) {
 		ext4_get_group_no_and_offset(sb, block, &group, &blkoff);
 
@@ -7474,7 +7476,6 @@ EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_get_group_number);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_release);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_read_block_bitmap_nowait);
 EXPORT_SYMBOL_FOR_EXT4_TEST(mb_set_bits);
-EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_fc_init_inode);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_mark_context);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_mb_new_blocks);
 EXPORT_SYMBOL_FOR_EXT4_TEST(ext4_free_blocks);
-- 
2.56.0.542.g3ef89b7020-goog


  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 ` Theodore Ts'o [this message]
2026-11-12  4:28   ` [PATCH 09/27] ext4: add KUnit tests for fs/ext4/fast_commit.c 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 ` [PATCH 21/27] ext4: add KUnit tests for fs/ext4/migrate.c Theodore Ts'o
2026-11-12  4:37   ` 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-10-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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox