* Re: [PATCH v2] generic/795: add unaligned boundary test cases for WRITE_ZEROES
[not found] <20260709083048.3849610-1-p.raghav@samsung.com>
@ 2026-07-10 8:23 ` Zhang Yi
2026-07-13 7:28 ` Pankaj Raghav
2026-07-13 10:04 ` Christoph Hellwig
0 siblings, 2 replies; 5+ messages in thread
From: Zhang Yi @ 2026-07-10 8:23 UTC (permalink / raw)
To: Pankaj Raghav, zlang
Cc: linux-xfs, hch, pankaj.raghav, fstests, djwong, yi.zhang,
Ext4 Developers List
+CC ext4 list
On 7/9/2026 4:30 PM, Pankaj Raghav wrote:
> During the review of WRITE_ZEROES support to XFS, Zhang Yi pointed out
> some important semantics for the boundary blocks when WRITE_ZEROES
> command is used.[1]
>
> The test cases check the boundary blocks when they are in different
> states and WRITE_ZEROES are issued that straddle the boundary. Along with that,
> test cases have been added when a EoF straddles an allocation unit.
>
> [1] https://lore.kernel.org/linux-xfs/0b7f1a4f-da1c-4297-8099-98d738070ab7@huaweicloud.com/
>
> Signed-off-by: Pankaj Raghav <p.raghav@samsung.com>
Thank you for the test! The changes look good to me, and it works well
with my ext4 fixes as well.
Reviewed-by: Zhang Yi <yi.zhang@huawei.com>
Unfortunately, I've found a bug in filefrag that causes delalloc extents
to be reported with a length of 0. This means this test case can miss
delalloc extents. The issue is in misc/filefrag.c's print_extent_info(),
where ext_len is incorrectly set to 0 for the FIEMAP_EXTENT_UNKNOWN or
EXT4_FIEMAP_EXTENT_HOLE cases. Could you please send a patch to fix this
as well?
Thanks,
Yi.
> ---
> CHanges since v1:
> - Use filefrag instead of FIEMAP as the latter uses FIEMAP_FLAG_SYNC by
> default when used via xfs_io (Zhang yi)
> - Add an extra testcase for dirty-unwritten edges (Zhang Yi)
>
> common/rc | 2 +-
> tests/generic/795 | 187 ++++++++++++++++++++++++++++++++++++++++++
> tests/generic/795.out | 19 +++++
> 3 files changed, 207 insertions(+), 1 deletion(-)
> create mode 100755 tests/generic/795
> create mode 100644 tests/generic/795.out
>
> diff --git a/common/rc b/common/rc
> index 79189e7e..5e59968f 100644
> --- a/common/rc
> +++ b/common/rc
> @@ -3008,7 +3008,7 @@ _require_xfs_io_command()
> testio=`$XFS_IO_PROG -F -f -c "$command $param 0 1m" $testfile 2>&1`
> param_checked="$param"
> ;;
> - "fpunch" | "fcollapse" | "zero" | "fzero" | "finsert" | "funshare")
> + "fpunch" | "fcollapse" | "zero" | "fzero" | "finsert" | "funshare" | "fwzero")
> local blocksize=$(_get_file_block_size $TEST_DIR)
> testio=`$XFS_IO_PROG -F -f -c "pwrite 0 $((5 * $blocksize))" \
> -c "fsync" -c "$command $blocksize $((2 * $blocksize))" \
> diff --git a/tests/generic/795 b/tests/generic/795
> new file mode 100755
> index 00000000..b1e05666
> --- /dev/null
> +++ b/tests/generic/795
> @@ -0,0 +1,187 @@
> +#! /bin/bash
> +# SPDX-License-Identifier: GPL-2.0
> +# Copyright (c) 2026 Samsung Electronics. All Rights Reserved.
> +#
> +# FS QA Test 795
> +#
> +# Verify FALLOC_FL_WRITE_ZEROES (xfs_io "fwzero") on an unaligned range,
> +# exercising every state the two boundary allocation units can be in.
> +#
> +# WRITE_ZEROES must leave the whole requested range backed by *written*
> +# (zeroed) extents while preserving the out-of-range bytes of the partial
> +# boundary units. The scenarios are: written_edges, hole_edges,
> +# clean_unwritten_edges, delalloc_edges and dirty_unwritten_edges.
> +#
> +# It then covers the EOF cases: a WRITE_ZEROES that *extends* the file to a
> +# non-unit-aligned size, followed by a further extension. WRITE_ZEROES must
> +# never leave written blocks beyond EOF.
> +
> +. ./common/preamble
> +_begin_fstest auto quick prealloc fiemap
> +
> +testfile=$TEST_DIR/$seq.testfile
> +
> +_cleanup()
> +{
> + cd /
> + rm -r -f $tmp.*
> + rm -f $testfile
> +}
> +
> +. ./common/filter
> +
> +_require_test
> +_require_xfs_io_command "fwzero"
> +_require_xfs_io_command "falloc"
> +_require_xfs_io_command "truncate"
> +_require_command "$FILEFRAG_PROG" filefrag
> +
> +# The allocation unit: rt extent size on an rt config, otherwise the block
> +# size. Deriving the range from this makes the head/tail units straddle rt
> +# extents when rextsize > 1.
> +u=$(_get_file_block_size $TEST_DIR)
> +
> +# The fs block size. On rt with rextsize > 1 the allocation unit u is the rt
> +# extent (u > b). A WRITE_ZEROES that extends EOF must only write out to the
> +# block-rounded EOF and leave the rest of the straddling rt extent unwritten,
> +# rather than written past EOF.
> +b=$(_get_block_size $TEST_DIR)
> +
> +# Unaligned range within EOF: start 1.5 units in, span 4 units, so both the
> +# head unit [u, 2u) and the tail unit [5u, 6u) are only partially covered,
> +# with fully covered units in between and data on both sides.
> +off=$((u + u / 2))
> +len=$((4 * u))
> +filesz=$((8 * u))
> +pattern=0xab
> +
> +# Expected images. For the written/delalloc/dirty-unwritten cases the pattern
> +# surrounds the zeroed range; for the hole/clean-unwritten cases the file reads
> +# back all zeroes.
> +$XFS_IO_PROG -f -c "pwrite -S $pattern 0 $filesz" $tmp.pattern >> $seqres.full 2>&1
> +$XFS_IO_PROG -c "pwrite -S 0 $off $len" $tmp.pattern >> $seqres.full 2>&1
> +$XFS_IO_PROG -f -c "truncate $filesz" $tmp.zero >> $seqres.full 2>&1
> +
> +# _filter_filefrag emits "phys#len#logical#flags" in bytes. filefrag has no
> +# range option, so clip the extents to the byte window [s, e) with awk before
> +# grepping - an extent [$3, $3+$2) overlaps the window iff $3 < e && $3+$2 > s.
> +_filefrag_window()
> +{
> + local file=$1 start=$2 end=$3
> +
> + $FILEFRAG_PROG -v "$file" | _filter_filefrag | \
> + $AWK_PROG -F'#' -v s=$start -v e=$end '$3 < e && $3 + $2 > s'
> +}
> +
> +# Assert the whole requested range is backed by written extents immediately
> +# after fwzero (before any sync/remount): no unwritten, no delalloc.
> +# The extent state is queried with filefrag: xfs_io's fiemap always sets
> +# FIEMAP_FLAG_SYNC, which would flush and convert delalloc to written before
> +# reporting.
> +_check_range_written()
> +{
> + local file=$1
> + local bad
> +
> + $FILEFRAG_PROG -v "$file" >> $seqres.full 2>&1
> + bad=$(_filefrag_window "$file" $off $((off + len)) | \
> + grep -E -c 'unwritten|delalloc|unknown')
> + if [ "$bad" -eq 0 ]; then
> + echo "extents: range fully written"
> + else
> + echo "extents: FAIL - $bad unwritten/delalloc extent(s) in range"
> + fi
> +}
> +
> +_check_data()
> +{
> + local file=$1
> + local want=$2
> +
> + if cmp -s "$file" "$want"; then
> + echo "data: matches expected image"
> + else
> + echo "data: FAIL - mismatch against expected image"
> + cmp "$file" "$want" | head
> + fi
> +}
> +
> +# Assert the tail past the block-rounded EOF holds no *written* extents: it must
> +# be unwritten or a hole.
> +_check_eof_tail()
> +{
> + local file=$1 start=$2 len=$3
> + local bad
> +
> + if [ "$len" -le 0 ]; then
> + echo "extents: eof tail unwritten"
> + return
> + fi
> + $FILEFRAG_PROG -v "$file" >> $seqres.full 2>&1
> + bad=$(_filefrag_window "$file" $start $((start + len)) | \
> + grep -E -v -c 'unwritten|delalloc|unknown')
> + if [ "$bad" -eq 0 ]; then
> + echo "extents: eof tail unwritten"
> + else
> + echo "extents: FAIL - $bad written extent(s) past block-rounded EOF"
> + fi
> +}
> +
> +_run_case()
> +{
> + local name=$1
> + local want=$2
> +
> + echo "=== $name ==="
> +
> + $XFS_IO_PROG -c "fwzero $off $len" $testfile >> $seqres.full 2>&1
> +
> + echo "== $name filefrag after fwzero ==" >> $seqres.full
> + _check_range_written $testfile
> + _test_cycle_mount
> + _check_data $testfile $want
> +
> + rm -f $testfile
> +}
> +
> +# 1) Boundary units already written, within i_size.
> +$XFS_IO_PROG -f -c "pwrite -S $pattern 0 $filesz" -c fsync $testfile \
> + >> $seqres.full 2>&1
> +_run_case "written_edges" $tmp.pattern
> +
> +# 2) Boundary units are holes (sparse file).
> +$XFS_IO_PROG -f -c "truncate $filesz" $testfile >> $seqres.full 2>&1
> +_run_case "hole_edges" $tmp.zero
> +
> +# 3) Boundary units are clean unwritten preallocation: read back as zeroes.
> +$XFS_IO_PROG -f -c "falloc 0 $filesz" $testfile >> $seqres.full 2>&1
> +_run_case "clean_unwritten_edges" $tmp.zero
> +
> +# 4) Boundary units are dirty/delalloc.
> +$XFS_IO_PROG -f -c "pwrite -S $pattern 0 $filesz" $testfile >> $seqres.full 2>&1
> +_run_case "delalloc_edges" $tmp.pattern
> +
> +# 5) Boundary units are dirty unwritten.
> +$XFS_IO_PROG -f -c "falloc 0 $filesz" -c "pwrite -S $pattern 0 $filesz" \
> + $testfile >> $seqres.full 2>&1
> +_run_case "dirty_unwritten_edges" $tmp.pattern
> +
> +# 6) EOF cases. A WRITE_ZEROES that extends the file to a non-unit-aligned
> +# size, followed by a further extend.
> +eof=$((3 * u + 1))
> +
> +echo "=== eof_then_truncate ==="
> +$XFS_IO_PROG -f -c "fwzero 0 $eof" $testfile >> $seqres.full 2>&1
> +# The write must stop at the block-rounded EOF; the rest of the rt extent that
> +# straddles EOF must be unwritten, not written past EOF.
> +eof_ru=$(( (eof + b - 1) & ~(b - 1) ))
> +_check_eof_tail $testfile $eof_ru $((4 * u - eof_ru))
> +
> +$XFS_IO_PROG -c "truncate $filesz" $testfile >> $seqres.full 2>&1
> +_test_cycle_mount
> +_check_data $testfile $tmp.zero
> +rm -f $testfile
> +
> +# success, all done
> +status=0
> +exit
> diff --git a/tests/generic/795.out b/tests/generic/795.out
> new file mode 100644
> index 00000000..dca6e571
> --- /dev/null
> +++ b/tests/generic/795.out
> @@ -0,0 +1,19 @@
> +QA output created by 795
> +=== written_edges ===
> +extents: range fully written
> +data: matches expected image
> +=== hole_edges ===
> +extents: range fully written
> +data: matches expected image
> +=== clean_unwritten_edges ===
> +extents: range fully written
> +data: matches expected image
> +=== delalloc_edges ===
> +extents: range fully written
> +data: matches expected image
> +=== dirty_unwritten_edges ===
> +extents: range fully written
> +data: matches expected image
> +=== eof_then_truncate ===
> +extents: eof tail unwritten
> +data: matches expected image
>
> base-commit: ffc8bad17e5b2f56e48dbac43f7c5ae8ac368fe5
^ permalink raw reply [flat|nested] 5+ messages in thread