#! /bin/bash
#
# Author: SpinetiX S.A.
# Copyright: 2011 SpinetiX S.A.
#

# The default temporary directory
TMPDIR=/var/tmp
export TMPDIR

# The directory with the data files
DATADIR=/usr/share/spxtest

# The start and end splash screen, should be 640x480
STARTSPLASH="$DATADIR"/start-splash.png
ENDSPLASH="$DATADIR"/end-splash.png

# Audio test file
AUDIOTEST="$DATADIR"/drums.mp3

# Video output test patterns
VIDEOTEST_640x480="$DATADIR"/cbars-640x480.png
VIDEOTEST_1024x768="$DATADIR"/cbars-1024x768.png
VIDEOTEST_1280x720="$DATADIR"/cbars-1280x720.png
VIDEOTEST_1920x1080="$DATADIR"/cbars-1920x1080.png

# The number of iterations for the CPU test
CPUTEST_ITERS=250

# The video test time for each pattern (seconds)
VIDEOTEST_TIME=5

# The video mode DB
VIDMODEDB=/usr/share/vidmode/vid.modes

# The video output sysfs interface files
SYSFSVIDDEV=/sys/class/x-display/disp0/device
SYSFSVIDMODE="$SYSFSVIDDEV"/vid_mode
SYSFSVIDOUT0PM="$SYSFSVIDDEV"/out0/power_mode
SYSFSVIDOUT1PM="$SYSFSVIDDEV"/out1/power_mode

# The LED control file
LEDFILE=/sys/devices/system/leds/leds0/event

# The audio output device
AUDIODEV=/dev/dsp

# USB devices info file
USBDEVINFO=/proc/bus/usb/devices

# USB disk device node
USBDISKDEV=/dev/sda

# The storage mount point
STMPT=/var/tmp

# The list of tests (array), also specifies run order
TEST_LIST=( 'leds' 'audio' 'video' 'serial' 'cpu' 'ddc_vga' 'ddc_hdmi' \
    'eeprom' 'sys_images' 'rtc' 'rtc_batt' 'sensors' 'button' 'storage' \
    'usb' )

# The labels for each of the tests above (array), order must match
TEST_LABELS=( 'LEDS' 'AUDIO' 'VIDEO' 'SERIAL' 'CPU' 'VGA-DDC' 'HDMI-DDC' \
    'EEPROM' 'SYSTEM-IMAGES' 'RTC' 'RTC-BATT' 'SENSORS' 'BUTTON' 'STORAGE' \
    'USB' )

# For each test we can specify a help text as TEST_HELP_$test.
# If the help text is hardware type dependent it should be named
# TEST_HELP_$test_$HWTYPE instead.

TEST_HELP_leds_Bonsai=\
'Requires the operator to check that the LED suspends normal operation and
remains off for 2 seconds, then it cycles 3 times as green for 1 second
followed by red for 1 second, then remains off for 2 second and resumes normal
operation.'

TEST_HELP_leds_Sakura=\
'Requires the operator to check that the system and network LEDs suspend
normal operation and remain off for 2 seconds, then it cycles 3 times as
green for 1 second followed by red for 1 second, then remains off for 2 seconds
and resume normal operation. The power LED is not tested.'

TEST_HELP_audio=\
'Requires the operator to listen to the audio output on the analog analog audio
line-out jack and attached HDMI display.'

TEST_HELP_video=\
'Requires the operator to visually check the image output on attached
displays.'

TEST_HELP_serial=\
'The unit should be connected to an echo service;
the echo service can be a terminal emulator where the user manually copies
and pastes the test string sent by the unit or an automated program.
Serial port settings are 115200 baud, 8 data bits, no parity, 1 stop bit and
no flow control. The timeout is 15 seconds.'

TEST_HELP_button=\
'Requires the operator to briefly press the push button on
the unit after the test starts (i.e. after the splash screen is
displayed and/or the audio is played).'

TEST_HELP_usb=\
'A USB memory stick should be plugged directly into the unit, at least
1 minute before starting the tests to allow for it to be fully
detected. If the unit is working properly the contents on the USB
memory stick will not be modified. The USB memory stick should be
High-Speed (i.e. USB 2.0), 16 MB or larger; no specific formatting is
required.'

# For each test we can specify the external checks that are to be
# performed as TEST_CHECK_NAME_$test, TEST_CHECK_LABEL_$test and
# TEST_CHECK_DESC_$test arrays; if no array is defined or the array
# is empty then no external checks are necessary for the test. For each
# test the 3 arrays must have the same number of elements.

TEST_CHECK_NAME_leds=( 'leds' )
TEST_CHECK_LABEL_leds=( 'LEDs' )
TEST_CHECK_DESC_leds=( 'LEDs toggled as expected.' )

TEST_CHECK_NAME_audio=( 'analog-audio' 'hdmi-audio' )
TEST_CHECK_LABEL_audio=( 'Line out audio' 'HDMI audio' )
TEST_CHECK_DESC_audio=( 'Analog audio (line out jack) properly output.' \
    'Audio on HDMI display properly output.' )

