#!/bin/sh
#
# Cukinia a firmware validation framework
#
# Copyright (C) 2017-2024 Savoir-faire Linux Inc.
#
# This program is free software, distributed under the Apache License
# version 2.0. See LICENSE for details.

CUKINIA_VERSION="0.8.1-dev"

# Default configuration file
CUKINIA_CONF='/etc/cukinia/cukinia.conf'

# Generic user functions templated to _cukinia_xxx()
GENERIC_FUNCTS='cmd
		cmdline
		dns_resolve
		gpio_libgpiod
		gpio_sysfs
		group
		http_request
		i2c
		kconf
		kmod
		knoerror
		kversion
		listen4
		mount
		netif_has_ip
		netif_is_up
		process_with_args
		process
		kthread
		python_pkg
		symlink
		sysctl
		systemd_failed
		systemd_unit
		test
		user
		user_memberof'

# By default log to stdout
__log_file="/dev/stdout"

# Initialize suite id
__suite_id=0

# Ensure cleanup of temporary files
trap cleanup 0

# Cleanup of temporary files
cleanup()
{
	rm -f $__stderr_tmp $__stdout_tmp $__logfile_tmp $__junitfile_tmp

	if [ -n "$__logfile_tmp" ]; then
		rm -f $__logfile_tmp.*
	fi
}

# Standard help message
usage()
{
	cat <<EOF
NAME:
	Cukinia - portable firmware validation framework.
	Copyright (C) 2017-2024 Savoir-faire Linux Inc.

	The name means "zucchini" in Polish. It is a nod to Cucumber,
	another neat test framework.


USAGE:
	cukinia [options] [config file]

OPTIONS:
	-h, --help	display this message
	-v, --version	display the version string
	-o <file>	output results to <file>
	-f <format>	set output format to <format>, currently supported:
       			csv junitxml
	--no-header	do not print headers in concerned output formats
	--trace         trace execution (for debugging)
EOF
}

# Parse single character options (sh builtin does not support long options)
while getopts ":o:f:hv-:" opt; do
	case $opt in
	o)
		__log_file=$OPTARG
		;;
	f)
		__log_format=$OPTARG
		;;
	h)
		usage
		exit 0
		;;
	v)
		echo "Cukinia version $CUKINIA_VERSION"
		exit 0
		;;
	# long options expantion (options without parameter only)
	-)
		case $OPTARG in
		help)
			usage
			exit 0
			;;
		no-header)
			__no_header=1
			;;
		trace)
			set -x
			;;
		version)
			echo "Cukinia version $CUKINIA_VERSION"
			exit 0
			;;
		*)
			printf "cukinia: $OPTARG: unsupported long option\n"
			;;
		esac
		;;
	*)
		printf "cukinia: $OPTARG: unsupported short option\n"
		;;
	esac
done
shift $((OPTIND-1))

# Temporary files
__logfile_tmp=$(mktemp)
__stderr_tmp=$(mktemp)
__stdout_tmp=$(mktemp)

# Init logging
cukinia_epoch=$(date +%s)
case "$__log_format" in
csv)
	if [ -z "$__no_header" ]; then
		echo "TEST MESSAGE, RESULT, TEST CLASS, TEST SUITE, TEST ID" >"$__logfile_tmp"
	else
		: >"$__logfile_tmp"
	fi
	;;

junitxml)
	if [ "$__log_file" = "/dev/stdout" ]; then
		usage
		echo "Fatal: junitxml requires -o <file>"
		exit 1
	fi
	;;

*)
	: >"$__logfile_tmp"
	;;
esac

# Take config file as first trailing argument
CUKINIA_CONF="$1"
if [ "$(dirname "$CUKINIA_CONF")" = "." ]; then
	CUKINIA_CONF="./$CUKINIA_CONF"
fi

# Use this file to override default config
if [ -r /etc/default/cukinia ]; then
	. /etc/default/cukinia
fi

# cukinia_conf_include: source additional configs
# arg: $* - configs to include
cukinia_conf_include()
{
	local f

	for f in "$@"; do
		# TODO: include the case where arg is a dir
		# in this case, we load every file in it!

		if [ ! -r "$f" ]; then
			echo >&2 "cukinia: FATAL: can't include \"$f\""
			exit 1
		fi
		. "$f"
	done
}

# logging: change log string options
# args: prefix "str" - prefix logs with "str"
# args: class "str" - sets classname for junitxml results
# args: suite "str" - sets testsuite for junitxml results
logging()
{
	case "$1" in
	prefix)
		shift
		__log_pfx="$*"
		;;
	class)
		if [ -n "$__log_format" ]; then
			shift
			__log_class="$*"
		fi
		;;
	suite)
		if [ "$__log_format" = "junitxml" ]; then
			shift
			_junitxml_add_suite "$1"
		elif [ "$__log_format" = "csv" ]; then
			shift
			__suite_name="$*"
		fi
		;;
	*)
		cukinia_log "logging: invalid parameter"
		return 1
		;;
	esac

	return 0
}

