#! /bin/bash
# SPDX-License-Identifier: GPL-2.0
# Copyright (c) 2020 IBM Corporation. All Rights Reserved.
#
# FS QA Test 619
#
# ENOSPC regression test in a multi-threaded scenario. Test allocation
# strategies of the file system and validate space anomalies as reported by
# the system versus the allocated by the program.
#
# The test is motivated by a bug in ext4 systems where-in ENOSPC is
# reported by the file system even though enough space for allocations is
# available[1].
# [1]: https://patchwork.ozlabs.org/patch/1294003
#
# Linux kernel patch series that fixes the above regression:
# 53f86b170dfa ("ext4: mballoc: add blocks to PA list under same spinlock
#               after allocating blocks")
# cf5e2ca6c990 ("ext4: mballoc: refactor ext4_mb_discard_preallocations()")
# 07b5b8e1ac40 ("ext4: mballoc: introduce pcpu seqcnt for freeing PA to
#               improve ENOSPC handling")
# 8ef123fe02ca ("ext4: mballoc: refactor ext4_mb_good_group()")
# 993778306e79 ("ext4: mballoc: use lock for checking free blocks while
#               retrying")
#
. ./common/preamble
_begin_fstest auto rw enospc prealloc

FS_SIZE=$((240*1024*1024)) # 240MB
DEBUG=1 # set to 0 to disable debug statements in shell and c-prog
FACT=0.7

# Disk allocation methods
FALLOCATE=1
FTRUNCATE=2

# Helps to build TEST_VECTORS
SMALL_FILE_SIZE=$((512 * 1024)) # in Bytes
BIG_FILE_SIZE=$((1536 * 1024))  # in Bytes
MIX_FILE_SIZE=$((2048 * 1024))  # (BIG + SMALL small file size)

# Import common functions.
. ./common/filter

# Modify as appropriate.
_supported_fs generic
_require_scratch
_require_test_program "t_enospc"
_require_xfs_io_command "falloc"

debug()
{
	if [ $DEBUG -eq 1 ]; then
		echo "$1" >> $seqres.full
	fi
}

# Calculate the number of threads needed to fill the disk space
# Arguments
# $1: the size of a file
# $2: ratio in which $1 file should be split into multiple files.
# $3: percentage of the disk space should be used during the test
# Calculate the number of threads needed to fill the disk space
calc_thread_cnt()
{
	local file_ratio_unit=$1
	local file_ratio=$2
	local disk_saturation=$3
	local tot_avail_size
	local avail_size
	local thread_cnt

	IFS=',' read -ra fratio <<< $file_ratio
	file_ratio_cnt=${#fratio[@]}

	tot_avail_size=$($DF_PROG --block-size=1 $SCRATCH_MNT | $AWK_PROG 'FNR == 2 { print $5 }')
	avail_size=$(echo $tot_avail_size*$disk_saturation | $BC_PROG)
	thread_cnt=$(echo "$file_ratio_cnt*($avail_size/$file_ratio_unit)" | $BC_PROG)

	debug "Total available size: $tot_avail_size"
	debug "Available size: $avail_size"
	debug "Thread count: $thread_cnt"

	echo ${thread_cnt}
}

# Arguments
# $1: a string containing test configuration separated by a colon.
#     $1 is treated as an array of arguments to the function.
#     Description of each array element is given below.
#
# @1: name of the test
# @2: thread in t_enospec exerciser will allocate file of @2 size
# @3: defines the proportion in which the file size defined in @2
#     should be divided into two files.
#     (valid @3: more than two values are not allowed)
#                values should be comma separated)
#                sum of all values must be 1)
# @4: define the percentage of available memory should be used to
#     during the test.
# @5: defines the disk allocation method (fallocate/ftruncate)
# @6: number of the test should run
run_testcase()
{
	IFS=':' read -ra args <<< $1
	local test_name=${args[0]}
	local file_ratio_unit=${args[1]}
	local file_ratio=${args[2]}
	local disk_saturation=${args[3]}
	local disk_alloc_method=${args[4]}
	local test_iteration_cnt=${args[5]}
	local extra_args=""
	local thread_cnt

	if [ "$disk_alloc_method" == "$FALLOCATE" ]; then
		extra_args="$extra_args -f"
	fi

	# enable the debug statements in c program
	if [ "$DEBUG" -eq 1 ]; then
		extra_args="$extra_args -v"
	fi

	debug "============ Test details start ============"
	debug "Test name: $test_name"
	debug "File ratio unit: $file_ratio_unit"
	debug "File ratio: $file_ratio"
	debug "Disk saturation $disk_saturation"
	debug "Disk alloc method $disk_alloc_method"
	debug "Test iteration count: $test_iteration_cnt"
	debug "Extra arg: $extra_args"

	for i in $(eval echo "{1..$test_iteration_cnt}"); do
		# Setup the device
		_scratch_mkfs_sized $FS_SIZE >> $seqres.full 2>&1
		_scratch_mount

		debug "===== Test: $test_name iteration: $i starts ====="
		thread_cnt=$(calc_thread_cnt $file_ratio_unit $file_ratio $disk_saturation)

		# Start the test
		$here/src/t_enospc -t $thread_cnt -s $file_ratio_unit -r $file_ratio -p $SCRATCH_MNT $extra_args >> $seqres.full

		status=$(echo $?)
		if [ $status -ne 0 ]; then
			use_per=$($DF_PROG -h | grep $SCRATCH_MNT | awk '{print substr($6, 1, length($6)-1)}' | $BC_PROG)
			alloc_per=$(echo "$FACT * 100" | $BC_PROG)
			# We are here since t_enospc failed with an error code.
			# If the used filesystem space is still < available space - that means
			# the test failed due to FS wrongly reported ENOSPC.
			if [ $(echo "$use_per < $alloc_per" | $BC_PROG) -ne 0 ]; then
				if [ $status -eq 134 ]; then
					# SIGABRT asserted exit code = 134
					echo "FAIL: Aborted assertion faliure"
				elif [ $status -eq 7 ]; then
					# SIGBUS asserted exit code = 7
					echo "FAIL: ENOSPC BUS faliure"
				fi
				echo "$test_name failed at iteration count: $i"
				echo "$($DF_PROG -h $SCRATCH_MNT)"
				echo "Allocated: $alloc_per% Used: $use_per%"
				exit
			fi
		fi

		# Make space for other tests
		_scratch_unmount

		debug "===== Test: $test_name iteration: $i ends ====="
	done
	debug "============ Test details end ============="
}

declare -a TEST_VECTORS=(
# test-name:file-ratio-unit:file-ratio:disk-saturation:disk-alloc-method:test-iteration-cnt
"Small-file-fallocate-test:$SMALL_FILE_SIZE:1:$FACT:$FALLOCATE:3"
"Big-file-fallocate-test:$BIG_FILE_SIZE:1:$FACT:$FALLOCATE:3"
"Mix-file-fallocate-test:$MIX_FILE_SIZE:0.75,0.25:$FACT:$FALLOCATE:3"
"Small-file-ftruncate-test:$SMALL_FILE_SIZE:1:$FACT:$FTRUNCATE:3"
"Big-file-ftruncate-test:$BIG_FILE_SIZE:1:$FACT:$FTRUNCATE:3"
"Mix-file-ftruncate-test:$MIX_FILE_SIZE:0.75,0.25:$FACT:$FTRUNCATE:3"
)

# real QA test starts here
for i in "${TEST_VECTORS[@]}"; do
	run_testcase $i
done

echo "Silence is golden"
status=0
exit