TEST_CHECK_NAME_video=( 'video-vga' 'video-hdmi' )
TEST_CHECK_LABEL_video=( 'VGA' 'HDMI' )
TEST_CHECK_DESC_video=( \
    'VGA test patterns properly shown on VGA (VESA) display.' \
    'HDMI test patterns properly shown on HDMI / DVI display.' )

#
# Hardware dependent variables
#

# The hardware type (initialized by init)
HWTYPE=

# The saved video settings to restore at end
SAVED_VIDMODE=
SAVED_OUT0PM=
SAVED_OUT1PM=

# The video device node
VIDDEV=

# The OSD device node, if available
OSDDEV=

# Window control sysfs file, if available
WINCTL=

# The file from which push button state can be read
BTNFILE=

# The EEPROMs access dirs, empty if none
EEPROM1DIR=
EEPROM2DIR=

# The hardware monitor dirs, empty if none
HWMON0DIR=

# The main storage device
STDEV=

# The serial port device
TTYSDEV=

# The type of MTD, nor or nand
MTDTYPE=

# The type of failsafe (recovery) resources filesystem
FFSTYPE=

#
# State variables
#

# The initial button count
INIT_BTNCNT=

# The list of tests to run (array), if empty all are run
unset RUN_TEST

# The lists of tests to skip (array)
unset SKIP_TEST

# If set to 'yes' the player is stopped at start and restarted at end
STOP_PLAYER=

#
# Functions
#

# Logs a message to syslog and stdout, the arguments are the message
# If the first argument is -n a newline is prepended and suffixed to stdout
function log() {
    local nl=
    if [ "$1" = "-n" ]; then
	nl=1
	shift
    fi
    # get input from /dev/null to prevent logger from reading from stdin
    # and blocking if there are not arguments
    logger -t "spxtest[$$]" -- "$@" < /dev/null
    [ -n "$nl" ] && echo
    echo "$@"
    [ -n "$nl" ] && echo
}

# Reads the button count to stdout
function read_button_count() {
    awk -F': ' '$1 == 0 {print $2}' "$BTNFILE"
    if [ $? -ne 0 ]; then
	log "ERROR: failed reading button status file" >&2
	return 1
    fi
    return 0
}

# Initializes global hardware dependent variables
function init_hwvars() {
    local tag sep val
    local hw

    while read tag sep val; do
	if [ "$tag" = "Hardware" ]; then
	    hw="$val"
	    break
	fi
    done < /proc/cpuinfo
    
    if [ -z "$hw" ]; then
	log "ERROR: could not find hardware type" >&2
	return 1
    fi

    HWTYPE="$hw"
    case "$HWTYPE" in
	"Bonsai")
	    WINCTL=/sys/class/graphics/fb0/device/win_ctl
	    OSDDEV=/dev/fb/0
	    VIDDEV=/dev/fb/1
	    BTNFILE=/proc/driver/bonsai_button
	    EEPROM1DIR=/sys/bus/i2c/devices/0-0050
	    [ -e "$EEPROM1DIR" ] || EEPROM1DIR=/sys/bus/i2c/devices/0-0052
	    EEPROM2DIR=/sys/bus/i2c/devices/0-0053
	    HWMON0DIR=
	    STDEV=/dev/mmcblk0p2
	    TTYSDEV=/dev/ttyS0
	    MTDTYPE=nor
	    FFSTYPE=cramfs
	    ;;
	"Sakura")
	    VIDDEV=/dev/video2
	    BTNFILE=/proc/driver/sakura_button
	    EEPROM1DIR=/sys/bus/i2c/devices/0-0052
	    EEPROM2DIR=
	    HWMON0DIR=/sys/class/hwmon/hwmon0/device
	    STDEV=/dev/hda1
	    TTYSDEV=/dev/ttyS1
	    MTDTYPE=nand
	    FFSTYPE=jffs2
	    ;;
	*)
	    log "ERROR: unknown hardware type '$HWTYPE'" >&2
	    return 1
	    ;;
    esac

    return 0
}

# Initializes the hardware variables and global state variables
function init() {
    local ret

    init_hwvars || return

    # Save the initial push button press count
    # failure to read the button count is not a fatal error
    INIT_BTNCNT="$(read_button_count)"
    ret=$?
    [ $ret -eq 0 ] || INIT_BTNCNT=

    return 0
}

# Stops the player component to free up video and audio output
function stop_player() {
    local ret=0

    log "INFO: stopping player"
    /etc/init.d/raperca-watchdog stop || ret=1
    sleep 5
    /etc/init.d/spxtest-watchdog start || ret=1
    /etc/init.d/raperca stop || ret=1
    [ $ret -eq 0 ] || log "ERROR: failed to properly stop player" >&2
    return $ret
}

