genode/tool/run/run

1043 lines
24 KiB
Plaintext
Executable File

#!/usr/bin/env 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 { } {
exec rm -rf [run_dir]
exec mkdir -p [run_dir]
exec mkdir -p [run_dir]/genode
}
##
# Append string to variable only if 'condition' is satisfied
#
proc append_if {condition var args} {
upvar $var up_var
if {$condition} { append up_var [join $args ""] }
}
##
# 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 $xml_file"
exit 1
}
}
##
# Validate configuration file to match an XML schema using xmllint
#
# \param bin binary name of the component behind the config
# \param xml_file configuration file
# \param xsd_file configuration schema file
# \param avail_xsd_files list of xsd files that might be used for
# configurations of children of the component
# \param nesting_level level of recursive calls of this procedure
#
proc check_config {bin xml_file xsd_file label avail_xsd_files xsd_inc nesting_level} {
# check prerequisites if this is not a recursive call
if {$nesting_level == 0} {
if {![have_installed xmllint]} {
puts ""
puts "Warning: cannot validate config syntax\
(please install xmllint)"
puts ""
exit 1
}
}
# check the given component configuration itself
puts " CHECK $label"
if {[catch {exec xmllint --noout --path $xsd_inc -schema $xsd_file $xml_file} result]} {
if {$result != "$xml_file validates"} {
puts stderr "$result"
puts stderr "Error: Invalid XML syntax"
exit 1
}
}
#
# If this is no instance of Genodes Init component, we can skip checking
# for potential child configurations.
#
if {$bin != "init"} {
return
}
#
# The names of the available XSD files tell us for which binaries we
# can do a check. So iterate through the XSD files and try to find
# corresponding child instances.
#
foreach child_xsd_file $avail_xsd_files {
set child_bin [file tail [file rootname $child_xsd_file]]
for {set child_instance 1} {1} {incr child_instance} {
#
# Try to find a child instance that uses this binary. We start
# with selecting the first matching start node, than the
# second, and so on. As soon as xmlscarlet returns an empty
# string, we know that there is no further matching start node.
#
set xpath_start_cond "child::binary\[@name=\'$child_bin\'\] or \
not(child::binary) and @name=\'$child_bin\'"
set xpath "string(/config/start\[$xpath_start_cond\]\[$child_instance\]/@name)"
set select_xpath "xmllint --xpath \"$xpath\" $xml_file"
if {[catch {exec {*}$select_xpath} child_name]} {
#
# No further child instance that uses this binary.
# Proceed with next binary.
#
break
}
#
# If the child has a config, check it with a recursive
# call to this procedure.
#
set xpath "/config/start\[$xpath_start_cond\]\[$child_instance\]/config"
set select_xpath "xmllint --xpath \"$xpath\" $xml_file"
if {[catch {exec {*}$select_xpath} child_xml]} {
# the child has no config
break
}
# write child config to temporary file
set child_xml_file ".config_$nesting_level.xml.tmp"
set child_xml_fd [open $child_xml_file w]
puts $child_xml_fd $child_xml
close $child_xml_fd
# call this procedure again on the child config file
set child_label "$label -> $child_name"
check_config $child_bin $child_xml_file $child_xsd_file \
$child_label $avail_xsd_files $xsd_inc \
[expr $nesting_level+1]
# clean up
exec rm -f $child_xml_file
}
}
}
##
# Install content of specified variable as init config file
#
proc install_config { args } {
set fh [open "[run_dir]/genode/config" "WRONLY CREAT TRUNC"]
puts $fh [join $args {}]
close $fh
}
##
# Integrate specified binaries into boot image
#
# \param binaries space-separated list of file names located within the
# '<build-dir>/bin/' directory
#
proc build_boot_image {binaries} {
check_for_missing_depot_archives
# lookup XSD files in the run-script-specific folder
set xsd_files ""
if {[catch {exec find [run_dir]/genode -name *.xsd} find_stdout] == 0} {
set xsd_files [split "$find_stdout" "\n"]
}
# lookup XSD files in the binaries folder
foreach binary $binaries {
if {[file exists "bin/$binary.xsd"]} {
lappend xsd_files "bin/$binary.xsd"
}
}
# determine which XSD file to use for the init config
foreach xsd_file $xsd_files {
set filename [file tail $xsd_file]
if {$filename == "init.xsd"} {
set init_xsd_file $xsd_file
}
}
# determine include directories that can be used by the XSD files
global repositories;
set xsd_inc ""
foreach repo $repositories {
if {[file exists $repo/xsd]} {
append xsd_inc "$repo/xsd " }
}
# check configurations of init and its children
puts "checking configuration syntax"
check_config init [run_dir]/genode/config $init_xsd_file init $xsd_files $xsd_inc 0
run_boot_dir $binaries
}
# set expect match-buffer size
match_max -d 40000
##
# 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.
#
proc run_genode_until {{wait_for_re forever} {timeout_value 0} {running_spawn_id -1}} {
#
# If a running_spawn_id is specified, wait for the expected output
#
if {$running_spawn_id != -1} {
wait_for_output $wait_for_re $timeout_value $running_spawn_id
return;
}
set retry 3
while { $retry != 0 } {
#
# Depending an the used run module, a reset can include
# shutting the power off and turning it on again.
#
if (![run_power_cycle]) {
puts "Power cycle step failed, retry."
sleep 3
incr retry -1;
continue
}
if {![run_load]} {
puts "Load step failed, retry."
# kill the spawned load process if there is one
if {[load_spawn_id] != -1} {
set pid [exp_pid -i [load_spawn_id]]
exec kill -9 $pid
}
incr retry -1;
continue;
}
if {![run_log $wait_for_re $timeout_value]} {
puts "Log step failed, retry."
incr retry -1;
continue;
}
return;
}
puts stderr "Boot process failed 3 times in series. I give up!";
exit -1;
}
##
# 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
}
}
##
# 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
}
##
# Return command-line argument value
#
# If a argument name is specified multiple times, return only the
# first match.
#
proc get_cmd_arg_first { arg_name default_value } {
global argv
set arg_idx [lsearch $argv $arg_name]
if {$arg_idx == -1} { return $default_value }
return [lindex $argv [expr $arg_idx + 1]]
}
#
# 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 board_var [get_cmd_arg --board ""]
set repositories [get_cmd_arg --repositories ""]
# 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 }
##
# 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 }] == 0} { return true; }
if {[catch { exec which "/sbin/$program" }] == 0} { return true; }
if {[catch { exec which "/usr/sbin/$program" }] == 0} { return true; }
if {[catch { exec which "/usr/local/bin/$program" }] == 0} { return true; }
return false;
}
##
# Check if a shell command is installed
#
# \param command name of the command to search
#
# \return absolute path of command if found, or exists if not
#
proc installed_command {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 available in your PATH variable.\n"
exit 1
}
##
# 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
}
##
# Return name of kernel-specific binary for a given generic name
#
# The boot_dir plugin may provide functions named 'binary_name_<binary>'
# where '<binary>' stands for a generic name like "timer" or "nic". The
# function returns the name of the binary to integrate into the boot
# directory under the name '<binary>'.
#
# If no such function exists, it returns the argument as is. This is the
# case for regular binaries that appear in the boot directory under their
# original name.
#
proc kernel_specific_binary { binary {silent ""} } {
regsub -all {\.} $binary "_" function_suffix
set function_name "binary_name_$function_suffix"
if {[info procs $function_name] == $function_name} {
set binary_name [$function_name]
if {$silent != "silent"} {
puts "using '$binary_name' as '$binary'"
}
return [$function_name]
}
return $binary
}
proc copy_genode_binaries_to_run_dir { binaries } {
foreach binary $binaries {
file copy -force bin/[kernel_specific_binary $binary] [run_dir]/genode/$binary
}
}
##
# 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
}
set platform_msg [run_boot_string]
if {$platform_msg eq ""} {
set platform_msg "undefined platform command startup string sequence"
}
expect {
-i $running_spawn_id $platform_msg { puts stderr "Error: platform rebooted unexpectedly"; exit -4 }
-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 }
}
append output $expect_out(buffer)
}
##
# Remove 'genode' directory in 'run_dir' unless --preserve-genode-dir is present
# in RUN_OPT
#
proc remove_genode_dir { } {
global env
if {![get_cmd_switch --preserve-genode-dir]} {
exec rm -rf [run_dir]/genode
}
}
##
## Fall-back implementations of all run module procedures
##
##
# Dummy boot_string procedure
#
proc run_boot_string { } { return ""; }
##
# Fall-back boot_dir module
#
# If this function is called someone forgot to include an appropriate boot_dir
# module. So, we exit with an error.
#
proc run_boot_dir { binaries } {
puts stderr "Error: boot_dir module missing, e.g., '--include boot_dir/hw'"
exit 1
}
##
# Dummy image build module
#
proc run_image { {image "" } } { return true; }
##
# Dummy load module
#
proc run_load { } { return true; }
##
# Dummy output module
#
# XXX explain why exit 0 is appropiate
#
proc run_log { wait_for_re timeout_value } { exit 0 }
##
# Dummy power_on module
#
proc run_power_on { } { return true; }
##
# Dummy power_off module
#
proc run_power_off { } { return true; }
##
# Default power cycle fallback procedure
#
proc run_power_cycle { } {
#
# On targets that are directly connected to a socket,
# turn the socket off and on again. On targets that
# use a module were that is not required, e.g. softreset,
# power_off is a NOP. Note, we give the target some time
# to effectively drain energy before switching it on again.
#
run_power_off
sleep 1
return [run_power_on]
}
##
# Default core linker options
#
proc core_ld_opts { } {
set ret { -Wl,-T }
lappend ret "-Wl,[genode_dir]/repos/base/src/ld/genode.ld"
return $ret
}
##
# Default core link address
#
proc core_link_address { } { return "0x01000000" }
##
# Check if a specific file is included
#
proc have_include { name } {
global include_list
foreach element $include_list {
if {[string equal $element $name]} {
return true
}
}
return false
}
##
# Override the exit procedure
#
# We have to override the exit procedure to make sure that a loaded
# run_power_off procedure is in any case execute when stopping the
# execution of the run tool.
#
rename exit real_exit
proc exit {{status 0}} {
run_power_off
real_exit $status
}
##
# Determine terminal program
#
proc terminal { } {
global env
if {[info exists env(COLORTERM)]} {
return $env(COLORTERM)
}
return $env(TERM)
}
##
# Return the board to build for
#
proc board { } {
global board_var
if {$board_var eq ""} {
puts "Unknown platform no BOARD variable set"
exit 1
}
return $board_var
}
##
# Determine GDB executable installed at the host
#
proc gdb { } {
if {[have_installed "[cross_dev_prefix]gdb"]} {
return "[cross_dev_prefix]gdb" }
return [installed_command gdb]
}
##
# Generate assembly code aggregating boot-module data from specified files.
#
proc generate_boot_modules_asm {path modules} {
# architecture dependent definitions
if {[have_spec "64bit"]} { set address_type ".quad"
} else { set address_type ".long" }
# header
set asm_src {}
append asm_src ".set MIN_PAGE_SIZE_LOG2, 12\n"
append asm_src ".set DATA_ACCESS_ALIGNM_LOG2, 3\n"
append asm_src "\n"
append asm_src ".section .data\n"
append asm_src "\n"
append asm_src ".p2align DATA_ACCESS_ALIGNM_LOG2\n"
append asm_src ".global _boot_modules_headers_begin\n"
append asm_src "_boot_modules_headers_begin:\n"
append asm_src "\n"
# module list
set i 0
foreach module $modules {
incr i
append asm_src "${address_type} _boot_module_${i}_name\n"
append asm_src "${address_type} _boot_module_${i}_begin\n"
append asm_src "${address_type} _boot_module_${i}_end -"
append asm_src " _boot_module_${i}_begin\n"
append asm_src "\n"
}
append asm_src ".global _boot_modules_headers_end\n"
append asm_src "_boot_modules_headers_end:\n"
append asm_src "\n"
# module names
set i 0
foreach module $modules {
incr i
append asm_src ".p2align DATA_ACCESS_ALIGNM_LOG2\n"
append asm_src "_boot_module_${i}_name:\n"
append asm_src ".string \"${module}\"\n"
append asm_src ".byte 0\n"
append asm_src "\n"
}
# header end
append asm_src ".section .data.boot_modules_binaries\n"
append asm_src "\n"
append asm_src ".global _boot_modules_binaries_begin\n"
append asm_src "_boot_modules_binaries_begin:\n"
append asm_src "\n"
# module data
set i 0
foreach module $modules {
incr i
append asm_src ".p2align MIN_PAGE_SIZE_LOG2\n"
append asm_src "_boot_module_${i}_begin:\n"
append asm_src ".incbin \"${path}/${module}\"\n"
append asm_src "_boot_module_${i}_end:\n"
append asm_src "\n"
}
append asm_src ".p2align MIN_PAGE_SIZE_LOG2\n"
append asm_src ".global _boot_modules_binaries_end\n"
append asm_src "_boot_modules_binaries_end:\n"
return $asm_src
}
##
# Link core image containing given modules
#
proc build_core {lib modules target link_address} {
# generate assembly code aggregating the modules data
set asm_src [generate_boot_modules_asm [run_dir]/genode $modules]
# architecture dependent definitions
set arch {}
if {[have_spec "x86_64"]} { set arch { -m64 -mcmodel=large } }
if {[have_spec "x86_32"]} { set arch -m32 }
# determine the libgcc
set libgcc [exec [cross_dev_prefix]gcc -print-libgcc-file-name {*}$arch]
# compile the boot modules into one object file
exec [cross_dev_prefix]gcc {*}$arch -c -x assembler -o [run_dir].boot_modules.o - << $asm_src
# link final image
exec [cross_dev_prefix]g++ {*}$arch -nostdlib {*}[core_ld_opts] \
-Wl,-z -Wl,max-page-size=0x1000 \
-Wl,-Ttext=$link_address -Wl,-gc-sections \
-Wl,-nostdlib -Wl,--whole-archive -Wl,--start-group \
$lib [run_dir].boot_modules.o -Wl,--no-whole-archive \
-Wl,--end-group $libgcc -o $target
}
##
# Return kernel-specific files to be excluded from the core image
#
proc kernel_files { } { return { } }
##
# Generate bootable core image containing all boot-modules
#
proc build_core_image { modules } {
set core_obj [kernel_specific_binary core.o]
# replace 'core' with actual core-object name in 'modules' list
if {[lsearch $modules "core"] != -1} {
set idx [lsearch $modules "core"]
set modules [lreplace $modules $idx $idx]
lappend modules $core_obj
}
copy_genode_binaries_to_run_dir $modules
# create core binary without modules for debugging
if {[file exists debug/$core_obj]} {
build_core debug/$core_obj {} [run_dir].core [core_link_address]
}
# determine modules to be incorporated into the core image
set modules [glob -nocomplain -tails -directory [run_dir]/genode/ *]
set excluded_modules [kernel_files]
lappend excluded_modules $core_obj
foreach excluded $excluded_modules {
set modules [lsearch -inline -not -all $modules $excluded] }
# check syntax of all boot modules named *.config
foreach file [glob -nocomplain [run_dir]/genode/*.config] {
check_xml_syntax $file }
# create core binary containing the boot modules
build_core [run_dir]/genode/$core_obj $modules [run_dir]/image.elf [core_link_address]
exec [cross_dev_prefix]strip [run_dir]/image.elf
# Save config part of the image.elf for easy inspection
exec cp -f [run_dir]/genode/config [run_dir].config
}
source [genode_dir]/tool/run/depot.inc
##
## Execution of run scripts
##
set include_list { }
##
# Read and execute files specified as '--include' arguments
#
# Out of convenience run modules may be included by using a relative path.
#
foreach include_name [get_cmd_arg --include ""] {
# first check if the include name is absolute
if {[string first "/" $include_name] == 0} {
puts "including $include_name"
lappend include_list $include_name
source $include_name
continue
}
# if it is relative, check run modules
set run_path [genode_dir]/tool/run
set dir { etc }
lappend dir $run_path
set found 0
foreach location $dir {
set include_file $location/$include_name
if {[file exists $include_file] == 1} {
puts "including $include_file"
lappend include_list $include_name
source $include_file
set found 1
break
} else {
}
}
if {!$found} {
puts stderr "Aborting, could not load '$include_file'"
exit -1;
}
}
puts "\nRun script execution successful."
exit 0