#!/bin/bash
# SPDX-License-Identifier: GPL-3.0+
# Copyright (C) 2017 Omar Sandoval
#
# Default helper functions.

shopt -s extglob

. common/shellcheck
# Include fio helpers by default.
. common/fio
. common/cgroup

# If a test runs multiple "subtests", then each subtest should typically run
# for TIMEOUT / number of subtests.
_divide_timeout() {
	local num_tests="$1"
	if [[ "${TIMEOUT:-}" ]]; then
		((TIMEOUT = (TIMEOUT + num_tests - 1) / num_tests))
	fi
}

_have_root() {
	if [[ $EUID -ne 0 ]]; then
		SKIP_REASONS+=("not running as root")
		return 1
	fi
	return 0
}

_module_file_exists()
{
	local ko_underscore=${1//-/_}.ko
	local ko_hyphen=${1//_/-}.ko
	local libpath
	local -i count

	libpath="/lib/modules/$(uname -r)/kernel"
	[[ ! -d $libpath ]] && return 1
	count=$(find "$libpath" -name "$ko_underscore*" -o \
		     -name "$ko_hyphen*" | wc -l)
	((count)) && return 0
	return 1
}

# Check that the specified module or driver is available, regardless of whether
# it is built-in or built separately as a module. Load the module if it is
# loadable and not yet loaded. In this case, the loaded module is unloaded at
# test case end regardless of whether the test case is skipped or executed.
_have_driver()
{
	local modname="${1//-/_}"

	if [ -d "/sys/module/${modname}" ]; then
		return 0
	fi

	if ! modprobe -q "${modname}"; then
		SKIP_REASONS+=("driver ${modname} is not available")
		return 1
	fi

	MODULES_TO_UNLOAD+=("${modname}")

	return 0
}

# Check that the specified module is available as a loadable module and not
# built-in the kernel.
_have_module() {
	if ! _module_file_exists "${1}"; then
		SKIP_REASONS+=("${1} module is not available")
		return 1
	fi
	return 0
}

_have_module_param() {
	if [ -d "/sys/module/$1" ]; then
		if [ -e "/sys/module/$1/parameters/$2" ]; then
			return 0
		fi
	fi

	if ! modinfo -F parm -0 "$1" | grep -q -z "^$2:"; then
		SKIP_REASONS+=("$1 module does not have parameter $2")
		return 1
	fi
	return 0
}

_have_module_param_value() {
	local modname="${1/-/_}"
	local param="$2"
	local expected_value="$3"
	local value

	if ! _have_driver "$modname"; then
		return 1;
	fi

	if ! _have_module_param "$modname" "$param"; then
		return 1
	fi

	value=$(cat "/sys/module/$modname/parameters/$param")
	if [[ "${value}" != "$expected_value" ]]; then
		SKIP_REASONS+=("$modname module parameter $param must be set to $expected_value")
		return 1
	fi

	return 0
}

_have_program() {
	if command -v "$1" >/dev/null 2>&1; then
		return 0
	fi
	SKIP_REASONS+=("$1 is not available")
	return 1
}

# Check whether the iproute2 snapshot is greater than or equal to $1.
_have_iproute2() {
	local snapshot

	if ! snapshot=$(ip -V | sed 's/ip utility, iproute2-ss//'); then
		SKIP_REASONS+=("ip utility not found")
		return 1
	fi
	if [[ "$snapshot" =~ ^[0-9]+$ && "$snapshot" -lt "$1" ]]; then
		SKIP_REASONS+=("ip utility too old")
		return 1
	fi
	return 0
}

_have_src_program() {
	if [[ ! -x "$SRCDIR/$1" ]]; then
		SKIP_REASONS+=("$1 was not built; run \`make\`")
		return 1
	fi
	return 0
}

_have_loop() {
	_have_driver loop && _have_program losetup
}

_have_blktrace() {
	# CONFIG_BLK_DEV_IO_TRACE might still be disabled, but this is easier
	# to check. We can fix it if someone complains.
	if [[ ! -d /sys/kernel/debug/block ]]; then
		SKIP_REASONS+=("CONFIG_DEBUG_FS is not enabled")
		return 1
	fi
	_have_program blktrace
}

_have_configfs() {
	if ! findmnt -t configfs /sys/kernel/config >/dev/null; then
		SKIP_REASONS+=("configfs is not mounted at /sys/kernel/config")
		return 1
	fi
	return 0
}

# Check if the specified kernel config files are available.
_have_kernel_config_file() {
	if [[ ! -f /proc/config.gz && ! -f /boot/config-$(uname -r) ]]; then
		SKIP_REASONS+=("kernel $(uname -r) config not found")
		return 1
	fi

	return 0
}

# Check if the specified kernel option is defined.
_check_kernel_option() {
	local f opt=$1

	for f in /proc/config.gz /boot/config-$(uname -r); do
		[ -e "$f" ] || continue
		if zgrep -q "^CONFIG_${opt}=[my]$" "$f"; then
			return 0
		fi
	done

	return 1
}

# Combine _have_kernel_config_file() and _check_kernel_option().
# Set SKIP_RESAON when _check_kernel_option() returns false.
_have_kernel_option() {
	local opt=$1

	_have_kernel_config_file || return
	if ! _check_kernel_option "$opt"; then
		SKIP_REASONS+=("kernel option $opt has not been enabled")
		return 1
	fi

	return 0
}

# Check whether the version of the running kernel is greater than or equal to
# $1.$2.$3
_have_kver() {
	local d=$1 e=$2 f=$3

	IFS='.' read -r a b c < <(uname -r | sed 's/-.*//;s/[^.0-9]//')
	if [ $((a * 65536 + b * 256 + c)) -lt $((d * 65536 + e * 256 + f)) ];
	then
		SKIP_REASONS+=("Kernel version too old")
		return 1
	fi
}

_have_tracefs() {
	stat /sys/kernel/debug/tracing/trace > /dev/null 2>&1
	if ! findmnt -t tracefs /sys/kernel/debug/tracing >/dev/null; then
		SKIP_REASONS+=("tracefs is not mounted at /sys/kernel/debug/tracing")
		return 1
	fi
	return 0
}

_have_tracepoint() {
	local event=$1

	if [[ ! -d /sys/kernel/debug/tracing/events/${event} ]]; then
		SKIP_REASONS+=("tracepoint ${event} does not exist")
		return 1
	fi
	return 0
}

_have_fs() {
	modprobe "$1" >/dev/null 2>&1
	if [[ ! -d "/sys/fs/$1" ]]; then
		SKIP_REASONS+=("kernel does not support filesystem $1")
		return 1
	fi
}

_require_min_cpus() {
	if [[ $(nproc) -ge $1 ]]; then
		return 0
	fi
	SKIP_REASONS+=("minimum $1 cpus required")
	return 1
}

_test_dev_is_rotational() {
	[[ $(cat "${TEST_DEV_SYSFS}/queue/rotational") -ne 0 ]]
}

_require_test_dev_is_rotational() {
	if ! _test_dev_is_rotational; then
		SKIP_REASONS+=("$TEST_DEV is not rotational")
		return 1
	fi
	return 0
}

_test_dev_can_discard() {
	[[ $(cat "${TEST_DEV_SYSFS}/queue/discard_max_bytes") -gt 0 ]]
}

_require_test_dev_can_discard() {
	if ! _test_dev_can_discard; then
		SKIP_REASONS+=("$TEST_DEV does not support discard")
		return 1
	fi
	return 0
}

_test_dev_queue_get() {
	if [[ $1 = scheduler ]]; then
		sed -e 's/.*\[//' -e 's/\].*//' "${TEST_DEV_SYSFS}/queue/scheduler"
	else
		cat "${TEST_DEV_SYSFS}/queue/$1"
	fi
}

_test_dev_queue_set() {
	# For bash >=4.3 we'd write if [[ ! -v TEST_DEV_QUEUE_SAVED["$1"] ]].
	if [[ -z ${TEST_DEV_QUEUE_SAVED["$1"]} &&
	      ${TEST_DEV_QUEUE_SAVED["$1"]-unset} == unset ]]; then
		TEST_DEV_QUEUE_SAVED["$1"]="$(_test_dev_queue_get "$1")"
	fi
	echo "$2" >"${TEST_DEV_SYSFS}/queue/$1"
}

_require_test_dev_is_pci() {
	if ! readlink -f "$TEST_DEV_SYSFS/device" | grep -q pci; then
		# nvme needs some special casing
		if readlink -f "$TEST_DEV_SYSFS/device" | grep -q nvme; then
			local blkdev
			local ctrldev
			# First get the controller device from the namespace blockdev
			blkdev="$(echo "$TEST_DEV_SYSFS" | cut -d '/' -f 7)"
			ctrldev="$(echo "$blkdev" | grep -Eo 'nvme[0-9]+')"
			# Then get the pci device from the controller device
			if readlink -f "$ctrldev/device" | grep -1 pci; then
				return 0
			fi
		fi

		SKIP_REASONS+=("$TEST_DEV is not a PCI device")
		return 1
	fi
	return 0
}

_get_pci_from_dev_sysfs() {
	readlink -f "$1/device" | \
		grep -Eo '[0-9a-f]{4,5}:[0-9a-f]{2}:[0-9a-f]{2}\.[0-9a-f]' | \
		tail -1
}

_get_pci_dev_from_blkdev() {
	_get_pci_from_dev_sysfs "$TEST_DEV_SYSFS"
}

_get_pci_parent_from_blkdev() {
	readlink -f "$TEST_DEV_SYSFS/device" | \
		grep -Eo '[0-9a-f]{4,5}:[0-9a-f]{2}:[0-9a-f]{2}\.[0-9a-f]' | \
		tail -2 | head -1
}

_require_test_dev_size() {
	local require_sz_mb
	local test_dev_sz_mb

	require_sz_mb="$(convert_to_mb "$1")"
	test_dev_sz_mb="$(($(blockdev --getsize64 "$TEST_DEV")/1024/1024))"

	if (( "$test_dev_sz_mb" < "$require_sz_mb" )); then
		SKIP_REASONS+=("${TEST_DEV} required at least ${require_sz_mb}m")
		return 1
	fi
	return 0
}

_require_test_dev_in_hotplug_slot() {
	local parent
	parent="$(_get_pci_parent_from_blkdev)"

	local slt_cap
	slt_cap="$(setpci -s "${parent}" CAP_EXP+14.w)"
	if [[ $((0x${slt_cap} & 0x60)) -ne $((0x60)) ]]; then
		SKIP_REASONS+=("$TEST_DEV is not in a hot pluggable slot")
		return 1
	fi
	return 0
}

_test_dev_is_partition() {
	[[ -n ${TEST_DEV_PART_SYSFS} ]]
}

# Return max open zones or max active zones of the test target device.
# If the device has both, return smaller value.
_test_dev_max_open_active_zones() {
	local -i moz=0
	local -i maz=0

	if [[ -r "${TEST_DEV_SYSFS}/queue/max_open_zones" ]]; then
		moz=$(_test_dev_queue_get max_open_zones)
	fi

	if [[ -r "${TEST_DEV_SYSFS}/queue/max_active_zones" ]]; then
		maz=$(_test_dev_queue_get max_active_zones)
	fi

	if ((!moz)) || ((maz && maz < moz)); then
		echo "${maz}"
	else
		echo "${moz}"
	fi
}

_require_test_dev_is_partition() {
	if ! _test_dev_is_partition; then
		SKIP_REASONS+=("${TEST_DEV} is not a partition device")
		return 1
	fi
	return 0
}

_require_normal_user() {
	if ! id "$NORMAL_USER" >/dev/null 2>&1; then
		SKIP_REASONS+=("valid NORMAL_USER is not specfied")
		return 1
	fi
	return 0
}

# Prints a space-separated list with the names of all I/O schedulers supported
# by block device $1.
_io_schedulers() {
	local path=/sys/class/block/$1/queue/scheduler

	[ -e "${path}" ] || return 1
	sed 's/[][]//g' "${path}"
}

# Older versions of xfs_io use pwrite64 and such, so the error messages won't
# match current versions of xfs_io. See c52086226bc6 ("filter: xfs_io output
# has dropped "64" from error messages") in xfstests.
_filter_xfs_io_error() {
	sed -e 's/^\(.*\)64\(: .*$\)/\1\2/'
}

# System uptime in seconds.
_uptime_s() {
	awk '{ print int($1) }' /proc/uptime
}

_have_writeable_kmsg() {
	if [[ ! -w /dev/kmsg ]]; then
		SKIP_REASONS+=("cannot write to /dev/kmsg")
		return 1
	fi
	return 0
}

# Run the given command as NORMAL_USER
_run_user() {
	su "$NORMAL_USER" -c "$1"
}

convert_to_mb()
{
	local str=$1
	local res

	res=$(echo "${str}" | sed -n 's/^\([0-9]\+\)$/\1/p')
	if [[ -n "${res}" ]]; then
		echo "$((res / 1024 / 1024))"
	fi

	res=$(echo "${str}" | sed -n 's/^\([0-9]\+\)[mM]$/\1/p')
	if [[ -n "${res}" ]]; then
		echo "$((res))"
	fi

	res=$(echo "${str}" | sed -n 's/^\([0-9]\+\)[gG]$/\1/p')
	if [[ -n "${res}" ]]; then
		echo "$((res * 1024))"
	fi
}