# cukinia_log: log results to the output media
cukinia_log()
{
	local message="$1"
	local result="$2"

	_should_log() {
		case "$result" in
		PASS|FAIL|SKIP) return 0 ;;
		esac
		return 1
	}

	case "$__log_format" in
	csv)
		if _should_log; then
			message=$(echo $message | sed -e "s/'/\\'/g") # ' -> \'
			echo "'$message',$result,$__log_class,$__suite_name,$__test_id" >>"$__logfile_tmp"
		fi
		;;

	junitxml)
		if _should_log; then
			_junitxml_add_case "$message" "$result"
		fi
		;;

	*)
		echo "${__log_pfx}$*"
		;;
	esac
}

# Retry current cukinia test with optionnal delay
# arg1..n: cukinia test arguments
_cukinia_retry()
{
	local arguments="$@"

	echo >&2 "Retry \"$__cukinia_cur_test\" in ${__wait_time:-0s}"
	__retry=$((__retry-1))
	sleep ${__wait_time:-0}
	cukinia_runner $arguments
}

# run the test and its context
# arg1..n: command to run and their arguments
cukinia_runner()
{
	local ret
	local result
	local arguments="$@"

	if [ -n "$__verbose" ]; then
		if [ "$__log_format" = "junitxml" ]; then
			"$@" 1>$__stdout_tmp 2>$__stderr_tmp
		else
			"$@"
		fi
	else
		"$@" >/dev/null 2>&1
	fi
	ret=$?

	if [ -n "$CUKINIA_ALWAYS_PASS" ]; then
		__not="pass"
	elif [ -n "$__skip_condition" ]; then
		_cukinia_result SKIP
		cukinia_skip=$((cukinia_skip + 1))
		return
	fi

	case "$__not..$ret" in
	pass..*)
		result=PASS
		;;
	1..0)
		result=FAIL
		;;
	1..*)
		result=PASS
		;;
	..0)
		result=PASS
		;;
	..*)
		result=FAIL
		;;
	esac

	case "$result" in
	PASS)
		if [ "${__on_success}" ]; then
			if [ "${__retry}" != 0 ]; then
				_cukinia_retry $arguments
				return $?
			fi
		fi
		cukinia_tests=$((cukinia_tests + 1))
		_cukinia_result PASS
		;;
	FAIL)
		if [ "${__on_failure}" ]; then
			if [ "${__retry}" != 0 ]; then
				_cukinia_retry $arguments
				return $?
			fi
		fi
		cukinia_failures=$((cukinia_failures + 1))
		cukinia_tests=$((cukinia_tests + 1))
		_cukinia_result FAIL
		;;
	esac

	return $ret
}

# cukinia_run_dir: run scripts in given directories
# arg: $*: list of directories
cukinia_run_dir()
{
	local dir

	for dir in "$@"; do
		[ -d "$dir" ] || continue
		_cukinia_run_dir "$dir"
	done
}

# test_id: add a test identifier to easily track it
# arg1: id name
# arg2..: command
test_id()
{
	local ret=0
	__test_id="$1"; shift

	"$@" || ret=1
	unset __test_id

	return $ret
}

# as: change the description of what follows
# arg1: "new description"
# arg2..: command
as()
{
	local ret=0
	__deco="$1"; shift

	"$@" || ret=1
	unset __deco

	return $ret
}

# not: negate the issue of what follows
not()
{
	local ret=1

	__not=1
	"$@" || ret=0
	unset __not

	return $ret
}

# verbose: the output of what follows will not be muted
# arg1..: command
verbose()
{
	local ret=0

	__verbose=1
	"$@" || ret=1
	unset __verbose

	return $ret
}

# _condition_run: run $@ if condition
# arg1: the condition
# arg2..: commands and arguments
_condition_run()
{
	local ret=0

	eval "$1" >/dev/null || __skip_condition=1
	shift

	"$@" || ret=1
	unset __skip_condition

	return $ret
}

# unless: condition test with a negative condition
# arg1: the condition expression
# arg2..: command
unless()
{
	local condition="$1"; shift
	_condition_run "! $condition" "$@"
}

# when: condition test when condition is true
# arg1: the condition expression
# arg2..: command
when()
{
	local condition="$1"; shift
	_condition_run "$condition" "$@"
}

# on: execute statement conditionally to test result
# arg1: Test result
# arg2..: command
on()
{
	local test_result="$1"; shift
	local ret=0

	if [ "$test_result" = "failure" ]; then
		__on_failure=1
	elif [ "$test_result" = "success" ]; then
		__on_success=1
	fi

	"$@" || ret=1
	unset __on_failure
	unset __on_success

	return $ret
}

# retry: Set a number of retry
# arg1: the number of retry
# arg2..: command
retry()
{
	local ret=0
	__retry="$1"; shift

	"$@" || ret=1
	unset __retry

	return $ret
}

