#!/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 { } { 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 # '/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 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_' # where '' 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 ''. # # 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) } ## # 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