# Starts the player component
function start_player() {
    local ret=0

    log "INFO: starting player"
    /etc/init.d/raperca start || ret=1
    /etc/init.d/spxtest-watchdog stop || ret=1
    sleep 5
    /etc/init.d/raperca-watchdog start || ret=1
    [ $ret -eq 0 ] || log "ERROR: failed to properly start player" >&2
    return $ret
}

# Loads the specified image as splash screen
function show_splash() {
    local splash="$1"
    local ret

    case "$HWTYPE" in
	"Bonsai")
	    fbtest "$OSDDEV" -qq -l "$splash" && \
		echo osd0 on > "$WINCTL" && echo osd0 center > "$WINCTL" && \
		echo vid0 off > "$WINCTL"
	    ret=$?
	    ;;
	"Sakura")
	    splashd -D "$VIDDEV" -n "$splash"
	    ret=$?
	    ;;
	*)
	    log "ERROR: unknown hardware type '$HWTYPE'" >&2
	    return 1
    esac

    [ $ret -eq 0 ] || \
	log "ERROR: failed loading splash screen '$splash'" >&2
    return $ret
}

# Initializes video settings for test
function video_init() {
    local ret=0

    # Save current settings and force video output power to on
    SAVED_VIDMODE="$(< "$SYSFSVIDMODE")" || ret=1
    if [ -f "$SYSFSVIDOUT0PM" ]; then
	SAVED_OUT0PM="$(< "$SYSFSVIDOUT0PM")" || ret=1
	echo 1 > "$SYSFSVIDOUT0PM" || ret=1
    fi
    if [ -f "$SYSFSVIDOUT1PM" ]; then
	SAVED_OUT1PM="$(< "$SYSFSVIDOUT1PM")" || ret=1
	echo 1 > "$SYSFSVIDOUT1PM" || ret=1
    fi

    [ $ret -eq 0 ] || \
	log "ERROR: failed initializing video output settings" >&2
    return $ret
}

# Restores all saved video settings
function video_restore() {
    local ret=0

    # Restore the video settings
    echo "$SAVED_VIDMODE" > "$SYSFSVIDMODE" || ret=1
    case "$HWTYPE" in
	"Bonsai")
	    echo osd0 center > "$WINCTL" || ret=1
	    fbtest "$VIDDEV" -qq -scr -r 0 0 || ret=1
	    echo vid0 off > "$WINCTL" || ret=1
	    ;;
	"Sakura")
	    splashd -D "$VIDDEV" -n -r || ret=1
	    ;;
	*)
	    log "ERROR: unknown hardware type '$HWTYPE'" >&2
	    ret=1
    esac
    if [ -f "$SYSFSVIDOUT0PM" ]; then
	echo "$SAVED_OUT0PM" >  "$SYSFSVIDOUT0PM" || ret=1
    fi
    if [ -f "$SYSFSVIDOUT1PM" ]; then
	echo "$SAVED_OUT1PM" >  "$SYSFSVIDOUT1PM" || ret=1
    fi

    [ $ret -eq 0 ] || \
	log "ERROR: failed restoring video output settings" >&2
    return $ret
}

# Find MTD number by partition name, number output in stdout
function find_mtd() {
    local name="$1"
    local mtdnr

    mtdnr="$(sed -ne '/^mtd[0-9]\+:.*"'"$name"'"$/{s,^mtd\([0-9]\+\):.*,\1,;p}' /proc/mtd)"
    [ $? -ne 0 ] && return 1
    [ -z "$mtdnr" ] && return 1
    echo "$mtdnr"
}

# Runs mkimage -l from a device via a temporary file
function mkimagedev()
{
    local tmp
    local ret
    tmp="$(mktemp -t mtd.XXXXXXXX)"
    if [ $? -ne 0 ]; then
	return 1
    fi
    if [ "$MTDTYPE" = "nand" ]; then
	nanddump -b -o "$1" > "$tmp"
    elif [ "$MTDTYPE" = "nor" ]; then
	cp "$1" "$tmp"
    else
	log "ERROR: invalid MTD type" >&2
	return 1
    fi
    if [ $? -ne 0 ]; then
	rm -f "$tmp"
	return 1
    fi
    mkimage -l "$tmp"
    ret=$?
    rm -f "$tmp"
    return $ret
}

#
# AUDIO
#

# Tests the audio by setting the output
function test_audio() {
    if [ ! -c "$AUDIODEV" ]; then
	log "ERROR: no sound device node found" >&2
	return 1
    fi
    ffmpeg -v -1 -xerror -i "$AUDIOTEST" -f oss "$AUDIODEV"
    if [ $? -ne 0 ]; then
	log "ERROR: failed to output audio" >&2
	return 1
    fi
    return 0
}

#
# VIDEO
#