# after: Set wait between each retry
# arg1: the wait time between each retry (in seconds)
# arg2..: command
after()
{
	local ret=0
	__wait_time="$1"; shift

	"$@" || ret=1
	unset __wait_time

	return $ret
}

# check_process: check if a process is running
# arg1: the process name which should be running
# arg2: boolean to check the full command line of the process in /proc/$pid/cmdline
# arg3: [optional] the user name
check_process(){
	local process=$1
	local check_cmdline=$2
	local user=$3
	local pidof_cmd
	local procdir
	local owner_uid
	local user_uid

	[ "$check_cmdline" = "1" ] && check_cmdline=true || check_cmdline=false

	if [ -n "$user" ]; then
		# make sure $user maps to a valid user
		user_uid="$(id -u "$user" 2>/dev/null)" # Use full path to id, because it has been defined as a cukinia function...
	fi

	if $check_cmdline; then
		# pidof command needs to be called only with first process name not full command line
		pidof_cmd="$(echo "$process" | awk '{print $1}')"
	else
		# when no full command line check, use the process name directly
		pidof_cmd="$process"
	fi

	for pid in $(pidof "$pidof_cmd"); do
		procdir="/proc/$pid"
		if $check_cmdline; then
			# match exact full command with expected process 
			[ "$(tr "\000" " " < "/proc/$pid/cmdline" 2>/dev/null | awk '{$1=$1; print}')" = "$process" ] || continue
		fi

		# skip kernel threads
		[ -x $procdir/exe ] || continue

		# process found, no owner check
		[ -z "$user" ] && return 0

		# check process owner
		owner_uid=$(awk '/^Uid:/ { print $2 }' < $procdir/status)
		[ "$owner_uid" = "$user_uid" ] && return 0
	done

	return 1
}

# _cukinia_python_pkg: try to import the python package
# arg1: the python package
_cukinia_python_pkg()
{
	local python_interpreter
	local package="$1"

	for python_interpreter in "python" "python3" "python2"; do
		python_interpreter="$(which "$python_interpreter")"
		test -n "$python_interpreter" && break
	done

	if [ -z "$python_interpreter" ]; then
		_cukinia_prepare "cukinia_python_pkg: interpreter not found"
		return 1
	fi

	_cukinia_prepare "Checking python package \"$package\" is ${__not:+NOT }available"
	"$python_interpreter" -c "import sys, pkgutil; sys.exit(0 if pkgutil.find_loader('$package') else 1)"
}

# _cukinia_prepare: Prepare test for result
# arg1: description of test
_cukinia_prepare()
{
	__cukinia_cur_test="${__deco:-${1}${__not:+ [!]}}"
}

# _cukinia_result: display test result
# arg1: "PASS" or "FAIL"
# arg2: optional error message
_cukinia_result()
{
	local result="$1"

	case "$__log_format" in
	csv|junitxml)
		cukinia_log "$__cukinia_cur_test" "$result"
		return
		;;
	esac

	case "$result" in
	PASS) result=$(_colorize green "PASS") ;;
	FAIL) result=$(_colorize red "FAIL") ;;
	SKIP) result=$(_colorize yellow2 "SKIP") ;;
	esac

	if [ "$__skip_condition" ]; then
		local suffix=" [$(_colorize yellow2 "unmet condition")]"
	fi

	cukinia_log "[$result] "${__test_id:+ ${__test_id} --}" ${__cukinia_cur_test}${suffix}"
}

# _cukinia_cmd: wrapper to any command
# arg1..n: arguments
_cukinia_cmd()
{
	_cukinia_prepare "Running \"$*\" is ${__not:+NOT }successful"
	"$@"
}

# _cukinia_cmdline: checks if kernel cmdline contains param $1
# arg1: match extended regex, param or param=val
_cukinia_cmdline()
{
	local match_regex="$1"

	_cukinia_prepare "Checking kernel cmdline has ${__not:+NOT }\"$match_regex\""
	grep -Eq "(^|\ )$match_regex(\ |$)" /proc/cmdline
}

# _cukinia_test: wrapper to test(1)
# arg1..n: arguments to test(1)
_cukinia_test()
{
	local desc="Running \"test $*\" ${__not:+NOT }returns success"

	case "$1" in
	-f) desc="Checking file \"$2\" ${__not:+NOT }exists" ;;
	-s) desc="Checking file \"$2\" is ${__not:+NOT }non-empty" ;;
	-d) desc="Checking directory \"$2\" ${__not:+NOT }exists" ;;
	-b) desc="Checking \"$2\" ${__not:+NOT }exists and is block special" ;;
	-c) desc="Checking \"$2\" ${__not:+NOT }exists and is character special" ;;
	-L|-h) desc="Checking \"$2\" is ${__not:+NOT }a symlink" ;;
	esac

	_cukinia_prepare "$desc"
	test "$@"
}

# _cukinia_user: checks if user $1 exists
_cukinia_user()
{
	local user="$1"

	_cukinia_prepare "Checking user \"$user\" ${__not:+NOT }exists"
	grep -q "^$user:" /etc/passwd
}

