#
# Execute a single test defined in a pkg recipe
#
# This run script executes a single test case as a static system scenario.
# It is meant as a handy tool for quickly reproducing, instrumenting, and
# debugging test cases without the need to create/use any depot archives.
#
# The run script combines the information found in the test's runtime
# definition with the following parameters and heuristics:
#
# - The test must be specified via the 'PKG' environment variable.
#   The specified value is used to find the matching pkg recipe within the
#   source tree. Therecipe must feature a 'runtime' definition as normally
#   used by the depot autopilot.
#
# - The run script scans the source tree for 'target.mk' files and the target
#   names defined via the 'TARGET = <name>' definition within those files.
#   The information is used to determine the correspondence between the
#   target's binary name and its build path (the location of the 'target.mk'
#   within the src/ directory). Note that ambiguities are not handled, e.g.,
#   multiple 'target.mk' files with the same TARGET name will likely cause
#   problems.
#
# - Based on the <content> information found in the test's runtime file,
#   the build targets and boot modules required by the test are determined
#   automatically.
#
# - The test's configuration is expected to have the form of a <config>
#   node in the test's runtime file.
#
# - The success/failure criterion is taken from the runtime's <events>
#   definition but only a subset of the depot-autopilot's features is
#   supported, namely a timeout (indicating a failure) and an expected log
#   (indicating success). If this information are not present, the test
#   is executed forever.
#
# For an example, issue the following command from your build directory:
#
#   make run/test KERNEL=linux PKG=test-fs_report
#

##
# Obtain name of the test pkg from the 'PKG' environment variable
#
proc pkg_name { } {

	global ::env

	if {![info exists ::env(PKG)]} {
		puts stderr "Error: environment variable 'PKG' not defined"
		exit 1
	}
	return $::env(PKG)
}

if {[catch {
	set pkg_recipe [glob "[genode_dir]/repos/*/recipes/pkg/[pkg_name]"]
}]} {
	puts stderr "Error: unable to find 'recipes/pkg/[pkg_name]' in repos"
	exit 1
}


##
# Collect content of the raw archives as used by the test
#
# This function returns a list of file paths that must be added as ROM modules
# to the boot image.
#
# Note that it does not evaluate the 'content.mk' file of the raw recipes but
# relies on the convention that the content of raw archives is hosted at the
# raw archive's recipe location.
#
# The function does not traverse nested pkg archives because no test uses this
# feature of pkg archives.
#
proc _files_from_raw_archives { archives_file } {

	if {![file exists $archives_file]} {
		puts stderr "Warning: $archives_file does not exist"
		return { }
	}

	set fh [open $archives_file "r"]
	set archives [split [read $fh] "\n"]
	close $fh

	set result { }

	foreach archive $archives {

		if {![regexp [_depot_archive_path_pattern] $archive dummy user type name]} {
			continue }

		if {$type != "raw"} {
			continue }

		if {[catch {
			set raw_recipe [glob "[genode_dir]/repos/*/recipes/raw/$name"]
			set files [glob "$raw_recipe/*"]
			foreach file $files {
				if {[string match "*/content.mk" $file]} { continue }
				if {[string match "*/hash"       $file]} { continue }
				lappend result $file
			}
		}]} {
			puts stderr "Warning: cannot obtain content of raw archive $name"
		}
	}
	return $result
}

set files_from_raw_archives [_files_from_raw_archives $pkg_recipe/archives]


##
# Return true if ROM module is provided by a raw archive
#
proc rom_module_from_raw_archive { rom } {

	global files_from_raw_archives

	foreach file $files_from_raw_archives {
		if {[string match "*/$rom" $file]} {
			return 1; } }

	return 0;
}


#
# Extract information from the test's runtime file
#

set runtime_file $pkg_recipe/runtime

if {![file exists $runtime_file]} {
	puts stderr "Error: $pkg_recipe lacks 'runtime' definition"
	exit 1
}

proc query_attr { node_path attr_name xml_file }  {

	set xpath "$node_path/attribute::$attr_name"
	set attr_value [exec xmllint --xpath $xpath $xml_file]

	# in the presence of multiple matching xpaths, return only the first
	regexp {"(.*)"} [lindex $attr_value 0] dummy value
	return $value;
}

proc query_node { node_path xml_file }  {

	set xpath "$node_path"
	set content [exec xmllint --xpath $xpath $xml_file]

	return $content;
}

proc try_query_attr_from_runtime { attr } {

	global runtime_file

	if {[catch {
		set result [query_attr /runtime $attr $runtime_file]
	}]} {
		puts stderr "Error: missing '$attr' attribute in <runtime> at $runtime_file"
		exit 1
	}
	return $result
}

set ram  [try_query_attr_from_runtime ram]
set caps [try_query_attr_from_runtime caps]

if {[catch {
	set config [query_node /runtime/config $runtime_file]
}]} {
	puts stderr "Error: <config> not present <runtime> sub node at $runtime_file"
	puts stderr "       The use of an separate config ROM is not supported."
	exit 1
}

proc desanitize_xml_characters { string } {
	regsub -all {&gt;} $string {>} string
	regsub -all {&lt;} $string {<} string
	return $string
}