# Sets the video timing passed in the first argument
function set_vidtiming() {
    local vidtiming="$1"

    # On Bonsai we need to disable the video window before video mode change;
    # leaving the OSD window enabled is OK
    if [ "$HWTYPE" = "Bonsai" ]; then
	echo vid0 off > "$WINCTL" && fbtest "$VIDDEV" -qq -scr -r 0 0
	if [ $? -ne 0 ]; then
	    log "ERROR: failed disabling video window prior to " \
		"video mode change" >&2
	    return 1
	fi
    fi

    # Apply the video timing
    echo "$vidtiming" > "$SYSFSVIDMODE"
    if [ $? -ne 0 ]; then
	log "ERROR: failed to set video timing '$vidtiming'" >&2
	return 1
    fi

    # On Bonsai, recenter the OSD window just in case it is visible
    if [ "$HWTYPE" = "Bonsai" ]; then
	echo osd0 center > "$WINCTL"
	if [ $? -ne 0 ]; then
	    log "ERROR: failed to re-center OSD window" >&2
	    return 1
	fi
    fi

    return 0
}

# Sets the corresponding video mode and loads the appropriate video
# test pattern. Arguments: video resolution (e.g., "640x480")
function load_video_test() {
    local res="$1"
    local testimg vidmode
    local vidtiming
    local w h
    local ret

    # Select pattern and corresponding video mode
    case "$res" in
	"640x480")
	    testimg="$VIDEOTEST_640x480"
	    vidmode="640x480@60p-4:3"
	    w=640
	    h=480
	    ;;
	"1024x768")
	    testimg="$VIDEOTEST_1024x768"
	    vidmode="1024x768@60p-4:3"
	    w=1024
	    h=768
	    ;;
	"1280x720")
	    testimg="$VIDEOTEST_1280x720"
	    vidmode="1280x720@60p-16:9"
	    w=1280
	    h=720
	    ;;
	"1920x1080")
	    testimg="$VIDEOTEST_1920x1080"
	    vidmode="1920x1080@60p-16:9"
	    w=1920
	    h=1080
	    ;;
	*)
	    log "ERROR: unknown resolution '$res'" >&2
	    return 1
    esac
    vidtiming="$(grep -m 1 -- "^[[:space:]]*$vidmode[[:space:]]" "$VIDMODEDB")"
    if [ $? -ne 0 ]; then
	log "ERROR: did not find '$vidmode' video timing definition" >&2
	return 1
    fi

    # Apply the video timing
    set_vidtiming "$vidtiming" || return $?

    # Show the pattern
    case "$HWTYPE" in
	"Bonsai")
	    echo osd0 off > "$WINCTL" && echo vid0 on > "$WINCTL" && \
		fbtest "$VIDDEV" -qq -r "$w" "$h" -l "$testimg"
	    ret=$?
	    ;;
	"Sakura")
	    splashd -D "$VIDDEV" -n -r "$testimg"
	    ret=$?
	    ;;
	*)
	    log "ERROR: unknown hardware type '$HWTYPE'" >&2
	    return 1
    esac

    if [ $ret -ne 0 ]; then
	log "ERROR: failed displaying video test pattern" >&2
    fi
    return $ret
}

# Performs the video tests
function test_video()
{
    local ret
    local resolutions res

    case "$HWTYPE" in
	"Bonsai")
	    resolutions=("640x480" "1024x768" "1280x720")
	    ;;
	"Sakura")
	    resolutions=("640x480" "1920x1080")
	    ;;
	*)
	    log "ERROR: unknown hardware type '$HWTYPE'" >&2
	    return 1
    esac

    ret=0
    for res in "${resolutions[@]}"; do
	log "INFO: testing ${res}@60"
	load_video_test "$res"
	if [ $? -ne 0 ]; then
	    log "ERROR: failed $res video output" >&2
	    ret=1
	else
	    sleep "$VIDEOTEST_TIME"
	fi
    done

    # Restore the splash screen
    show_splash "$STARTSPLASH" || ret=$?

    return "$ret"
}

#
# DDC
#

function test_ddc_vga() {
    local ddcbus
    local ret

    case "$HWTYPE" in
	"Bonsai")
	    ddcbus=0
	    if [ -e /sys/bus/i2c/devices/"$ddcbus"-0050 ]; then
		# DDC address claimed by another device => no VGA DDC
		log "INFO: no VGA DDC support on this hardware, skip" >&2
		return 0
	    fi
	    ;;
	"Sakura")
	    ddcbus=1
	    ;;
	*)
	    log "ERROR: unknown hardware type '$HWTYPE'" >&2
	    return 1
    esac

    ddc-i2c -b /dev/i2c/"$ddcbus" -a 0x50
    ret=$?
    [ $ret -eq 0 ] || log "ERROR: failed reading EDID via DDC" >&2
    return $ret
}

function test_ddc_hdmi() {
    local ret

    ddc-i2c -a 0x3f
    ret=$?
    [ $ret -eq 0 ] || log "ERROR: failed reading EDID via DDC" >&2
    return $ret
}

#
# RTC
#