# _cukinia_user_memberof: checks if user is in groups
# arg1: user
# arg2..n: groups
_cukinia_user_memberof()
{
	local username="$1"
	shift
	local groupnames="$@"
	local user_groups=$(groups "$username")

	_cukinia_prepare "Checking user \"$username\" is ${__not:+NOT }member of \"$groupnames\""

	for groupname in $groupnames; do
		if ! echo "$user_groups" | grep -w -q "$groupname"; then
			return 1
		fi
	done

	return 0
}

# _cukinia_group: checks if group $1 exists
_cukinia_group()
{
	local group="$1"

	_cukinia_prepare "Checking group \"$group\" ${__not:+NOT }exists"
	grep -q "^$group:" /etc/group
}

# _cukinia_kmod: checks if kernel module $1 is loaded
_cukinia_kmod()
{
	local kmod="$1"

	_cukinia_prepare "Checking kernel module \"$kmod\" is ${__not:+NOT }loaded"
	grep -q "^$kmod " /proc/modules
}

# _cukinia_kconf: checks if kernel config option $1 is set as $2
# arg: $1: kernel configuration symbol name (without the CONFIG_ prefix)
# arg: $2: the desired setting for $1 option (y, m, n)
_cukinia_kconf()
{
	local ikconfig="/proc/config.gz"
	local bootconfig="/boot/config-$(uname -r)"
	local kconf="$1"
	local kset="$2"
	local line=""

	if [ ! -e "$ikconfig" ] && [ ! -e "$bootconfig" ]; then
		_cukinia_prepare "cukinia_kconf: $ikconfig (CONFIG_IKCONFIG_PROC=y) or $bootconfig is ${__not:+NOT }required"
		return 1
	fi

	_cukinia_prepare "Checking kernel config \"$kconf\" ${__not:+NOT }set to \"$kset\""

	case "$kset" in
	y|m) line="CONFIG_$kconf=$kset" ;;
	n) line="# CONFIG_$kconf is not set" ;;
	*)
		_cukinia_prepare "cukinia_kconf: invalid 2nd argument (use y|m|n)"
		return 1
		;;
	esac

	if [ ! -e "$ikconfig" ]; then
		cat $bootconfig | grep -q "^$line"
	else
		zcat /proc/config.gz | grep -q "^$line"
	fi
}

# _cukinia_kversion: check if running kernel version is MAJ.MIN
# arg1: the kernel version (MAJ.MIN format, e.g. 5.14)
_cukinia_kversion()
{
	local kver="$1"
	_cukinia_prepare "Checking if kernel version is $kver"
	test "$(uname -r | cut -d. -f1,2)" = "$kver"
}

# cukinia_process_with_args: checks if process $pname with arguments runs
# Optional: check if the process exist for a given username $puser
# arg: $1: the process name
# arg: $2: [optional] the user name
_cukinia_process_with_args()
{
	local pname="$1"
	local puser="$2"

	_cukinia_prepare "Checking process \"$pname\" ${__not:+NOT }running as ${puser:-any user}"

	check_process "$pname" "1" "$puser"

	return $?
}

# cukinia_process: checks if process $pname runs
# Optional: check if the process exist for a given username $puser
# arg: $1: the process name
# arg: $2: [optional] the user name
_cukinia_process()
{
	local pname="$1"
	local puser="$2"

	_cukinia_prepare "Checking process \"$pname\" ${__not:+NOT }running as ${puser:-any user}"

	check_process "$pname" "0" "$puser"

	return $?
}

# cukinia_kthread: checks if kernel thread $pname runs
# arg: $1: the kernel thread name
_cukinia_kthread()
{
	local pname="$1"
	local procdir
	local pid

	_cukinia_prepare "Checking kernel thread \"$pname\" is ${__not:+NOT }running"

	# scan /proc to get thread name
        # as some version of pidof do not look for kernel thread, use pgrep with exact match
	for pid in $(pgrep -x "$pname"); do
		# check that it is a kernel thread
		local status_file="/proc/$pid/status"
		grep -q "Name" "$status_file" && ! grep -q "^VmSize" "$status_file" && return 0
	done

	return 1
}

# cukinia_http_request: test the request using wget and wait for a return.
# arg1: url
_cukinia_http_request()
{
	local url="$1"

	_cukinia_prepare "Checking http url \"$url\" does ${__not:+NOT }return 200"
	wget -q -O /dev/null "$url"
}

# _cukinia_sysctl: check kernel parameters with sysctl
# args1: kernel parameter
# args2: test value
_cukinia_sysctl()
{
	local kparam="$1"
	local value="$2"

	_cukinia_prepare "Checking sysctl \"$kparam\" is ${__not:+NOT }set to \"$value\""
	test "$(sysctl -n "$kparam")" = "$value"
}

# _cukinia_symlink: validate the target of symlink
# args1: link
# args2: target
_cukinia_symlink()
{
	local link="$1"
	local target="$2"

	_cukinia_prepare "Checking link \"$link\" does ${__not:+NOT }point to \"$target\""

	# fail on non-symlinks
	[ -L "$link" ] || return 1

	[ "$(readlink -f "$link")" = "$(readlink -f "$target")" ] && return 0 || return 1
}

