#! /bin/bash
# SPDX-License-Identifier: GPL-2.0
# Copyright (c) 2013 Red Hat, Inc., Tomas Racek <tracek@redhat.com>
#
# FS QA Test No. 746
#
# Test that filesystem sends discard requests only on free blocks
#
. ./common/preamble
_begin_fstest auto trim fiemap

_require_test
_require_loop
_require_fstrim
_require_xfs_io_command "fiemap"

_require_fs_space $TEST_DIR 307200
fssize=$(_small_fs_size_mb 300)           # 200m phys/virt size

case "$FSTYP" in
btrfs)
	_require_btrfs_command inspect-internal dump-super
	_require_btrfs_command inspect-internal dump-tree

	# 3g for btrfs to have distinct bgs
	_require_fs_space $TEST_DIR 3145728
	fssize=3000
	;;
ext4)
	_require_dumpe2fs
	;;
xfs)
	;;
*)
	_notrun "Requires fs-specific way to check discard ranges"
esac

# Override the default cleanup function.
_cleanup()
{
	_unmount $loop_mnt &> /dev/null
	[ -n "$loop_dev" ] && _destroy_loop_device $loop_dev
	if [ $status -eq 0 ]; then
		rm -rf $tmp
		rm $img_file
	fi
}

get_holes()
{
	# It's not a good idea to be running tools against the image file
	# backing a live filesystem because the filesystem could be maintaining
	# in-core state that will perturb the free space map on umount.  Stick
	# to established convention which requires the filesystem to be
	# unmounted while we probe the underlying file.
	_unmount $loop_mnt

	# FIEMAP only works on regular files, so call it on the backing file
	# and not the loop device like everything else
	$XFS_IO_PROG -F -c fiemap $img_file | grep hole | \
		$SED_PROG 's/.*\[\(.*\)\.\.\(.*\)\].*/\1 \2/'
	_mount $loop_dev $loop_mnt
}

get_free_sectors()
{
	case $FSTYP in
	ext4)
	_unmount $loop_mnt
	$DUMPE2FS_PROG $loop_dev  2>&1 | grep " Free blocks" | cut -d ":" -f2- | \
		tr ',' '\n' | $SED_PROG 's/^ //' | \
		$AWK_PROG -v spb=$sectors_per_block 'BEGIN{FS="-"};
		     NF {
			if($2 != "") # range of blocks
				print spb * $1, spb * ($2 + 1) - 1;
			else		# just single block
				print spb * $1, spb * ($1 + 1) - 1;
		     }'
	;;
	xfs)
	agsize=`$XFS_INFO_PROG $loop_mnt | $SED_PROG -n 's/.*agsize=\(.*\) blks.*/\1/p'`
	# Convert free space (agno, block, length) to (start sector, end sector)
	_unmount $loop_mnt
	$XFS_DB_PROG -r -c "freesp -d" $loop_dev | $SED_PROG '/^.*from/,$d'| \
		 $AWK_PROG -v spb=$sectors_per_block -v agsize=$agsize \
		'{ print spb * ($1 * agsize + $2), spb * ($1 * agsize + $2 + $3) - 1 }'
	;;
	btrfs)
		local device_size=$($BTRFS_UTIL_PROG filesystem show --raw $loop_mnt 2>&1 \
			| sed -n "s/^.*size \([0-9]*\).*$/\1/p")

		local nodesize=$($BTRFS_UTIL_PROG inspect-internal dump-super $loop_dev \
			| sed -n 's/nodesize\s*\(.*\)/\1/p')

		# Get holes within block groups
		$BTRFS_UTIL_PROG inspect-internal dump-tree -t extent $loop_dev \
			| $AWK_PROG -v sectorsize=512 -v nodesize=$nodesize -f $here/src/parse-extent-tree.awk

		# Get holes within unallocated space on disk
		$BTRFS_UTIL_PROG inspect-internal dump-tree -t dev $loop_dev \
			| $AWK_PROG -v sectorsize=512 -v devsize=$device_size -f $here/src/parse-dev-tree.awk

	;;
	esac
}