function test_rtc() {
    local rtcdate

    rtcdate="$(hwclock --utc --show --noadjfile)"
    if [ $? -ne 0 ]; then
	log "ERROR: failed reading from RTC clock" >&2
	return 1
    fi
    log "INFO: RTC date: $rtcdate"
    log "INFO: system date:" "$(date +"%c [%Z / UTC%z]")"
    return 0
}

function test_rtc_batt() {
    local vl

    vl="$(< /sys/bus/platform/devices/bonsairtc.0/voltage_low)"
    if [ $? -ne 0 ]; then
	log "ERROR: failed reading RTC battery voltage low indicator"
	return 1
    fi
    if [ "$vl" != "0" ]; then
	log "ERROR: RTC battery discharged or missing"
	return 1
    fi
    return 0
}

#
# Hardware sensors
#

function test_sensors() {
    local name
    local temp

    if [ -z "$HWMON0DIR" ]; then
	log "INFO: no sensors to test on this hardware type"
	return 0
    fi

    # What follows is specific to Sakura, other supported platforms do not
    # have any sensors available.

    if [ "$HWTYPE" != "Sakura" ]; then
	log "ERROR: sensor on unknown hardware type (internal error)" >&2
	return 1
    fi
    if [ ! -d "$HWMON0DIR" ]; then
	log 'ERROR: hwmon0 interface missing, sensor not detected?' >&2
	return 1
    fi
    if [ ! -f "$HWMON0DIR"/name ]; then
	log "ERROR: hwmon0 device name missing" >&2
	return 1
    fi
    name="$(< "$HWMON0DIR"/name)"
    if [ "$name" != "tmp431" ]; then
	log "ERROR: hwmon0 has incorrect name ('$name' instead of 'tmp431')" >&2
	return 1
    fi
    temp="$(< "$HWMON0DIR"/temp1_input)"
    if [ $? -ne 0 ] || [ -z "$temp" ]; then
	log "ERROR: failed reading temperature from hwmon0 sensor" >&2
	return 1
    fi
    # NOTE: temperature is in milli-degrees Celcius
    if ! [ "$temp" -ge -10000 -a "$temp" -le 90000 ]; then
	log "ERROR: read bogus temperature '$temp' from hwmon0 sensor" >&2
    fi
    return 0
}

#
# Push button
#

# Test for push button clicks
function test_button() {
    local btncnt
    local i=0

    if [ -z "$INIT_BTNCNT" ]; then
	log "ERROR: no initial button press count"
	return 1
    fi
    while [[ i++ -lt 50 ]]; do
	btncnt="$(read_button_count)" || return 1
	[ "$btncnt" -gt "$INIT_BTNCNT" ] && return 0
	usleep 100000
    done
    log "ERROR: timeout waiting for button press" >&2
    return 1
}

#
# LED
#

# Test the LEDs. The system LED is set to off for 2 seconds, then
# 3 cycles of green for 1 sec and red for 1 sec, then off
# for 2 seconds and the released. For Sakura the network LED follows
# the same pattern, except that off is used instead of green.
function test_leds() {
    local led="$1"
    local ret=0
    local i

    echo claim > "$LEDFILE" || ret=1
    echo green off > "$LEDFILE" || ret=1
    echo red off > "$LEDFILE" || ret=1
    if [ "$HWTYPE" == "Sakura" ]; then
	echo blue off > "$LEDFILE" || ret=1
	echo amber off > "$LEDFILE" || ret=1
    fi
    sleep 2
    for (( i=0; i < 3; i++ )); do
	echo green on > "$LEDFILE" || ret=1
	if [ "$HWTYPE" == "Sakura" ]; then
	    echo blue on > "$LEDFILE" || ret=1
	fi
	sleep 1
	echo green off > "$LEDFILE" || ret=1
	echo red on > "$LEDFILE" || ret=1
	if [ "$HWTYPE" == "Sakura" ]; then
	    echo blue off > "$LEDFILE" || ret=1
	    echo amber on > "$LEDFILE" || ret=1
	fi
	sleep 1
	echo red off > "$LEDFILE" || ret=1
	if [ "$HWTYPE" == "Sakura" ]; then
	    echo amber off > "$LEDFILE" || ret=1
	fi
    done
    sleep 2
    echo release > "$LEDFILE" || ret=1

    return $ret
}

#
# USB
#