# _cukinia_systemd_active: Check for unit activity
# arg1: The systemd unit name
_cukinia_systemd_unit()
{
	local unit="$1"

	_cukinia_prepare "Checking if systemd unit \"$unit\" is active"
	systemctl is-active "$unit"
}

# _cukinia_systemd_failed: Fail on systemd unit failures
_cukinia_systemd_failed()
{
	_cukinia_prepare "Checking for no failed systemd units"
	if systemctl --failed | grep -qw "failed"; then
		return 1
	fi
	return 0
}

# _cukinia_listen4: check for a locally bound ipv4 port
# arg1: proto (tcp,udp,any)
# arg2: local port# (numeric)
_cukinia_listen4()
{
	local proto="$1"
	local port="$2"
	local opts="ln"
	local re

	case "$proto" in
	tcp)
		opts="${opts}t"
		re="tcp"
		;;
	udp)
		opts="${opts}u"
		re="udp"
		;;
	any)
		opts="${opts}tu"
		re="tcp|udp"
		;;
	esac

	_cukinia_prepare "Checking $re v4 port \"$port\" is ${__not:+NOT }open"

	netstat -$opts | awk '
		BEGIN {
		    ret = 1;
		}
		$1 ~ /^('$re')$/ && $4 ~ /:'$port'$/ {
		    ret = 0;
		}
		END {
		    exit ret;
	    }'
}

# _cukinia_mount: check for a specific mount point
# arg1: the device
# arg2: the mountpoint
# arg3: (optional) the filesystem type (eg. ext4), or "any"
# arg@: (optional) a list of options to look for
_cukinia_mount()
{
	local device="$1"
	local mountpoint="$2"
	local fstype="$3"
	local options
	local found=0
	local result

	if [ "$#" -gt 3 ]; then
		shift 3
		options="$*"
	else
		shift 2
	fi

	_cukinia_prepare "Checking $device on $mountpoint type ${fstype:-any} (${options:-any}) ${__not:+NOT} mounted"

	# use fstype as an awk regex if present
	if [ "$fstype" = "any" ] || [ -z "$fstype" ]; then
		fstype=".*"
	fi

	result=$(awk '
		$1 == "'$device'" && $2 == "'$mountpoint'" && $3 ~ /^'"$fstype"'$/ {
			print $4;
		}' /proc/mounts | sort | uniq)

	[ -n "$result" ] || return 1

	# ensure all expected mount options are set
	for term in $options; do
		if echo "$result" | grep -qw "$term"; then
			found=$((found + 1))
		fi
	done

	[ "$found" -eq "$#" ] || return 1
	return 0
}

# _cukinia_i2c: Check if i2c device exists
# Optional: Check of a device address
# Optional: Check the device uses the correct driver
# arg1: the bus number
# arg2: (optional) the device address
# arg3: (optional) the device driver name
_cukinia_i2c()
{
	local bus_number="$1"
	local device_address="$2"
	local driver_name="$3"
	local ret

	_cukinia_prepare "Checking if i2c-$bus_number${device_address:+-$device_address} exists${driver_name:+ and is $driver_name}"

	if [ ! -d /sys/bus/i2c/devices/i2c-"$bus_number" ]; then
		return 1
	fi

	if [ -z "$device_address" ]; then
		return 0
	fi

	if [ ! -x "$(which i2cdetect)" ]; then
		_cukinia_prepare "cukinia_i2c: i2cdetect is required"
		return 1
	fi

	device_address=${device_address#0x}
	i2cdetect -y "$bus_number" 0x"$device_address" 0x"$device_address" | cut -d ':' -f2 | grep -E "$device_address|UU"
	ret=$?
	if [ $ret -ne 0 ]; then
		return $ret
	fi

	if [ -z "$driver_name" ]; then
		return 0
	fi

	local device_path=$bus_number-$(printf '%0'4'd' $device_address)
	local link=/sys/bus/i2c/devices/$device_path/driver
	local target=/sys/bus/i2c/drivers/$driver_name

	# fail on non-symlinks
	[ -L "$link" ] || return 1

	[ "$(readlink -f "$link")" = "$(readlink -f "$target")" ] && return 0 || return 1
}

# _cukinia_gpio_libgpiod: Check gpio configuration via libgpiod
# arg1: (optional) the pins configured in input
# arg2: (optional) the pins configured in output high
# arg3: (optional) the pins configured in output low
# arg4: (optional) the gpiochip used (default:gpiochip0)
_cukinia_gpio_libgpiod()
{
	local input_pins
	local output_low_pins
	local output_high_pins
	local gpiochip="gpiochip0"
	local direction
	local value

	while [ $# -gt 0 ]; do
		case "$1" in
		-i)
			input_pins="$2"
			shift
			;;
		-l)
			output_low_pins="$2"
			shift
			;;
		-h)
			output_high_pins="$2"
			shift
			;;
		-g)
			gpiochip="$2"
			shift
			break
			;;
		esac
		shift
	done

	_cukinia_prepare "Checking if $gpiochip pins are well configured via libgpiod"
	if [ -n "$input_pins" ]; then
		for pin in $input_pins; do
			direction=$(gpioinfo "$gpiochip" | sed -nE "/line[[:space:]]+$pin:/s/.*[[:space:]](input|output)[[:space:]].*/\1/p")
			if [ "input" != "$direction" ]; then
				return 1
			fi
		done
	fi
	if [ -n "$output_low_pins" ]; then
		for pin in $output_low_pins; do
			direction=$(gpioinfo "$gpiochip" | sed -nE "/line[[:space:]]+$pin:/s/.*[[:space:]](input|output)[[:space:]].*/\1/p")
			value=$(gpioinfo "$gpiochip" | sed -nE "s/.*line[[:space:]]+$pin:.*(active-(low|high)).*/\1/p")
			if [ "output/active-low" != "$direction/$value" ]; then
				return 1
			fi
		done
	fi
	if [ -n "$output_high_pins" ]; then
		for pin in $output_high_pins; do
			direction=$(gpioinfo "$gpiochip" | sed -nE "/line[[:space:]]+$pin:/s/.*[[:space:]](input|output)[[:space:]].*/\1/p")
			value=$(gpioinfo "$gpiochip" | sed -nE "s/.*line[[:space:]]+$pin:.*(active-(low|high)).*/\1/p")
			if [ "output/active-high" != "$direction/$value" ]; then
				return 1
			fi
		done
	fi
	return 0
}

