#!/usr/bin/expect # # \brief Framework for running automated tests # \author Norman Feske # \date 2010-03-16 # # Usage: run --name --include ... # # 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} { upvar $var up_var if {$condition} { append up_var $string } } ## # Append element to list only if 'condition' is satisfied # proc lappend_if {condition var string} { upvar $var up_var if {$condition} { lappend up_var $string } } ## # Check syntax of specified XML file using xmllint # proc check_xml_syntax {xml_file} { if {![have_installed xmllint]} { puts "Warning: Cannot validate config syntax (please install xmllint)" return; } if {[catch {exec xmllint --noout $xml_file} result]} { puts stderr $result puts stderr "Error: Invalid XML syntax in file [run_dir]/config" exit 1 } } ## # 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 check_xml_syntax [run_dir]/genode/config } ## # Integrate specified binaries into boot image # # \param binaries space-separated list of file names located within the # '/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 # spawn_id may be a list of spawned processes if needed # \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 } } ## # Create disk image with the content of the run directory # # optional parameter: --disk-size ... disk size in MiB # proc create_disk_image_from_run_dir { } { global run_target if {![regexp "disk" $run_target]} { return } requires_installation_of parted requires_installation_of resize2fs requires_installation_of fallocate set grub_img "[genode_dir]/tool/grub2-head.img" set disk_img "[run_dir].img" set part1_img "[run_dir]-part1.img" set run_size [expr [regsub {\s.*} [exec du -sm [run_dir]] {}] + 4] set disk_size [get_cmd_arg --disk-size $run_size] set part1_size [expr $disk_size - 1]MiB # extract and resize partition image exec dd if=$grub_img of=$part1_img bs=1M skip=1 2>/dev/null exec fallocate -l $part1_size $part1_img exec resize2fs $part1_img 2>/dev/null # populate partition with binaries exec [genode_dir]/tool/rump -F ext2fs -p [run_dir] $part1_img # merge final image from GRUB2 head and partition exec dd if=$grub_img of=$disk_img status=noxfer bs=1M count=1 2>/dev/null exec dd if=$part1_img of=$disk_img status=noxfer bs=1M seek=1 2>/dev/null exec parted -s $disk_img -- rm 1 mkpart primary 2048s -1s set 1 boot on exec rm -f $part1_img puts "Created image file $disk_img ($disk_size MiB)" } ## # 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] && [expr [have_installed amttool] || [have_installed wsman] ] } { 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 } ## # Reset via Intel AMT (works up to version smaller Intel AMT 9) # proc amt_reset_soap_eoi { } { set timeout 20 set exit_result 1 # # amttool expects in the environment variable AMT_PASSWORD the password # set ::env(AMT_PASSWORD) $::env(AMT_TEST_MACHINE_PWD) while { $exit_result != 0 } { set try_again 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 } } expect { "result: pt_status: success" { break } eof { set try_again 1 } timeout { puts "Error: amttool timed out"; exit -6 } } 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 } if {$try_again != 0 } { continue } set exit_result [lindex $result 3] } } ## # Reset via Intel AMT wsman protocol # proc amt_reset_wsman { } { set xml_request "amt-reset-wsman.xml" set fh [open $xml_request "WRONLY CREAT TRUNC"] puts $fh { 5 http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ComputerSystem CIM_ComputerSystem ManagedSystem } close $fh exec wsman invoke -a RequestPowerStateChange -J $xml_request "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService?SystemCreationClassName=\"CIM_ComputerSystem\",SystemName=\"Intel(r) AMT\",CreationClassName=\"CIM_PowerManagementService\",Name=\"Intel(r) AMT Power Management Service\"" --port 16992 -h $::env(AMT_TEST_MACHINE_IP) --username admin -p $::env(AMT_TEST_MACHINE_PWD) -V -v } ## # Execute scenario using Intel's AMT # proc spawn_amt { wait_for_re timeout_value } { global spawn_id if {![is_amt_available]} { return 0 } # # amttool and wsman are supported for reset # set amt_tool [get_cmd_arg --amt-tool "default"] # # reset the box # if {[have_installed wsman] && ( $amt_tool == "wsman" || $amt_tool == "default") } { amt_reset_wsman } else { if {[have_installed amttool] && ($amt_tool == "amttool" || $amt_tool == "default") } { amt_reset_soap_eoi } else { puts stderr "specified tool \"$amt_tool\" for using Intel AMT is unknown or is not installed" exit -1 } } 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.*" 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 } ## # Load image to target hardware via JTAG # proc jtag_load { } { if {![have_spec arm] || ![have_installed openocd]} { puts "No support for JTAG detected." exit -1 } set debugger [get_cmd_arg --jtag-debugger 1] set board [get_cmd_arg --jtag-board 1] set elf_img "[run_dir]/image.elf" # sleep a bit, board might need some time to come up sleep 8 # parse ELF entrypoint set entrypoint [exec [cross_dev_prefix]readelf -h $elf_img | \ grep "Entry point address: " | \ sed -e "s/.*Entry point address: *//"] eval spawn openocd -f $debugger -f $board -c init -c halt -c \"load_image $elf_img\" -c \"resume $entrypoint\" set jtag_spawn_id $spawn_id set timeout 360 expect { "downloaded" { } eof { puts stderr "openocd command process died unexpectedly" } timeout { puts stderr "Loading timed out" } } } ## # 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 } if {[regexp "jtag" $run_target]} { jtag_load } set timeout 210 while {true} { eval spawn $serial_cmd set serial_spawn_id $spawn_id expect { $kernel_msg { wait_for_output $wait_for_re $timeout_value $serial_spawn_id; return; } eof { continue; } timeout { puts stderr "Boot process timed out"; close; incr retry -1; break; } } } } puts stderr "Boot process failed 3 times in series. I give up!"; exit -1; } ## # 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 } ## # Check if a shell command is installed # # \param command name of the command to search # # \return absolute path of command if cound, or exists if not # proc check_installed {command} { if { [catch {set path [exec which $command]}] == 0} { return $path } set dir { /sbin /usr/sbin /usr/local/bin } foreach location $dir { append location / $command if { [file exists $location] == 1} { return $location } } puts stderr "Error: '$command' command could be not found. Please make sure to install the" puts stderr " packet containing '$command', or make it avaiable in your PATH variable.\n" exit 1 } ## # 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 }