function test_usb() {
    local nrhubs nrdevs nrmem
    local i=0

    nrhubs="$(grep '^D:.* Cls=09(' "$USBDEVINFO" | wc -l)"
    nrdevs="$(grep '^D:.* Cls=\(0[^9]\|[^0].\)(' "$USBDEVINFO" | wc -l)"

    if ! [ "$nrdevs" -ge 1 ]; then
	if [ "$nrhubs" -gt 1 ]; then
	    log "ERROR: no USB devices connected, only hubs" >&2
	else
	    log "ERROR: no USB devices connected" >&2
	fi
	return 1
    fi

    nrmem="$(grep '^I:.* Cls=08(' "$USBDEVINFO" | wc -l)"
    if ! [ "$nrmem" -ge 1 ]; then
	log "ERROR: no USB mass storage device found" >&2
	return 1
    fi

    # Wait a bit for device to appear
    while [[ ++i -lt 5 ]]; do
	[ -b "$USBDISKDEV" ] && break
	sleep 1
    done
    if [ ! -b "$USBDISKDEV" ]; then
	log "ERROR: no device node for USB mass storage device (CD-ROM?)" >&2
	return 1
    fi

    # Now read from device to test it, but first clear all cached buffers
    blockdev --flushbufs "$USBDISKDEV"
    if [ $? -ne 0 ]; then
	log "ERROR: failed to flush USB disk buffers" >&2
	return 1
    fi
    dd bs=4k count=1024 if="$USBDISKDEV" of=/dev/null
    if [ $? -ne 0 ]; then
	log "ERROR: failed reading from USB disk" >&2
	return 1
    fi

    return 0
}

#
# Mass storage (SD / CF card)
#

# For write size get MemTotal from /proc/meminfo and add 20%
function test_storage() {
    local dev size
    local freeblocks blocksize freesize minsize
    local file bsize

    if [ ! -b "$STDEV" ]; then
	log "ERROR: main storage device not present" >&2
	return 1
    fi

    dev="$(awk '$2=="'"$STMPT"'" {print $1}' /proc/mounts)"
    if [ $? -ne 0 ]; then
	log "ERROR: failed checking storage mount point" >&2
	return 1
    fi
    if [ "$dev" != "$STDEV" ]; then
	log "ERROR: storage mounted from incorrect device '$dev'" >&2
	return 1
    fi

    # Use a write size of 120% of available RAM so that we are
    # sure to not read-back from cache
    size="$(awk '/^MemTotal:/ { print int($2*1.2) }' < /proc/meminfo)"
    if [ $? -ne 0 ]; then
	log "ERROR: failed to compute storage test size" >&2
	return 1
    fi
    if ! [ $size -ge 16384 ]; then
	log "ERROR: invalid or too small memory size '$size'" >&2
	return 1
    fi

    # Test there is enough free size
    freeblocks="$(stat -f -c "%a" "$STMPT")"
    if [ $? -ne 0 ]; then
	log "ERROR: failed to get storage's free size" >&2
	return 1
    fi
    blocksize="$(stat -f -c "%s" "$STMPT")"
    if [ $? -ne 0 ]; then
	log "ERROR: failed to get storage's block size" >&2
	return 1
    fi
    if ! [ "$freeblocks" -ge 1 ]; then
	log "ERROR: invalid value of free block count '$freeblocks'" >&2
	return 1
    fi
    if ! [ "$blocksize" -ge 1024 ]; then
	log "ERROR: invalid or too small blocksize '$blocksize'" >&2
	return 1
    fi
    (( freesize = freeblocks * ( blocksize / 1024 ) ))
    (( minsize = 2 * size ))
    if ! [ "$freesize" -ge "$minsize" ]; then
	log "ERROR: not enough free space to test storage" >&2
	return 1
    fi

    # Now test
    file="$(mktemp "$STMPT"/spxtest.XXXXXX)"
    if [ $? -ne 0 ]; then
	log "ERROR: failed to make temporary test file" >&2
	rm -f -- "$file"
	return 1
    fi
    (( bsize = 1024 * size ))
    chkwrite w "$file" "$bsize"
    if [ $? -ne 0 ]; then
	log "ERROR: failed writing phase" >&2
	rm -f -- "$file"
	return 1
    fi
    chkwrite c "$file" "$bsize"
    if [ $? -ne 0 ]; then
	log "ERROR: failed read/check phase" >&2
	rm -f -- "$file"
	return 1
    fi
    rm -f "$file"
    if [ $? -ne 0 ]; then
	log "ERROR: failed removing up temporary test file" >&2
	return 1
    fi
    return 0
}

#
# Serial port
#

function test_serial() {
    local ret

    serialtest "$TTYSDEV"
    ret=$?
    if [ $ret -eq 2 ]; then
	log "ERROR: the device is in use" >&2
    fi
    return $ret
}

#
# EEPROM
#

# Verifies the given EEPROM
function verify_eeprom() {
    local datafile="$1"
    local sig

    if [ ! -f "$datafile" ]; then
	log "ERROR: EEPROM data interface missing" >&2
	return 1
    fi
    cat "$datafile" > /dev/null
    if [ $? -ne 0 ]; then
	log "ERROR: failed reading from EEPROM" >&2
	return 1
    fi
    sig="$(dd bs=3 count=1 if="$datafile")"
    if [ "$sig" != "SPX" ]; then
	log "ERROR: invalid signature '$sig' in EEPROM" >&2
	return 1
    fi
    return 0
}

