#!/bin/bash

# set _FORCE_LOCAL environment variable to run blockwise unit tests even on local
# nfs. Some tests will fail because nfs is eager to write for example 4095 bytes
# in O_DIRECT mode.

BW_UNIT=./unit-utils-io
STRACE=strace
MNT_DIR=./mnt_bwunit
LOCAL_FILE=./blockwise_localfile

# $1 path to scsi debug bdev
scsi_debug_teardown() {
	local _tries=15;

	while [ -b "$1" -a $_tries -gt 0 ]; do
		rmmod scsi_debug >/dev/null 2>&1
		if [ -b "$1" ]; then
			sleep .1
			_tries=$((_tries-1))
		fi
	done

	test ! -b "$1" || rmmod scsi_debug >/dev/null 2>&1
}

cleanup() {
	if [ -d "$MNT_DIR" ] ; then
	    umount -f $MNT_DIR 2>/dev/null
	    rmdir $MNT_DIR 2>/dev/null
	fi
	rm -f $LOCAL_FILE 2> /dev/null
	scsi_debug_teardown "$DEV" || exit 100
}

fail()
{
	if [ -n "$1" ] ; then echo "FAIL $1" ; else echo "FAIL" ; fi
	cleanup
	exit 100
}

fail_count()
{
	echo "$MSG[FAIL]"
	FAILS=$((FAILS+1))
}

warn_count()
{
	echo "$MSG[WARNING]"
	WARNS=$((WARNS+1))
}

skip()
{
	echo "TEST SKIPPED: $1"
	cleanup
	exit 77
}