set config [desanitize_xml_characters $config]


#
# Gather the source locations of all targets found in the source tree to
# define the build targets and boot modules required by the test.
#
set target_mks [exec sh -c "cd [genode_dir]/repos; \
                              grep \"TARGET.*=\" `find -name target.mk`"]

foreach target_mk [split $target_mks "\n"] {
	regsub {^.*?/src/} $target_mk {} target_mk

	if {[regexp {(.+?)/target.mk.*\s([^\s]+)\s*$} $target_mk dummy target_path target_name]} {
		set src_sub_dir($target_name) $target_path
	}
}

proc content_rom_modules { } {

	global runtime_file

	set attributes [query_node "/runtime/content/rom/attribute::label" $runtime_file]

	set rom_names {}
	foreach attr $attributes {
		regexp {"(.*)"} $attr dummy rom_name
		lappend rom_names $rom_name
	}
	return $rom_names
}

foreach rom [content_rom_modules] {

	# raw-archive content is installed directly into [run_dir]/genode
	if {![rom_module_from_raw_archive $rom]} {
		lappend boot_modules $rom
	}

	# handle vfs plugin shared-object targets
	if {[regexp "^vfs_(.+).lib.so$" $rom dummy plugin]} {
		lappend build_targets lib/vfs/$plugin
		continue
	}

	if {[info exists src_sub_dir($rom)]} {
		lappend build_targets $src_sub_dir($rom)
	}
}


#
# Build
#

lappend build_targets core init timer

# strip duplications
set sorted_build_targets [lsort -unique $build_targets]

build $sorted_build_targets


#
# Configure and assemble boot image
#

create_boot_directory

foreach file $files_from_raw_archives {
	file copy -force $file [run_dir]/genode/ }

install_config {
	<config prio_levels="2">
		<parent-provides>
			<service name="ROM"/>
			<service name="IRQ"/>
			<service name="IO_MEM"/>
			<service name="IO_PORT"/>
			<service name="PD"/>
			<service name="RM"/>
			<service name="CPU"/>
			<service name="LOG"/>
			<service name="TRACE"/>
		</parent-provides>

		<start name="timer" caps="100">
			<resource name="RAM" quantum="1M"/>
			<provides><service name="Timer"/></provides>
			<route>
				<service name="PD">      <parent/> </service>
				<service name="CPU">     <parent/> </service>
				<service name="LOG">     <parent/> </service>
				<service name="ROM">     <parent/> </service>
				<service name="IO_MEM">  <parent/> </service>
				<service name="IRQ">     <parent/> </service>
				<service name="IO_PORT"> <parent/> </service>
			</route>
		</start>

		<start name="test" priority="-1" caps="} $caps {">
			<resource name="RAM" quantum="} $ram {"/>
			<binary name="init"/>
			<route>
				<service name="ROM">   <parent/>    </service>
				<service name="PD">    <parent/>    </service>
				<service name="RM">    <parent/>    </service>
				<service name="CPU">   <parent/>    </service>
				<service name="LOG">   <parent/>    </service>
				<service name="TRACE"> <parent/>    </service>
				<service name="Timer"> <child name="timer"/> </service>
			</route>
			} $config {
		</start>
	</config>
}

lappend boot_modules core ld.lib.so init timer

build_boot_image [lsort -unique $boot_modules]


##
# Return failure timeout in seconds
#
proc query_failure_timeout { } {

	global runtime_file

	set meaning ""
	catch {
		set meaning [query_attr /runtime/events/timeout meaning $runtime_file]
		set sec     [query_attr /runtime/events/timeout sec     $runtime_file]
	}
	if {$meaning == "failed"} {
		return $sec }

	return 0
}

set failure_timeout [expr [query_failure_timeout] + 10]


##
# Return regexp pattern to match the log output for detecting the success
#
proc query_expected_log_pattern { } {

	global runtime_file

	set log ""
	catch {
		set log [query_node "/runtime/events/log\[@meaning='succeeded'\]/child::text()" $runtime_file]
	} { return "" }

	# strip leading and trailing whitespace
	regsub {^\s*} $log {} log
	regsub {\s*$} $log {} log

	# filter pattern line by line
	set lines [split $log "\n"]

	set prefixed_lines { }
	foreach line $lines {
		regsub {^\s*\[init} $line {[init -> test} line

		set line [desanitize_xml_characters $line]

		# quote regexp characters
		foreach char [list {[} {]} {(} {)} {.}] {
			regsub -all "\\$char" $line "\\$char" line }

		# replace wildcards by non-greedy regexp wildcards
		regsub -all {\*} $line {.*?} line

		lappend prefixed_lines "$line.*?"
	}

	set joined [join $prefixed_lines "\n"]
	return ".*?$joined.*?"
}

set expected_pattern [query_expected_log_pattern]

if {$expected_pattern == ""} {
	puts stderr "Warning: unable to obtain expected log pattern from $runtime_file"
	run_genode_until {^this-should=never-match$} $failure_timeout
}

if {$failure_timeout == 0} {
	puts stderr "Warning: could not determine failure timeout, discharge timeout"
	set failure_timeout 1000
}

append qemu_args " -nographic "

run_genode_until $expected_pattern $failure_timeout