# _cukinia_gpio_sysfs: Check gpio configuration via sysfs
# arg1: (optional) the pins configured in input
# arg2: (optional) the pins configured in output high
# arg3: (optional) the pins configured in output low
# arg4: (optional) the gpiochip used (default:gpiochip0)
_cukinia_gpio_sysfs()
{
	local input_pins
	local output_low_pins
	local output_high_pins
	local gpiochip="gpiochip0"
	local direction
	local value

	while [ $# -gt 0 ]; do
		case "$1" in
		-i)
			input_pins="$2"
			shift
			;;
		-l)
			output_low_pins="$2"
			shift
			;;
		-h)
			output_high_pins="$2"
			shift
			;;
		-g)
			gpiochip="$2"
			shift
			break
			;;
		esac
		shift
	done

	_cukinia_prepare "Checking if $gpiochip pins are well configured via sysfs"
	if [ -n "$input_pins" ]; then
		for pin in $input_pins; do
			echo "$pin" > /sys/class/gpio/export
			direction=$(cat /sys/class/gpio/gpio"${pin}"/direction)
			echo "$pin" > /sys/class/gpio/unexport
			if [ "in" != "$direction" ]; then
				return 1
			fi
		done
	fi
	if [ -n "$output_low_pins" ]; then
		for pin in $output_low_pins; do
			echo "$pin" > /sys/class/gpio/export
			direction=$(cat /sys/class/gpio/gpio"${pin}"/direction)
			value=$(cat /sys/class/gpio/gpio"${pin}"/value)
			echo "$pin" > /sys/class/gpio/unexport
			if [ "out/0" != "$direction/$value" ]; then
				return 1
			fi
		done
	fi
	if [ -n "$output_high_pins" ]; then
		for pin in $output_high_pins; do
			echo "$pin" > /sys/class/gpio/export
			direction=$(cat /sys/class/gpio/gpio"${pin}"/direction)
			value=$(cat /sys/class/gpio/gpio"${pin}"/value)
			echo "$pin" > /sys/class/gpio/unexport
			if [ "out/1" != "$direction/$value" ]; then
				return 1
			fi
		done
	fi
	return 0
}

# _cukinia_knoerror: Check the kernel has booted without important errors
# arg1: the message priority
_cukinia_knoerror()
{
	local priority="$1"
	local ret
	local use_journalctl
	local p_loop=$priority
	local priority_dmesg=$priority

	if [ -x "$(which journalctl)" ]; then
		use_journalctl=1
	else
		use_journalctl=0
	fi

	_cukinia_prepare "Checking that kernel has booted with no error (priority: $priority)"

	if [ "$use_journalctl" -eq 1 ]; then
		journalctl --dmesg --priority "$priority" | grep "No entries"
		ret=$?
	else
		if dmesg --level "$priority"; then
			#dmesg is supporting --level option
			while [ "$p_loop" != 0 ]; do
				p_loop=$((p_loop-1))
				priority_dmesg=$priority_dmesg,$p_loop
			done
			if [ -z "$(dmesg --level "$priority_dmesg")" ]; then
				ret=0
			else
				ret=1
			fi
		else
			#dmesg is not supporting --level option
			_cukinia_prepare "Checking that kernel has booted with no error (using grep)"
			if [ -z "$(dmesg | grep -Ei 'error|fail|warning')" ]; then
				ret=0
			else
				ret=1
			fi

		fi
	fi
	return $ret
}