# Tests all EEPROMs available
function test_eeprom() {
    local ret=0

    if [ -n "$EEPROM1DIR" ]; then
	log "INFO: testing" "$(< "$EEPROM1DIR"/subname)" "EEPROM"
	verify_eeprom "$EEPROM1DIR"/data
	[ $? -eq 0 ] || ret=1
    fi
    if [ -n "$EEPROM2DIR" ]; then
	log "INFO: testing" "$(< "$EEPROM2DIR"/subname)" "EEPROM"
	verify_eeprom "$EEPROM2DIR"/data
	[ $? -eq 0 ] || ret=1
    fi
    return $ret
}

#
# CPU
#

function test_cpu() {
    # The test is run in DSP and is provided by raperca, so we need
    # to load its kernel modules first
    /etc/raperca/loadmodules.sh && cputest -n "$CPUTEST_ITERS"
}

#
# System images
#

function test_sys_images() {
    local ret=0
    local mtdnr dev
    local pnames pn
    local fname hasfres
    local out
    local mpt

    case "$HWTYPE" in
	"Bonsai")
	    pnames=("kernel" "failsafe-kernel" "failsafe-fs")
	    # need to check $fname for version to detect if
	    # failsafe resources should be present
	    fname="failsafe-fs"
	    hasfres=
	    ;;
	"Sakura")
	    pnames=("kernel" "failsafe-img")
	    # all failsafe versions use resources, no need to check
	    fname=
	    hasfres=yes
	    ;;
	*)
	    log "ERROR: unknown hardware type '$HWTYPE'" >&2
	    return 1
    esac

    # check each image
    for pn in "${pnames[@]}"; do
	log "INFO: checking image '$pn'"
	mtdnr="$(find_mtd "$pn")"
	if [ $? -ne 0 ]; then
	    log "ERROR: could not find flash partition '$pn'" >&2
	    ret=1
	    continue
	fi
	dev=/dev/mtd"$mtdnr"ro
	if [ ! -c "$dev" ]; then
	    log "ERROR: read-only character device missing for '$pn'" >&2
	    ret=1
	    continue
	fi
	out="$(mkimagedev "$dev")"
	if [ $? -ne 0 ]; then
	    echo "$out"
	    log "ERROR: invalid image '$pn'" >&2
	    ret=1
	    continue
	fi
	echo "$out"
	if [ "$pn" = "$fname" ]; then
	    # failsafe (a.k.a. recovery) resources are used starting with 1.1
	    echo "$out" | \
		grep -qi '^image name:.*initrd[[:space:]]\+\([0-9][0-9]\|[2-9]\.\|1\.[1-9]\)' && \
		hasfres=yes
	fi
    done

    # check the failsafe (a.k.a. recovery) resources if they should be present
    if [ "$hasfres" != "yes" ]; then
	return $ret
    fi

    pn="failsafe-res"
    log "INFO: checking recovery resources on partition '$pn'"
    mtdnr="$(find_mtd "$pn")"
    if [ $? -ne 0 ]; then
	log "ERROR: could not find flash partition '$pn'" >&2
	return 1
    fi
    dev=/dev/mtdblock"$mtdnr"
    if [ ! -b "$dev" ]; then
	log "ERROR: block device missing for '$pn'" >&2
	return 1
    fi
    mpt="$(mktemp -d /tmp/mnt.XXXXXX)"
    mount -t "$FFSTYPE" -r "$dev" "$mpt"
    if [ $? -eq 0 ]; then
	# we use tar to read all the files, if an I/O error happens
	# tar will report it; tar skips all files if output is /dev/null,
	# so we go through a pipe to /dev/null
	tar c -C "$mpt" . | cat > /dev/null
	if [ $? -ne 0 ]; then
	    echo "ERROR: failed reading recovery resource's files" >&2
	    ret=1
	fi
	umount "$mpt"
    else
	log "ERROR: recovery resources filesystem missing or corrupt" >&2
	ret=1
    fi
    rmdir "$mpt"

    return $ret
}

#
# MAIN
#

# Zero exit status if given test should be run
function test_selected() {
    local test="$1"
    local i

    for (( i=0; i < "${#SKIP_TEST[@]}"; i++ )); do
	[ "$test" == "${SKIP_TEST[i]}" ] && return 1
    done
    [ "${#RUN_TEST[@]}" -eq 0 ] && return 0 # empty means run all
    for (( i=0; i < "${#RUN_TEST[@]}"; i++ )); do
	[ "$test" == "${RUN_TEST[i]}" ] && return 0
    done
    return 1
}

# Runs the named test
# arg1: test name (function postfix)
# arg2: test label (single word)
function run_test() {
    local test="$1"
    local label="$2"

    log "INFO: running test $label"
    test_$test
}

