#!/usr/bin/expect

#
# \brief  Framework for running automated tests
# \author Norman Feske
# \date   2010-03-16
#
# Usage: run --name <run_name> --include <run_script> ...
#
# The '--name' argument is used for as name for the boot-image and
# temporary directories. The files includes via the '--include'
# argument provide platform-specific additions/refinements to the
# test framework as well as the actual test steps.
#


##
# Remove leading and trailing whitespace from string
#
proc strip_whitespace {string} {
	regsub -all {^\s+} $string "" string
	regsub -all {\s+$} $string "" string
	return $string
}


##
# Check if the specified spec requirement is satisfied
#
proc assert_spec {spec} {
	global specs
	if {[lsearch $specs $spec] == -1} {
		puts stderr "Test requires '$spec'"
		exit 0
	}
}


##
# Build genode targets specified as space-separated strings
#
# If the build process fails, this procedure will exit the program with
# the error code -4.
#
proc build {targets} {

	if {[get_cmd_switch --skip-build]} return

	regsub -all {\s\s+} $targets " " targets
	puts "building targets: $targets"
	set timeout 10000
	set pid [eval "spawn make $targets"]
	expect { eof { } }
	if {[lindex [wait $pid] end] != 0} {
		puts "Error: Genode build failed"
		exit -4
	}
	puts "genode build completed"
}


##
# Create a fresh boot directory
#
proc create_boot_directory { } { }


##
# Append string to variable only if 'condition' is satisfied
#
proc append_if {condition var string} {
	global $var
	if {$condition} { append $var $string }
}


##
# Append element to list only if 'condition' is satisfied
#
proc lappend_if {condition var string} {
	global $var
	if {$condition} { lappend $var $string }
}



##
# Install content of specfied variable as init config file
#
proc install_config {config} {
	set fh [open "[run_dir]/genode/config" "WRONLY CREAT TRUNC"]
	puts $fh $config
	close $fh
}


##
# Integrate specified binaries into boot image
#
# \param binaries  space-separated list of file names located within the
#                  '<build-dir>/bin/' directory
#
# This function should be implemented by a platform-specific file
# included via the '--include' argument.
#
proc build_boot_image {binaries} { }


##
# Execute Genode
#
# \param  wait_for_re    regular expression that matches the test completion
# \param  timeout_value  timeout in seconds
# \param  spawn_id       spawn_id of a already running and spawned process
# \global output         contains the core output (modified)
#
# If the function is called without any argument, Genode is executed in
# interactive mode.
#
# If the test execution times out, this procedure will exit the program with
# the error code -2.
#
# This function must be implemented by the platform-specific test environment.
# If not implemented, the program exits with the error code -3.
#
proc run_genode_until {{wait_for_re forever} {timeout_value 0} {running_spawn_id -1}} {
	puts stderr "Error: 'run_genode_until' is not implemented for this platform"
	exit -3
}