add_device() {
	rmmod scsi_debug >/dev/null 2>&1
	if [ -d /sys/module/scsi_debug ] ; then
		skip "Cannot use scsi_debug module (in use or compiled-in)."
	fi
	modprobe scsi_debug $@ delay=0 >/dev/null 2>&1
	if [ $? -ne 0 ] ; then
		skip "This kernel seems to not support proper scsi_debug module."
	fi
	sleep 1
	DEV=$(grep -l -e scsi_debug /sys/block/*/device/model | cut -f4 -d /)
	DEV="/dev/$DEV"
	[ -b $DEV ] || fail "Cannot find $DEV."
}

falloc() {
	dd if=/dev/zero of=$2 bs=1M count=$1 2> /dev/null
}

run_all_in_fs() {
	for file in $(ls img_fs_*.img.xz) ; do
		echo -n "Run tests in $file put on top block device. "
		xz -d -c $file | dd of=$DEV bs=1M 2>/dev/null || fail "bad image"
		[ ! -d $MNT_DIR ] && mkdir $MNT_DIR
		mount $DEV $MNT_DIR 2>/dev/null
		if [ $? -ne 0 ]; then
			echo "[N/A]"
			continue;
		fi
		rm -rf $MNT_DIR/* 2>/dev/null
		local tfile=$MNT_DIR/bwunit_tstfile
		falloc $DEVSIZEMB $tfile || fail "enospc?"
		local iobsize=$(stat -c "%o" $tfile)
		test -n "$iobsize" -a $iobsize -gt 0 || fail
		local oldbsize=$BSIZE
		BSIZE=$iobsize
		run_all $tfile
		BSIZE=$oldbsize
		umount $MNT_DIR || fail
		echo "[OK]"
	done
}

trunc_file() {
	test $1 -eq 0 || truncate -c -s $1 $2 2>/dev/null || dd if=/dev/zero of=$2 bs=$1 count=1 2>/dev/null || fail "Failed to truncate test file $2."
}

RUN() {
	local _res=$1
	shift
	local _dev=$1
	shift
	local _fn=$1
	shift
	local _type="bdev"
	local _fsize=0

	test -b $_dev || {
		_type="file"
		_fsize=$(stat -c "%s" $_dev)
	}

	case "$_res" in
	P)
		MSG="Testing $_fn on $_type with params $@ [expecting TRUE]..."
		$BW_UNIT $_dev $_fn $@
		if [ $? -ne 0 ]; then
			if [ $_type = "file" ]; then
				warn_count
			else
				fail_count
			fi
			trunc_file $_fsize $_dev
			test -z "$STRACE" || $STRACE -o ./$BW_UNIT-fail-$FAILS-should-pass.log $BW_UNIT $_dev $_fn $@ 2> /dev/null
		else
			MSG="$MSG[OK]"
		fi
		;;
	F)
		MSG="Testing $_fn on $_type with params $@ [expecting FALSE]..."
		$BW_UNIT $_dev $_fn $@ 2> /dev/null
		if [ $? -eq 0 ]; then
			if [ $_type = "file" ]; then
				warn_count
			else
				fail_count
			fi
			trunc_file $_fsize $_dev
			test -z "$STRACE" || $STRACE -o ./$BW_UNIT-fail-$FAILS-should-fail.log $BW_UNIT $_dev $_fn $@ 2> /dev/null
		else
			MSG="$MSG[OK]"
		fi
		;;
	*)
		fail "Internal test error"
		;;
	esac

	trunc_file $_fsize $_dev
}

run_all() {
	if [ -b "$1" ]; then
		BD_FAIL="F"
	else
		BD_FAIL="P"
	fi

	# buffer io support only blocksize aligned ios
	# device/file fn_name length
	RUN "P" $1 read_buffer $BSIZE
	RUN "P" $1 read_buffer $((2*BSIZE))
	RUN "F" $1 read_buffer $((BSIZE-1))
	RUN "F" $1 read_buffer $((BSIZE+1))
	RUN "P" $1 read_buffer 0

	RUN "P" $1 write_buffer $BSIZE
	RUN "P" $1 write_buffer $((2*BSIZE))

	RUN "F" $1 write_buffer $((BSIZE-1))
	RUN "F" $1 write_buffer $((BSIZE+1))
	RUN "F" $1 write_buffer 0

	# basic blockwise functions
	# device/file fn_name length bsize
	RUN "P" $1 read_blockwise 0 $BSIZE
	RUN "P" $1 read_blockwise $((BSIZE)) $BSIZE
	RUN "P" $1 read_blockwise $((BSIZE-1)) $BSIZE
	RUN "P" $1 read_blockwise $((BSIZE+1)) $BSIZE
	RUN "P" $1 read_blockwise $((DEVSIZE)) $BSIZE
	RUN "P" $1 read_blockwise $((DEVSIZE-1)) $BSIZE
	RUN "F" $1 read_blockwise $((DEVSIZE+1)) $BSIZE

	RUN "P" $1 write_blockwise 0 $BSIZE
	RUN "P" $1 write_blockwise $((BSIZE)) $BSIZE
	RUN "P" $1 write_blockwise $((BSIZE-1)) $BSIZE
	RUN "P" $1 write_blockwise $((BSIZE+1)) $BSIZE
	RUN "P" $1 write_blockwise $((DEVSIZE)) $BSIZE
	RUN "P" $1 write_blockwise $((DEVSIZE-1)) $BSIZE
	RUN "$BD_FAIL" $1 write_blockwise $((DEVSIZE+1)) $BSIZE

	# seek variant blockwise functions
	# device/file fn_name length bsize offset
	RUN "P" $1 read_lseek_blockwise 0 $BSIZE 0
	RUN "P" $1 read_lseek_blockwise 0 $BSIZE 1
	RUN "P" $1 read_lseek_blockwise 0 $BSIZE $((DEVSIZE))
	# length = 0 is significant here
	RUN "P" $1 read_lseek_blockwise 0 $BSIZE $((DEVSIZE+1))

	# beginning of device
	RUN "P" $1 read_lseek_blockwise 1 $BSIZE 0
	RUN "P" $1 read_lseek_blockwise 1 $BSIZE 1
	RUN "P" $1 read_lseek_blockwise 1 $BSIZE $((BSIZE-1))
	RUN "P" $1 read_lseek_blockwise 1 $BSIZE $((BSIZE/2))

	# somewhere in the 'middle'
	RUN "P" $1 read_lseek_blockwise 1 $BSIZE $BSIZE
	RUN "P" $1 read_lseek_blockwise 1 $BSIZE $((BSIZE+1))
	RUN "P" $1 read_lseek_blockwise 1 $BSIZE $((2*BSIZE-1))
	RUN "P" $1 read_lseek_blockwise 1 $BSIZE $((BSIZE+BSIZE/2-1))

	# cross-sector tests
	RUN "P" $1 read_lseek_blockwise 2 $BSIZE $((BSIZE-1))
	RUN "P" $1 read_lseek_blockwise $((BSIZE+1)) $BSIZE $((BSIZE-1))
	RUN "P" $1 read_lseek_blockwise $((BSIZE+2)) $BSIZE $((BSIZE-1))
	RUN "P" $1 read_lseek_blockwise 2 $BSIZE $((2*BSIZE-1))
	RUN "P" $1 read_lseek_blockwise $((BSIZE+1)) $BSIZE $((2*BSIZE-1))
	RUN "P" $1 read_lseek_blockwise $((BSIZE+2)) $BSIZE $((2*BSIZE-1))

	# including one whole sector
	RUN "P" $1 read_lseek_blockwise $((BSIZE+2)) $BSIZE $((BSIZE))
	RUN "P" $1 read_lseek_blockwise $((2*BSIZE)) $BSIZE $((BSIZE+1))
	RUN "P" $1 read_lseek_blockwise $((2*BSIZE)) $BSIZE $((BSIZE-1))
	RUN "P" $1 read_lseek_blockwise $((BSIZE+2)) $BSIZE $((BSIZE-1))
	RUN "P" $1 read_lseek_blockwise $((2*BSIZE)) $BSIZE $((BSIZE+1))
	RUN "P" $1 read_lseek_blockwise $((3*BSIZE-2)) $BSIZE $((BSIZE+1))

	# hiting exactly the sector boundary
	RUN "P" $1 read_lseek_blockwise $((BSIZE-1)) $BSIZE 1
	RUN "P" $1 read_lseek_blockwise $((BSIZE-1)) $BSIZE $((BSIZE+1))
	RUN "P" $1 read_lseek_blockwise $((BSIZE+1)) $BSIZE $((BSIZE-1))
	RUN "P" $1 read_lseek_blockwise $((BSIZE+1)) $BSIZE $((2*BSIZE-1))

	# device end
	RUN "P" $1 read_lseek_blockwise 1 $BSIZE $((DEVSIZE-1))
	RUN "P" $1 read_lseek_blockwise $((BSIZE-1)) $BSIZE $((DEVSIZE-BSIZE+1))
	RUN "P" $1 read_lseek_blockwise $((BSIZE)) $BSIZE $((DEVSIZE-BSIZE))
	RUN "P" $1 read_lseek_blockwise $((BSIZE+1)) $BSIZE $((DEVSIZE-BSIZE-1))

	# this must fail on both device and file
	RUN "F" $1 read_lseek_blockwise 1 $BSIZE $((DEVSIZE))
	RUN "F" $1 read_lseek_blockwise $((BSIZE-1)) $BSIZE $((DEVSIZE-BSIZE+2))
	RUN "F" $1 read_lseek_blockwise $((BSIZE)) $BSIZE $((DEVSIZE-BSIZE+1))
	RUN "F" $1 read_lseek_blockwise $((BSIZE+1)) $BSIZE $((DEVSIZE-BSIZE))

	RUN "P" $1 write_lseek_blockwise 0 $BSIZE 0
	# TODO: this may pass but must not write a byte (write(0) is undefined).
	# 	Test it with underlying dm-error or phony read/write syscalls.
	#	Skipping read is optimization.
	# HINT: currently it performs useless write and read as well
	RUN "P" $1 write_lseek_blockwise 0 $BSIZE 1
	RUN "P" $1 write_lseek_blockwise 0 $BSIZE $BSIZE

	# beginning of device
	RUN "P" $1 write_lseek_blockwise 1 $BSIZE 0
	RUN "P" $1 write_lseek_blockwise 1 $BSIZE 1
	RUN "P" $1 write_lseek_blockwise 1 $BSIZE $((BSIZE-1))
	RUN "P" $1 write_lseek_blockwise 1 $BSIZE $((BSIZE/2))

	# somewhere in the 'middle'
	RUN "P" $1 write_lseek_blockwise 1 $BSIZE $BSIZE
	RUN "P" $1 write_lseek_blockwise 1 $BSIZE $((BSIZE+1))
	RUN "P" $1 write_lseek_blockwise 1 $BSIZE $((2*BSIZE-1))
	RUN "P" $1 write_lseek_blockwise 1 $BSIZE $((BSIZE+BSIZE/2-1))

	# cross-sector tests
	RUN "P" $1 write_lseek_blockwise 2 $BSIZE $((BSIZE-1))
	RUN "P" $1 write_lseek_blockwise $((BSIZE+1)) $BSIZE $((BSIZE-1))
	RUN "P" $1 write_lseek_blockwise $((BSIZE+2)) $BSIZE $((BSIZE-1))
	RUN "P" $1 write_lseek_blockwise 2 $BSIZE $((2*BSIZE-1))
	RUN "P" $1 write_lseek_blockwise $((BSIZE+1)) $BSIZE $((2*BSIZE-1))
	RUN "P" $1 write_lseek_blockwise $((BSIZE+2)) $BSIZE $((2*BSIZE-1))

	# including one whole sector
	RUN "P" $1 write_lseek_blockwise $((BSIZE+2)) $BSIZE $((BSIZE))
	RUN "P" $1 write_lseek_blockwise $((2*BSIZE)) $BSIZE $((BSIZE+1))
	RUN "P" $1 write_lseek_blockwise $((2*BSIZE)) $BSIZE $((BSIZE-1))
	RUN "P" $1 write_lseek_blockwise $((BSIZE+2)) $BSIZE $((BSIZE-1))
	RUN "P" $1 write_lseek_blockwise $((2*BSIZE)) $BSIZE $((BSIZE+1))
	RUN "P" $1 write_lseek_blockwise $((3*BSIZE-2)) $BSIZE $((BSIZE+1))

	# hiting exactly the sector boundary
	RUN "P" $1 write_lseek_blockwise $((BSIZE-1)) $BSIZE 1
	RUN "P" $1 write_lseek_blockwise $((BSIZE-1)) $BSIZE $((BSIZE+1))
	RUN "P" $1 write_lseek_blockwise $((BSIZE+1)) $BSIZE $((BSIZE-1))
	RUN "P" $1 write_lseek_blockwise $((BSIZE+1)) $BSIZE $((2*BSIZE-1))

	# device end
	RUN "P" $1 write_lseek_blockwise 1 $BSIZE $((DEVSIZE-1))
	RUN "P" $1 write_lseek_blockwise $((BSIZE-1)) $BSIZE $((DEVSIZE-BSIZE+1))
	RUN "P" $1 write_lseek_blockwise $((BSIZE)) $BSIZE $((DEVSIZE-BSIZE))
	RUN "P" $1 write_lseek_blockwise $((BSIZE+1)) $BSIZE $((DEVSIZE-BSIZE-1))

	# this must fail on device, but pass on file (which is unfortunate and maybe design mistake)
	RUN "$BD_FAIL" $1 write_lseek_blockwise 1 $BSIZE $((DEVSIZE))
	RUN "$BD_FAIL" $1 write_lseek_blockwise $((BSIZE-1)) $BSIZE $((DEVSIZE-BSIZE+2))
	RUN "$BD_FAIL" $1 write_lseek_blockwise $((BSIZE)) $BSIZE $((DEVSIZE-BSIZE+1))
	RUN "$BD_FAIL" $1 write_lseek_blockwise $((BSIZE+1)) $BSIZE $((DEVSIZE-BSIZE))
}

command -v $STRACE >/dev/null || unset STRACE
test -x $BW_UNIT || skip "Run \"make `basename $BW_UNIT`\" first"

FAILS=0
WARNS=0
DEVSIZEMB=2
DEVSIZE=$((DEVSIZEMB*1024*1024))

PAGE_SIZE=$(getconf PAGE_SIZE)
echo "System PAGE_SIZE=$PAGE_SIZE"

echo "Run tests in local filesystem"
falloc $DEVSIZEMB $LOCAL_FILE || fail "Failed to create file in local filesystem."
BSIZE=$(stat -c "%o" $LOCAL_FILE)
if [ $BSIZE -gt $((512*1024)) ]; then
	echo "Detected file block size: $BSIZE bytes"
	echo "Tuning it down to system page size ($PAGE_SIZE bytes)"
	BSIZE=$PAGE_SIZE
fi
run_all $LOCAL_FILE

[ $(id -u) -eq 0 ] || {
	echo "WARNING: You must be root to run remaining tests."
	test $FAILS -eq 0 || fail "($FAILS wrong result(s) in total)"
	cleanup
	exit 0
}

DEVBSIZE=512
BSIZE=$DEVBSIZE
EXP=0
DEVSIZEMBIMG=32

echo "# Create classic 512B drive"
echo "# (logical_block_size=$DEVBSIZE, physical_block_size=$((DEVBSIZE*(1<<EXP))))"
add_device dev_size_mb=$DEVSIZEMB sector_size=$DEVBSIZE physblk_exp=$EXP num_tgts=1
run_all $DEV
cleanup
add_device dev_size_mb=$DEVSIZEMBIMG sector_size=$DEVBSIZE physblk_exp=$EXP num_tgts=1
run_all_in_fs
cleanup

EXP=3
echo "# Create desktop-class 4K drive"
echo "# (logical_block_size=$DEVBSIZE, physical_block_size=$((DEVBSIZE*(1<<EXP))))"
add_device dev_size_mb=$DEVSIZEMB physblk_exp=$EXP sector_size=$DEVBSIZE num_tgts=1
run_all $DEV
BSIZE=$((DEVBSIZE*(1<<EXP)))
run_all $DEV
cleanup

add_device dev_size_mb=$DEVSIZEMBIMG physblk_exp=$EXP sector_size=$DEVBSIZE num_tgts=1
run_all_in_fs
cleanup

DEVBSIZE=4096
BSIZE=$DEVBSIZE
EXP=0
echo "# Create enterprise-class 4K drive"
echo "# (logical_block_size=$DEVBSIZE, physical_block_size=$((DEVBSIZE*(1<<EXP))))"
add_device dev_size_mb=$DEVSIZEMB physblk_exp=$EXP sector_size=$DEVBSIZE num_tgts=1
run_all $DEV
cleanup
add_device dev_size_mb=$DEVSIZEMBIMG sector_size=$DEVBSIZE physblk_exp=$EXP num_tgts=1
run_all_in_fs
cleanup

test $WARNS -eq 0 || echo "(WARNING: $WARNS suspicious result(s) in total)"
test $FAILS -eq 0 || fail "($FAILS wrong result(s) in total)"