function main() {
    local ret
    local r i
    local nr_tests nr_skipped nr_passed nr_failed
    local test label status

    nr_failed=0

    ret=0
    if [ "$STOP_PLAYER" == "yes" ]; then
	stop_player || ret=$?
    fi
    video_init || ret=$?
    show_splash "$STARTSPLASH" || ret=$?
    if [ $ret -eq 0 ]; then
	status='PASSED'
    else
	status='FAILED'
	(( nr_failed++ ))
    fi
    log -n "TEST START: $status"

    nr_tests="${#TEST_LIST[@]}"
    nr_passed=0
    nr_skipped=0
    for (( i=1; i <= nr_tests; i++ )); do
	test="${TEST_LIST[i-1]}"
	label="${TEST_LABELS[i-1]}"
	status='FAILED' # default
	if test_selected "$test"; then
	    run_test "$test" "$label"
	    r=$?
	    if [ $r -eq 0 ]; then
		status='PASSED'
		(( nr_passed++ ))
	    else
		(( nr_failed++ ))
	    fi
	else
	    status='SKIPPED'
	    (( nr_skipped++ ))
	fi
	log -n "TEST RESULT: $label $status ($i/$nr_tests)"
    done

    ret=0
    show_splash "$ENDSPLASH" || ret=$?
    video_restore || ret=$?
    if [ "$STOP_PLAYER" == "yes" ]; then
	start_player || ret=$?
    fi
    if [ $ret -eq 0 ]; then
	status='PASSED'
    else
	status='FAILED'
	(( nr_failed++ ))
    fi
    log -n "TEST FINISH: $status"

    log -n "ALL TESTS RESULT: $nr_passed PASSED $nr_skipped SKIPPED" \
	"$nr_failed FAILED"

    [ "$nr_failed" -eq 0 ]
    return
}

function show_usage() {
    echo "$0: [options]"
    echo "Options:"
    echo -e "  -h\tShow this help message and exit"
    echo -e "  -l\tList the test names and report labels and exit"
    echo -e "  -p\tStop player when starting and restart it at end"
    echo -e "  -r <test>\tRun only the specified test; may be given multiple"
    echo -e "\ttimes. If 'all' is given all tests are selected. By default"
    echo -e "\tall tests are selected."
    echo -e "  -s <test>\tSkip the specified test; may be given multiple times"
}

# Lists the available tests as an XML document, which includes the
# list of necessary external checks for each test
function list_tests_xml() {
    local i j
    local tn tl th
    local cn cl cd

    init_hwvars || return

    echo '<?xml version="1.0" encoding="UTF-8" ?>'
    echo "<spxtest>"
    for (( i=0; i < "${#TEST_LIST[@]}"; i++ )); do
	tn="${TEST_LIST[i]}"
	tl="${TEST_LABELS[i]}"
	th="$(eval echo \"\${TEST_HELP_${tn}_${HWTYPE}}\")"
	[ -z "$th" ] && th="$(eval echo \"\${TEST_HELP_${tn}}\")"
	echo "  <test>"
	echo "    <name>$tn</name>"
	echo "    <label>$tl</label>"
	[ -n "$th" ] && echo "    <help>$th</help>"
	eval cn=\( \"\${TEST_CHECK_NAME_$tn[@]}\" \)
	eval cl=\( \"\${TEST_CHECK_LABEL_$tn[@]}\" \)
	eval cd=\( \"\${TEST_CHECK_DESC_$tn[@]}\" \)
	for (( j=0; j < "${#cn[@]}"; j++ )); do
	    echo "    <check>"
	    echo "      <name>${cn[j]}</name>"
	    echo "      <label>${cl[j]}</label>"
	    echo "      <desc>${cd[j]}</desc>"
	    echo "    </check>"
	done
	echo "  </test>"
    done
    echo "</spxtest>"

    return 0
}

# Lists the available tests in text form, it does not include the
# list of necessary external checks for each test
function list_tests() {
    local i

    printf "%-10s\t%s\n" "test name" "label"
    echo "---------------------"
    for (( i=0; i < "${#TEST_LIST[@]}"; i++ )); do
	printf "%-10s\t%s\n" "${TEST_LIST[i]}" "${TEST_LABELS[i]}"
    done

    return 0
}

# Parse options
while getopts :ps:r:lLh opt; do
    case "$opt" in
	p)
	    STOP_PLAYER=yes
	    ;;
	s)
	    SKIP_TEST[${#SKIP_TEST[@]}]="$OPTARG"
	    ;;
	r)
	    if [ "$OPTARG" == "all" ]; then
		unset RUN_TEST
	    else
		RUN_TEST[${#RUN_TEST[@]}]="$OPTARG"
	    fi
	    ;;
	h)
	    show_usage "$0";
	    exit 0
	    ;;
	l)
	    list_tests
	    exit
	    ;;
	L)
	    list_tests_xml
	    exit
	    ;;
	':')
	    echo "ERROR: missing argument for option -${OPTARG}" >&2
	    exit 1
	    ;;
	'?')
	    echo "ERROR: unknown option -${OPTARG}" >&2
	    exit 1
	    ;;
	*)
	    echo "ERROR: unexpected error while parsing options" >&2
	    exit 1
    esac
done

log "INFO: spxtest start"
init && main
ret=$?
log "INFO: spxtest end"
exit $ret

