genode/tool/run

803 lines
19 KiB
Plaintext
Executable File

#!/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' tool returns a non-zero return code even if
# successful. So we ignore the return code here.
#
catch { exec [genode_dir]/tool/create_iso iso ISO=[run_dir] }
if {![file exists "[run_dir].iso"]} {
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]} { append qemu_args " -M realview-pbx-a9 -m 256 " }
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 platform_panda] || [have_spec platform_arndale]} {
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 30
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
}