# _cukinia_netif_is_up: check if a network interface is up
# arg1: interface name
_cukinia_netif_is_up() {
	local interface="$1"
	_cukinia_prepare "Checking if interface \"$interface\" is ${__not:+NOT }up"
	[ -f "/sys/class/net/$interface/operstate" ] && [ "$(cat /sys/class/net/$interface/operstate)" = "up" ]
}

# _cukinia_netif_has_ip(): check if an interface has an IP address assigned
# arg1: interface name, eg "eth0"
# arg2: (optional) IP address family, "-6" for IPv6 (default: IPv4)
# arg3: (optional) extra interface flags to check, eg "dynamic" or "scope global"
_cukinia_netif_has_ip() {
	local interface="$1"
	local family_arg="$2"
	local extra_flags="$3"
	local regex

	if [ "$family_arg" = "-6" ]; then
		regex="inet6.[0-9a-f]"
	else
		family_arg="-4"
		regex="inet.[0-9]"
	fi

	if [ "$extra_flags" ]; then
		regex="$regex.*$extra_flags"
	fi

	_cukinia_prepare "Checking interface \"$interface\" has ${__not:+NO }ipv${family_arg##-} assigned ${extra_flags:+($extra_flags)}"
	ip -o $family_arg addr show dev "$interface" | grep -q "$regex"
}

# _cukinia_dns_resolve: check if a hostname can be resolved
# arg1: hostname
_cukinia_dns_resolve() {
	local hostname="$1"

	_cukinia_prepare "Checking if hostname \"$hostname\" can ${__not:+NOT }be resolved"

	host "$hostname" >/dev/null && return 0 || return 1
}

# Print elapsed time since cukinia startup (in seconds)
_time_elapsed()
{
	local now=$(date +%s)

	echo $((now - cukinia_epoch))
}

# Create junit-xml header
# arg1: path to output file
_junitxml_start()
{
	local outfile="$1"

	cat >$outfile <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
EOF
}

# Create junit-xml footer
# arg1: outfile
_junitxml_end()
{
	local outfile="$1"

	# close the current testsuite if needed
	if [ -n "$__suite_name" ]; then
		_junitxml_end_suite $outfile
	fi

	echo "</testsuites>" >>$outfile
}

# End the current junit-xml testsuite
# arg1: output file for the <testsuite> block
_junitxml_end_suite()
{
	local name="$__suite_name"
	local outfile="$1"
	local timestamp
	local failures
	local seconds
	local errors
	local tests
	local start
	local stop
	local ts

	if [ -z "$name" ]; then
		return 1
	fi

	# cleanup suite-specific variables
	timestamp=$__suite_timestamp
	failures=$__suite_failures
	skipped=$__suite_skipped
	errors=$__suite_errors
	tests=$__suite_tests
	unset __suite_timestamp
	unset __suite_failures
	unset __suite_skipped
	unset __suite_errors
	unset __suite_tests
	unset __suite_name

	hostname=$(hostname)

	# derive suite duration from the timestamp
	# note: busybox 1.27 'date' may choke on the iso8601 T format
	ts=$(echo $timestamp | sed -e 's/T/ /')
	start=$(date -d "$ts" +%s)
	stop=$(date +%s)
	seconds=$((stop - start))

	# append the whole testsuite block to the outfile
	cat >>$outfile <<EOF
  <testsuite name="$name" package="$name" errors="$errors" tests="$tests"
   id="$__suite_id" hostname="$hostname" failures="$failures"
   timestamp="$timestamp" skipped="$skipped" time="$seconds">
    <properties></properties>
EOF
	cat "$__logfile_tmp.$name" >>$outfile
	rm -f "$__logfile_tmp.$name"

	cat >>"$outfile" <<EOF
      <system-out><![CDATA[]]></system-out>
      <system-err><![CDATA[]]></system-err>
EOF

	echo "  </testsuite>" >>$outfile
	__suite_id=$((__suite_id + 1))
}

# Start a new <testsuite> section
# arg1: name of the test suite ([a-zA-Z0-9_]+)
_junitxml_add_suite()
{
	local name="$1"

	# strip the name from any fancy character
	name="$(echo $name | sed -e 's/[^a-zA-Z0-9_ ]//g')"

	if [ -z "$name" ]; then
		return 1
	fi

	# we should not add an already existing suite name
	if echo $__suite_list | grep -qw "$name"; then
		return 1
	fi

	# close the previous suite if adding a new one
	if [ -n "$__suite_name" ]; then
		_junitxml_end_suite $__logfile_tmp
	fi


	# append this test suite to our list
	__suite_list="$__suite_list $name"

	# suite properties
	__suite_name="$name"
	__suite_tests=0
	__suite_errors=0
	__suite_skipped=0
	__suite_failures=0
	__suite_timestamp=$(date +%Y-%m-%dT%H:%M:%S)
}

