#!/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_REASON="not running as root"
		return 1
	fi
	return 0
}

_have_modules() {
	local missing=()
	local module

	for module in "$@"; do
		if ! modprobe -n -q "$module"; then
			missing+=("$module")
		fi
	done
	if [[ ${#missing} -gt 1 ]]; then
		SKIP_REASON="the following modules are not available: ${missing[*]}"
		return 1
	elif [[ ${#missing} -eq 1 ]]; then
		SKIP_REASON="${missing[0]} 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_REASON="$1 module does not have parameter $2"
		return 1
	fi
	return 0
}

_have_module_param_value() {
	local value

	modprobe "$1"

	if ! _have_module_param "$1" "$2"; then
		return 1
	fi

	value=$(cat "/sys/module/$1/parameters/$2")
	if [[ "${value}" != "$3" ]]; then
		SKIP_REASON="$1 module parameter $2 must be set to $3"
		return 1
	fi

	return 0
}

_have_program() {
	if command -v "$1" >/dev/null 2>&1; then
		return 0
	fi
	SKIP_REASON="$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_REASON="ip utility not found"
		return 1
	fi
	if [[ "$snapshot" =~ ^[0-9]+$ && "$snapshot" -lt "$1" ]]; then
		SKIP_REASON="ip utility too old"
		return 1
	fi
	return 0
}

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

_have_loop() {
	_have_modules 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_REASON="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_REASON="configfs is not mounted at /sys/kernel/config"
		return 1
	fi
	return 0
}

_have_kernel_option() {
	local f opt=$1

	SKIP_REASON="kernel $(uname -r) config not found"
	for f in /proc/config.gz /boot/config-$(uname -r); do
		[ -e "$f" ] || continue
		if zgrep -q "^CONFIG_${opt}=[my]$" "$f"; then
			unset SKIP_REASON
			return 0
		else
			SKIP_REASON="kernel option $opt has not been enabled"
		fi
	done

	return 1
}

# 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/-.*//')
	if [ $((a * 65536 + b * 256 + c)) -lt $((d * 65536 + e * 256 + f)) ];
	then
		SKIP_REASON="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_REASON="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_REASON="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_REASON="kernel does not support filesystem $1"
		return 1
	fi
}

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

_require_test_dev_is_rotational() {
	if ! _test_dev_is_rotational; then
		SKIP_REASON="$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_REASON="$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_REASON="$TEST_DEV is not a PCI device"
		return 1
	fi
	return 0
}

_get_pci_dev_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 -1
}

_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_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_REASON="$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_REASON="${TEST_DEV} is not a partition device"
		return 1
	fi
	return 0
}

# 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
}

# Arguments: module to unload ($1) and retry count ($2).
unload_module() {
	local i m=$1 rc=${2:-1}

	[ ! -e "/sys/module/$m" ] && return 0
	for ((i=rc;i>0;i--)); do
		modprobe -r "$m"
		[ ! -e "/sys/module/$m" ] && return 0
		sleep .1
	done
	return 1
}