merge_ranges()
{
	# Merges consecutive ranges from two input files
	file1=$1
	file2=$2

	tmp_file=$tmp/sectors.tmp

	cat $file1 $file2 | sort -n > $tmp_file

	read line < $tmp_file
	start=${line% *}
	end=${line#* }

	# Continue from second line
	sed -i "1d" $tmp_file
	while read line; do
		curr_start=${line% *}
		curr_end=${line#* }

		if [ `expr $end + 1` -ge $curr_start ]; then
			if [ $curr_end -gt $end ]; then
				end=$curr_end
			fi
		else
			echo $start $end
			start=$curr_start
			end=$curr_end
		fi
	done < $tmp_file

	# Print last line
	echo $start $end

	rm $tmp_file
}

tmp=`mktemp -d`

img_file=$TEST_DIR/$$.fs
dd if=/dev/zero of=$img_file bs=1M count=$fssize &> /dev/null

loop_dev=$(_create_loop_device $img_file)
loop_mnt=$tmp/loop_mnt

fiemap_ref="$tmp/reference"
fiemap_after="$tmp/after"
free_sectors="$tmp/free_sectors"
merged_sectors="$tmp/merged_free_sectors"

mkdir $loop_mnt

[ "$FSTYP" = "xfs" ] && MKFS_OPTIONS="-f $MKFS_OPTIONS"
[ "$FSTYP" = "btrfs" ] && MKFS_OPTIONS="$MKFS_OPTIONS -f -dsingle -msingle"

_mkfs_dev $loop_dev
_mount $loop_dev $loop_mnt

echo -n "Generating garbage on loop..."
# Goal is to fill it up, ignore any errors.
for i in `seq 1 10`; do
	mkdir $loop_mnt/$i &> /dev/null
	cp -r $here/* $loop_mnt/$i &> /dev/null || break
done

# Get reference fiemap, this can contain i.e. uninitialized inode table
_sync_fs $loop_mnt
get_holes > $fiemap_ref

# Delete some files
find $loop_mnt -type f -print | $AWK_PROG \
	'BEGIN {srand()}; {if(rand() > 0.7) printf("%s\0", $0);}' | xargs -0 rm
echo "done."

echo -n "Running fstrim..."
$FSTRIM_PROG $loop_mnt &> /dev/null
echo "done."

echo -n "Detecting interesting holes in image..."
# Get after-trim fiemap
_sync_fs $loop_mnt
get_holes > $fiemap_after
echo "done."

echo -n "Comparing holes to the reported space from FS..."
# Get block size
block_size=$(_get_block_size $loop_mnt/)
sectors_per_block=`expr $block_size / 512`

# Obtain free space from filesystem
get_free_sectors > $free_sectors
# Merge original holes with free sectors
merge_ranges $fiemap_ref $free_sectors > $merged_sectors

# Check that all holes after fstrim call were already present before or
# that they match free space reported from FS
while read line; do
        from=${line% *}
        to=${line#* }
	if ! $AWK_PROG -v s=$from -v e=$to \
		'{ if ($1 <= s && e <= $2) found = 1};
		END { if(found) exit 0; else exit 1}' $merged_sectors
	then
		echo "Sectors $from-$to are not marked as free!"

		# Dump the state to make it easier to debug this...
		echo free_sectors >> $seqres.full
		sort -g < $free_sectors >> $seqres.full
		echo fiemap_ref >> $seqres.full
		sort -g < $fiemap_ref >> $seqres.full
		echo merged_sectors >> $seqres.full
		sort -g < $merged_sectors >> $seqres.full
		echo fiemap_after >> $seqres.full
		sort -g < $fiemap_after >> $seqres.full
		exit
	fi
done < $fiemap_after
echo "done."

_unmount $loop_mnt &>/dev/null
_destroy_loop_device $loop_dev
unset loop_dev

status=0
exit