# Add a junit-xml testcase entry
# arg1: text description for that test
# arg2: test result (PASS or FAIL)
_junitxml_add_case()
{
	local message="$1"
	local result="$2"
	local logfile
	local varname
	local suite
	local name

	# ensure testcase runs in a default test suite
	if [ -z "$__suite_name" ]; then
		_junitxml_add_suite default # sets $__suite_name
	fi

	suite="$__suite_name"
	logfile="$__logfile_tmp.$suite"

	# escape all forbidden chars in xml {", ', <, >, &}
	name=$(echo $message | sed -e "
			 s/&/&amp;/g;
			 s/'/\&apos;/g;
			 s/\"/\&quot;/g;
			 s/</\&lt;/g;
			 s/>/\&gt;/g;")

	__suite_tests=$((__suite_tests + 1))

	cat >>"$logfile" <<EOF
    <testcase classname="${__log_class:-cukinia}" name="$name" time="0">
EOF

	if [ "$result" = "FAIL" ]; then
		__suite_failures=$((__suite_failures + 1))

		cat >>"$logfile" <<EOF
      <failure type="unit-test" message="failed"><![CDATA[$message: FAIL]]></failure>
EOF
	elif [ "$result" = "SKIP" ]; then
		__suite_skipped=$((__suite_skipped + 1))

		cat >>"$logfile" <<EOF
      <skipped message="skipped"><![CDATA[$message: SKIP]]></skipped>
EOF
	fi

	if [ -s "$__stderr_tmp" ]; then
		cat >>"$logfile" <<EOF
      <system-err><![CDATA[$(cat $__stderr_tmp | tr -dc [:print:])]]></system-err>
EOF
	> $__stderr_tmp
	fi

	if [ -s "$__stdout_tmp" ]; then
		cat >>"$logfile" <<EOF
      <system-out><![CDATA[$(cat $__stdout_tmp | tr -dc [:print:])]]></system-out>
EOF
	> $__stdout_tmp
	fi

	if [ -n "$__test_id" ]; then
		cat >>"$logfile" <<EOF
      <properties>
        <property name="cukinia.id" value="$__test_id"/>
      </properties>
EOF
	fi

	echo "    </testcase>" >>"$logfile"
}

# _colorize: return colorized string
# arg1: color name
# arg2..: string
_colorize()
{
	local color="$1"; shift
	local text="$*"
	local nc='\033[0m'
	local c

	if [ -n "$__log_format" ] || [ "$__log_file" != "/dev/stdout" ]; then
		printf "%s" "$text"
		return
	fi

	# Only colorize a few terminal types
	case "$TERM" in
	linux*|xterm*|screen*|tmux*|vt102) ;;
	*) echo "$@"; return ;;
	esac

	case "$color" in
	gray)   c='\033[1;30m' ;;
	red)    c='\033[1;31m' ;;
	green)  c='\033[1;32m' ;;
	yellow) c='\033[1;33m' ;;
	blue)   c='\033[1;34m' ;;
	purple) c='\033[1;35m' ;;
	cyan)   c='\033[1;36m' ;;

	red2)    c='\033[1;91m' ;;
	green2)  c='\033[1;92m' ;;
	yellow2) c='\033[1;93m' ;;
	blue2)   c='\033[1;94m' ;;
	purple2) c='\033[1;95m' ;;
	cyan2)   c='\033[1;96m' ;;
	white2)  c='\033[1;97m' ;;
	esac

	printf "${c}${text}${nc}"
}

# _cukinia_run_dir: run all tests file in $1
_cukinia_run_dir()
{
	local dir="$1"
	local ret=0

	for f in "$dir"/*; do
		[ -f "$f" ] || continue
		_cukinia_prepare "External: $f"
		cukinia_runner "$f" || ret=1
	done

	return $ret
}

# version to int utility
# useful for version checks e.g. using cukinia_test
# arg1: version string, max 4 x 4 digits (eg. "4.19.135")
_ver2int()
{
	echo "$1" | awk '{
		split($1, A, ".");
		result = A[1] * 10^12 + A[2] * 10^8 + A[3] * 10^4 + A[4];
		printf("%.0f\n", result);
	}'
}

# Template generic cukinia_* functions
for func in $GENERIC_FUNCTS; do
	eval "$(cat <<EOF
cukinia_$func()
{
	cukinia_runner _cukinia_$func "\$@"
}
EOF
)"
done

cukinia_tests=0
cukinia_failures=0
cukinia_skip=0
cukinia_conf_include "$CUKINIA_CONF"

# Finish logging
case "$__log_format" in
junitxml)
	__junitfile_tmp=$(mktemp)
	_junitxml_start "$__junitfile_tmp"
	cat "$__logfile_tmp" >>"$__junitfile_tmp"
	_junitxml_end "$__junitfile_tmp"
	cat "$__junitfile_tmp" >"$__log_file"
	;;
csv)
	cat "$__logfile_tmp" >"$__log_file"
	;;
esac

[ $cukinia_failures -eq 0 ] && exit 0 || exit 1