##
# Remove color information from output
#
proc filter_out_color_escape_sequences { } {
	global output
	regsub -all {\e\[.*?m} $output "" output
}


##
# Remove superfluous empty lines and unify line endings from output
#
proc trim_lines { } {
	global output
	regsub -all {[\r\n]+} $output "\n" output
}


##
# Filter output based on the specified pattern
#
# Only those lines that match the pattern are preserved.
#
proc grep_output {pattern} {
	global output

	filter_out_color_escape_sequences

	trim_lines

	set output_list [split $output "\n"]
	set filtered ""
	foreach line $output_list {
		if {[regexp $pattern $line]} {
			append filtered "$line\n"
		}
	}
	set output $filtered
}


##
# Unify known variations that appear in the test output
#
# \global output test output (modified)
#
proc unify_output {pattern replacement} {
	global output
	regsub -all $pattern $output $replacement output
}


##
# Compare output against expected output line by line
#
# \param  good   expected test output
# \global output test output
#
# This procedure will exit the program with the error code -1 if the
# comparison fails.
#
proc compare_output_to { good } {
	global output
	set output_list [split [strip_whitespace $output] "\n"]
	set good_list   [split [strip_whitespace $good]   "\n"]

	set i 0
	set mismatch_cnt 0
	foreach good_line $good_list {
		set output_line [strip_whitespace [lindex $output_list $i]]
		set good_line   [strip_whitespace $good_line]

		if {$output_line != $good_line} {
			puts ""
			puts stderr "Line $i of output is unexpected"
			puts stderr " expected: '$good_line'"
			puts stderr " got:      '$output_line'"
			incr mismatch_cnt
		}
		incr i
	}

	#
	# if $good is empty the foreach-loop isn't entered
	# so we've to check for it separately
	#
	if {![llength $good_list] && [llength $output_list]} {
		foreach output_line $output_list {
			set output_line [strip_whitespace $output_line]
			puts ""
			puts stderr "Line $i of output is unexpected"
			puts stderr " got:      '$output_line'"
			incr mismatch_cnt
			incr i
		}
	}

	if {$mismatch_cnt > 0} {
		puts "Error: Test failed, $mismatch_cnt unexpected lines of output"
		exit -1
	} else {
		puts "Test succeeded"
	}
}


##
# Return true if command-line switch was specified
#
proc get_cmd_switch { arg_name } {
	global argv
	return [expr [lsearch $argv $arg_name] >= 0]
}


##
# Return command-line argument value
#
# If a argument name is specified multiple times, a
# list of argument values is returned.
#
proc get_cmd_arg { arg_name default_value } {
	global argv

	# find argument name in argv list
	set arg_idx_list [lsearch -all $argv $arg_name]

	if {[llength $arg_idx_list] == 0} { return $default_value }

	set result {}
	foreach arg_idx $arg_idx_list {
		set next_idx [expr $arg_idx + 1]

		# stop if argv ends with the argument name
		if {$next_idx >= [llength $argv]} continue

		# return list element following the argument name
		lappend result [lindex $argv $next_idx]
	}

	# if argument occurred only once, return its value
	if {[llength $result] == 1} { return [lindex $result 0] }

	# if argument occurred multiple times, contain list of arguments
	return $result
}


#
# Read command-line arguments
#

set run_name         [get_cmd_arg --name       "noname"]
set genode_dir       [get_cmd_arg --genode-dir       ""]
set cross_dev_prefix [get_cmd_arg --cross-dev-prefix ""]
set specs            [get_cmd_arg --specs            ""]
set repositories     [get_cmd_arg --repositories     ""]
set qemu_args        [get_cmd_arg --qemu-args        ""]
set run_target       [get_cmd_arg --target       "qemu"]
set serial_cmd       [get_cmd_arg --serial-cmd "picocom -b 115200 /dev/ttyUSB0"]



#
# Enable run scripts to extend 'qemu_arg' via 'append' without bothering
# about the required whitespace in front of the custom arguments.
#
append qemu_args " "

# accessor functions for command-line arguments
proc run_name         { } { global run_name;         return $run_name }
proc run_dir          { } { global run_name;         return var/run/$run_name }
proc genode_dir       { } { global genode_dir;       return $genode_dir }
proc cross_dev_prefix { } { global cross_dev_prefix; return $cross_dev_prefix }

# set expect match-buffer size
match_max -d 20000

##
# Return true if spec value is set for the build
#
proc have_spec {spec} { global specs; return [expr [lsearch $specs $spec] != -1] }


##
# Return true if specified program is installed
#
proc have_installed {program} {
	if {[catch { exec which $program }]} { return false; }
	return true
}


##
# Return true if specified program is installed on the host platform
#
proc requires_installation_of {program} {
	if {![have_installed $program]} {
		puts "Run script aborted because $program is not installed"; exit
	}
}


##
# Return first repository containing the given path
#
proc repository_contains {path} {
	global repositories;
	foreach i $repositories {
		if {[file exists $i/$path]} { return $i }
	}
}


##
## Utilities for performing steps that are the same on several platforms
##

##
# Read kernel location from build-directory configuration
#
# If config file does not exist or if there is no 'KERNEL' declaration in the
# config file, the function returns 'default_location'. If the config file
# points to a non-existing kernel image, the function aborts with the exit
# value -6.
#
proc kernel_location_from_config_file { config_file default_location } {
	global _kernel

	if {![info exists _kernel]} {
		if {[file exists $config_file]} {
			set _kernel [exec sed -n "/^KERNEL/s/^.*=\\s*//p" $config_file]

			# check if the regular expression matched
			if {$_kernel != ""} {
				if {[file exists $_kernel]} {
					return $_kernel
				} else {
					puts stderr "Error: kernel specified in '$config_file' does not exist"
					exit -6
				}
			}
		}

		# try to fall back to version hosted with the Genode build directory
		set _kernel $default_location
	}
	return $_kernel
}


##
# Install files needed to create a bootable ISO image
#
# The ISO boot concept uses isolinux to load GRUB, which in turn loads Genode.
# This way we can make use of isolinux' support for booting ISO images from a
# USB stick.
#
proc install_iso_bootloader_to_run_dir { } {

	exec mkdir -p [run_dir]/boot/isolinux
	exec cp [genode_dir]/tool/boot/chain.c32    [run_dir]/boot/isolinux
	exec cp [genode_dir]/tool/boot/isolinux.bin [run_dir]/boot/isolinux
	exec cp [genode_dir]/tool/boot/isolinux.cfg [run_dir]/boot/isolinux

	exec mkdir -p [run_dir]/boot/grub
	exec cp [genode_dir]/tool/boot/stage2_eltorito [run_dir]/boot/grub
}


##
# Copy the specified binaries from the 'bin/' directory to the run
# directory and try to strip executables.
#
proc copy_and_strip_genode_binaries_to_run_dir { binaries } {

	foreach binary $binaries {
		exec cp bin/$binary [run_dir]/genode
		catch {
			exec [cross_dev_prefix]strip [run_dir]/genode/$binary || true }
	}
}


##
# Create ISO image with the content of the run directory
#
proc create_iso_image_from_run_dir { } {

	puts "creating ISO image..."
	exec rm -f "[run_dir].iso"

	#
	# The 'create_iso' write diagnostics to stderr, which are interpreted as
	# execution failure by expect unless '-ignorestderr' is set on 'exec'.
	#
	if {[catch {exec -ignorestderr [genode_dir]/tool/create_iso iso ISO=[run_dir]} ]} {
		puts stderr "Error: ISO image creation failed"
		exit -5
	}
}

##
# Wait for a specific output of a already running spawned process
#
proc wait_for_output { wait_for_re timeout_value running_spawn_id } {
	global output

	if {$wait_for_re == "forever"} {
		set timeout -1
		interact {
			\003 {
				send_user "Expect: 'interact' received 'strg+c' and was cancelled\n";
				exit
			}
			-i $running_spawn_id
		}
	} else {
		set timeout $timeout_value
	}

	expect {
		-i $running_spawn_id -re $wait_for_re { }
		eof     { puts stderr "Error: Spawned process died unexpectedly";   exit -3 }
		timeout { puts stderr "Error: Test execution timed out"; exit -2 }
	}
	set output $expect_out(buffer)
}

##
# Execute scenario using Qemu
#
proc spawn_qemu { wait_for_re timeout_value } {
	global qemu_args
	global qemu
	global spawn_id
	global run_target

	#
	# Back out on platforms w/o Qemu support
	#
	if {![is_qemu_available]} { return 0 }

	if {[have_spec x86_32]} { set qemu "qemu-system-i386" }
	if {[have_spec x86_64]} { set qemu "qemu-system-x86_64" }
	if {[have_spec arm]}    { set qemu "qemu-system-arm" }

	#
	# Only the x86_64 variant of Qemu provides the emulation of hardware
	# virtualization features used by NOVA. So let's always stick to this
	# varient of Qemu when working with NOVA even when operating in 32bit.
	#
	if {[have_spec nova]} { set qemu "qemu-system-x86_64" }

	#
	# Redirect serial output to stdio, but only in graphics mode and no
	# explicit configuration of serial interfaces is specified in the run
	# script. The 'mon' prefix enables the access to the qemu console.
	#
	if {![regexp -- {-nographic} $qemu_args dummy] &&
	    ![regexp -- {-serial}    $qemu_args dummy]} {
	    append qemu_args " -serial mon:stdio " }

	# tweak emulated platform for specific platforms
	if {[have_spec platform_pbxa9]}  {
		#
		# For PBXA9 qemu adjusts provided RAM chips to the -m arg. Thus we
		# filter user values and force value that enables all chips that Genode
		# expects to be available. Not doing so leads to inexplicable errors.
		#
		regsub -all {\-m ([0-9])+} $qemu_args "" qemu_args
		append qemu_args " -m 768"
		append qemu_args " -M realview-pbx-a9"
	}
	if {[have_spec platform_vpb926]} { append qemu_args " -M versatilepb     -m 128 " }
	if {[have_spec platform_vea9x4]} { append qemu_args " -M vexpress-a9 -cpu cortex-a9 -m 256 " }

	# on x86, we support booting via pxe or iso image [default]
	if {[have_spec x86]} {
		if {[regexp "qemu" $run_target] && [regexp "pxe" $run_target]} {
			append qemu_args " -boot n -tftp [run_dir] -bootp boot/pulsar -no-reboot -no-shutdown "
		} else {
			append qemu_args " -cdrom [run_dir].iso "
		}
	}

	# on ARM, we supply the boot image as kernel
	if {[have_spec arm]} { append qemu_args " -kernel [run_dir]/image.elf " }

	eval spawn $qemu $qemu_args
	set qemu_spawn_id $spawn_id
	wait_for_output $wait_for_re $timeout_value $qemu_spawn_id
}


##
# Check whether Qemu support is available
#
proc is_qemu_available { } {
	global run_target

	if {![regexp "qemu" $run_target]} { return false }

	if {[have_spec linux]} { return false }

	if {[have_spec platform_panda]
	 || [have_spec platform_arndale]
	 || [have_spec platform_rpi]} {
		puts stderr "skipping execution because platform is not supported by qemu"
		return false
	}
	return true
}


##
# Check whether AMT support is available
#
proc is_amt_available { } {
	global run_target

	if {![have_spec x86] || ![regexp "amt" $run_target]} { return false }

	if {[info exists ::env(AMT_TEST_MACHINE_IP)] && 
	    [info exists ::env(AMT_TEST_MACHINE_PWD)] &&
		[have_installed amtterm] &&
		[have_installed amttool]} {
		return true
	}
	puts "No support for Intel's AMT detected."
	return false
}


##
# Check whether output is expected via a local attached serial device
#
proc is_serial_available { } {
	global run_target

	if {![regexp "serial" $run_target]} { return false }

	return true
}


##
# Execute scenario using Intel's AMT 
#
proc spawn_amt { wait_for_re timeout_value } {
	global spawn_id

	if {![is_amt_available]} { return 0 }

	#
	# amttool expects in the environment variable AMT_PASSWORD the password
	#
	set ::env(AMT_PASSWORD) $::env(AMT_TEST_MACHINE_PWD)

	#
	# reset the box
	#
	set timeout 10
	set exit_result 1

	while { $exit_result != 0 } {
		set time_start [ clock seconds ]
		spawn amttool $::env(AMT_TEST_MACHINE_IP) reset
		expect {
			"host"  { send "y\r"; }
			eof     { puts "Error: amttool died unexpectedly"; exit -4 }
			timeout { puts "Error: amttool timed out"; exit -5 }
		}
		catch wait result
		set time_end [ clock seconds ]
		if {[expr $time_end - $time_start] <= 1} {
			incr timeout -1
		} else {
			incr timeout [expr -1 * ($time_end - $time_start)]
		}
		if {$timeout < 0} {
			set timeout 0
		}
		set exit_result [lindex $result 3]
	}
	sleep 5

	#
	# grab output
	#
	set amtterm  "amtterm -u admin -p $::env(AMT_TEST_MACHINE_PWD) -v $::env(AMT_TEST_MACHINE_IP)"
	if {$wait_for_re == "forever"} {
		set timeout -1
	} else {
		set timeout [expr $timeout_value + 30]
	}
	set exit_result 1

	while { $exit_result != 0 } {
		set time_start [ clock seconds ]
		set pid [eval "spawn $amtterm"]
		expect {
			-re $wait_for_re { break }
			eof     { continue }
			timeout { puts "Error: Test execution timed out"; exit -2 }
		}
		catch wait result
		set time_end [ clock seconds ]
		if {[expr $time_end - $time_start] <= 1} {
			incr timeout -1
		} else {
			incr timeout [expr -1 * ($time_end - $time_start)]
		}
		if {$timeout < 0} {
			set timeout 0
		}
		set exit_result [lindex $result 3]
	}
	global output
	set output $expect_out(buffer)
}


##
# Reset test machine via IP power plug NETIO-230B from Koukaam
#
proc power_plug_connect {} {
	set server_ip [get_cmd_arg --reset-ip 1]
	set user_name [get_cmd_arg --reset-user 1]
	set password  [get_cmd_arg --reset-passwd 1]

	spawn telnet $server_ip 1234
	set connection_id $spawn_id
	expect -i $connection_id "KSHELL V1.3"
	send -i $connection_id "login $user_name $password\n"
	expect -i $connection_id "250 OK"

	return $connection_id
}


proc power_plug_reset {} {
	set power_port [get_cmd_arg --reset-port 1]

	set connection_id [power_plug_connect]

	send -i $connection_id "port $power_port\n"
	expect -i $connection_id -re {250 [0-9]+.*\n}
	regexp -all {[0-9]+} $expect_out(0,string) power_status
	if {!$power_status} {
		puts "port $power_port is off - switching it on"
		send -i $connection_id "port $power_port 1\n"
		expect -i $connection_id "250 OK"
	} else {
		puts "port $power_port is on - reset port"
		send -i $connection_id "port $power_port int\n"
		expect -i $connection_id "250 OK"
	}
}


##
# Overwrite exit handler to switch off power plug adapter at script exit
#
rename exit power_plug_off_exit
proc exit {{status 0}} {
	global run_target
	if {[regexp "reset" $run_target]} {
		set power_port [get_cmd_arg --reset-port 1]
		set connection_id [power_plug_connect]

		puts "switch port $power_port off"
		send -i $connection_id "port $power_port 0\n"
		expect -i $connection_id "250 OK"
	}
	power_plug_off_exit $status
}


##
# Execute scenario expecting output via serial device
#
proc spawn_serial { wait_for_re timeout_value kernel_msg } {
	global spawn_id
	global serial_cmd
	global run_target

	set retry 3
	while { $retry != 0 } {

		if {[regexp "reset" $run_target]} {
			power_plug_reset
		}

		eval spawn $serial_cmd
		set serial_spawn_id $spawn_id

		set timeout 210
		expect {
			$kernel_msg  { break; }
			eof     { puts stderr "Serial command process died unexpectedly"; incr retry -1; }
			timeout { puts stderr "Boot process timed out"; close; incr retry -1; }
		}
	}
	if { $retry == 0 } {
		puts stderr "Boot process failed 3 times in series. I give up!";
		exit -1;
	}

	wait_for_output $wait_for_re $timeout_value $serial_spawn_id
}


##
# Determine terminal program
#
proc terminal { } {
	global env
	if {[info exists env(COLORTERM)]} {
		return $env(COLORTERM)
	}
	return $env(TERM)
}


##
# Determine GDB executable installed at the host
#
proc gdb { } {
	if {[have_installed "[cross_dev_prefix]gdb"]} {
		return "[cross_dev_prefix]gdb" }

	if {[have_installed gdb]} {
		return "gdb" }

	requires_installation_of gdb
}


##
#  U-boot bootloader specific uImage
#
# \param  elf_img   ELF binary to build uImage from
#
proc build_uboot_image {elf_img} {
	global run_target

	if {[regexp "uboot" $run_target]} {

		# parse ELF entrypoint and load address
		set entrypoint [exec [cross_dev_prefix]readelf -h $elf_img | \
		                grep "Entry point address: " | \
		                sed -e "s/.*Entry point address: *//"]
		set load_addr [exec [cross_dev_prefix]readelf -l $elf_img | \
		               grep -m 1 "LOAD"]
		set load_addr [lindex [regexp -inline -all -- {\S+} $load_addr] 3]

		# compress ELF
		set bin_img "[run_dir]/image.bin"
		exec [cross_dev_prefix]objcopy -O binary $elf_img $bin_img
		exec gzip --best --force $bin_img

		# create compressed uImage
		set uboot_img [run_dir]/uImage
		exec mkimage -A arm -O linux -T kernel -C gzip -a $load_addr \
		             -e $entrypoint -d $bin_img.gz $uboot_img
		exec rm -rf $bin_img.gz
	}
}


##
## Execution of run scripts
##

#
# Read and execute files specified as '--include' arguments
#

foreach include_name [get_cmd_arg --include ""] {
	puts "using run script $include_name"
	source $include_name
}