#===============================================================================
#
#    hosttest.exp
#
#    Support for host-side testing
#
#===============================================================================
######COPYRIGHTBEGIN####
#                                                                          
# ----------------------------------------------------------------------------
# Copyright (C) 1998, 1999, 2000 Red Hat, Inc.
#
# This file is part of the eCos host tools.
#
# This program is free software; you can redistribute it and/or modify it 
# under the terms of the GNU General Public License as published by the Free 
# Software Foundation; either version 2 of the License, or (at your option) 
# any later version.
# 
# This program is distributed in the hope that it will be useful, but WITHOUT 
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for 
# more details.
# 
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 
# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
# ----------------------------------------------------------------------------
#                                                                          
######COPYRIGHTEND####
#===============================================================================
######DESCRIPTIONBEGIN####
#
# Author(s):	bartv
# Contributors:	bartv
# Date:		1998-11-25
# Note:         Arguably this should be a loadable package
#
#####DESCRIPTIONEND####
#===============================================================================
#

# ----------------------------------------------------------------------------
# This script gets loaded by host-side DejaGnu test harnesses to provide
# various utilities for testing eCos host applications. It lives in the
# host-side infrastructure directory and gets installed in
# $(PREFIX)/share/dejagnu.
#
# The script can assume that a number of globals from the site.exp
# file have been read in. These include:
#     tool           - name of the tool (i.e. the package)
#     srcdir         - location of the source directory
#     objdir         - location of the build tree
#     host_alias     - config triplet
#     host_triplet   - ditto
#
# The generated Makefile has some additional information that is useful.
#     CC             - name of the C compiler that is used
#     CXX            - name of the C++ compiler
#     prefix         - where everything gets installed
#     OBJEXT         - either o or obj
#     EXEEXT         - either nothing or .exe
#     VERSION        - the version number
#     CFLAGS         - flags to use when compiling C code
#     CXXFLAGS       - flags to use when compiling C++ code
#
# hosttest_initialize
#     Perform any initialization steps that are needed. Currently this
#     means reading in the Makefile from the top-level of the build tree
#     and figuring out the values of CC, CXX, and anything else that is
#     useful. Any errors should be reported via perror and then the
#     script should exit.
#
#     There is an optional argument, a list of additional variables which
#     should be present in the makefile and whose values are desired.

proc hosttest_initialize { { pkg_vars {} } } {

    # First check that this script is actually running inside DejaGnu
    if { [info exists ::objdir] == 0 } {
	puts "Variable ::objdir is not defined, is this script really running inside DejaGnu?"
	exit 1
    }

    # The information is stored in an array hosttest_data. Make sure this
    # array exists.
    array set ::hosttest_data {}

    # Now clear out any entries in the array
    foreach entry [array names ::hosttest_data] {
	unset ::hosttest_data($entry)
    }

    # Now read in the build tree's Makefile (and not the testsuite's Makefile)
    set filename [file join $::objdir .. Makefile]
    if { [file exists $filename] == 0 } {
	perror "Initialization error: the build tree's Makefile $filename does not exist."
	exit 1
    }
    set status [ catch {
	set fd [open $filename r]
	set contents [read $fd]
	close $fd
    } message]
    if { $status != 0 } {
	perror "Error reading $filename.\n$message"
	exit 1
    }

    # The data is available. Search it for each of the variables of
    # interest. Some variables are optional and are given default
    # values.
    set ::hosttest_data(CFLAGS) ""
    set ::hosttest_data(CXXFLAGS) ""
    
    set lines [split $contents "\n"]

    foreach var [concat { CC CXX prefix OBJEXT EXEEXT VERSION CFLAGS CXXFLAGS } $pkg_vars] {

	set pattern "^$var\[ \t\]*:?=\[ \t\]* (.*)\$"
	set dummy ""
	set match ""

	foreach line $lines {
	    if { [regexp -- $pattern $line dummy match] == 1 } {
		set ::hosttest_data($var) $match
		break
	    }
	}
	if { [info exists ::hosttest_data($var)] == 0 } {
	    perror "Variable $var is not defined in $filename"
	    exit 1
	}
    }

    # If compiling with VC++ remove any cygwin-isms from the prefix
    if { [string match "cl*" $::hosttest_data(CC)] } {
	set status [catch "exec cygpath -w $::hosttest_data(prefix)" message]
	if { $status == 0 } {
	    regsub -all -- {\\} $message {/} ::hosttest_data(prefix)
	} else {
	    perror "Converting cygwin pathname $::hosttest_data(prefix)\n$message"
	    exit 1
	}
    }
}


# ----------------------------------------------------------------------------
# hosttest_extract_version
#     Assuming there has been a call to initialize, the required information
#     should be available in the hosttest_data array. The initialize
#     function should have aborted if the data is not available.

proc hosttest_extract_version { } {

    if { [info exists ::hosttest_data(VERSION)] == 0 } {
	error "No version information - host testing has not been properly initialized."
    }
    
    if { [info exists ::objdir] == 0 } {
	error "Variable ::objdir is not defined, is this script really running inside DejaGnu?"
    }
    return $::hosttest_data(VERSION)
}

# ----------------------------------------------------------------------------
# hosttest_compile
#    compile and link one or more source files. The arguments are:
#    1) the name of the test case
#    2) a list of one or more source files that need to be compiled.
#       Both .c and .cxx files are supported, and the appropriate
#       compiler will be used. If this list is empty then the
#       code will look for a .c or a .cxx file which matches the
#       name of the test executable. Source files are assumed to
#       be relative to $::srcdir/$::subdir
#    3) a list (possibly empty) of directories that should be in the
#       include path. The build tree's directory is automatically in
#       the path, as is $(PREFIX)/include. Note that the build tree
#       is actually one level above objdir, on the assumption that
#       objdir is the testsuite subdirectory of the real objdir.
#    4) ditto for library search paths.
#    5) and a list of additional libraries that should be linked.
#
# Currently it is not possible to pass compiler flags since those
# might need translating between gcc and VC++. This may have to be
# resolved.
#
# Currently linking is not done via libtool. This may have to change.
#
# The various object files and the executable are placed in a directory
# testcase in the build tree, to avoid the risk of name clashes. This
# directory must not exist yet. There is a separate routine hosttest_clean
# which simply expunges the entire testcase directory.
#
# The output of a succesful compile or built is reported using
# verbose at level 2. Unsuccesful compiles or builts are reported using
# level 1.

proc hosttest_compile { name sources incdirs libdirs libs } {

    # Make sure that the testcase directory does not yet exist, then
    # create it. This guarantees a clean system and reasonable access
    # permissions. Each testcase invocation should involve a call to
    # the clean function.
    set dirname [file join $::objdir "testcase"]
    if { [file exists $dirname] != 0 } {
	# An empty directory is ok.
	if { [llength [glob -nocomplain -- [file join $dirname "*"]]] != 0 } {
	    error "hosttest_compile: $dirname already exists"
	}
    }

    set status [catch { file mkdir $dirname } message]
    if { $status != 0 } {
	error "hosttest_compile: unable to create directory $dirname"
    }

    # The only argument that must be valid is the test name.
    if { $name == "" } {
	error "hosttest_compile: invalid test case name"
    }

    # If the list of sources is empty then look for a suitable
    # file in the appropriate directory.
    if { [llength $sources] == 0 } {
	set filename [file join $::srcdir $::subdir "${name}.c"]
	if { [file exists $filename] && [file isfile $filename] } {
	    lappend sources [file tail $filename]
	} else {
	    set filename [file join $::srcdir $::subdir "${name}.cxx"]
	    if { [file exists $filename] && [file isfile $filename] } {
		lappend sources [file tail $filename]
	    } else {
		error "hosttest_compile: no sources listed and unable to find ${name}.c or ${name}.cxx"
	    }
	}
    }

    # For each source file, generate a compile command line and try to execute
    # it. The command line takes the form:
    #  (CC|CXX) -c (CFLAGS|CXXFLAGS) (INCDIRS) -o xxx yyy
    #
    # It is also useful to produce a list of the object files that need to
    # linked later on, and to work out which tool should be invoked for
    # linking.
    set object_files {}
    set has_cxx_files 0
    
    foreach source $sources {
	set commandline ""
	if { [file extension $source] == ".c" } {
	    append commandline "$::hosttest_data(CC) -c $::hosttest_data(CFLAGS) "
	} elseif { [file extension $source] == ".cxx" } {
	    set has_cxx_files 1
	    append commandline "$::hosttest_data(CXX) -c $::hosttest_data(CXXFLAGS) "
	} else {
	    error "hosttest_compile: files of type [file extension $source] ($source) are not yet supported."
	}

	# Include path: start with the source tree. Then the build tree.
	# Then any additional directories specified explicitly by the
	# testcase. Finish off with the prefix. Note that header files
	# in the prefix directory may be out of date, depending on whether
	# or not there has been an install recently.
	append commandline "-I[file join [pwd] [file dirname $::srcdir]] "
	append commandline "-I[file join [pwd] [file dirname $::objdir]] "
	foreach dir $incdirs {
	    append commandline "-I[file join [pwd] $dir] "
	}
	append commandline "-I[file join [pwd] $::hosttest_data(prefix) include] "
	
	# The output file must go into the testcase directory and have the right suffix
	set objfile "[file root [file tail $source]].$::hosttest_data(OBJEXT)"
	lappend object_files $objfile
	if { [string match "cl*" $::hosttest_data(CC)] } {
	    append commandline "-Fo[file join $::objdir testcase $objfile] "
	} else {
	    append commandline "-o [file join $::objdir testcase $objfile] "
	}

	# Finally provide the source file.
	append commandline "[file join $::srcdir $::subdir $source]"
	verbose -log -- $commandline
	
	# Time to invoke the compiler.
	set status [catch { set result [eval exec -keepnewline -- $commandline] } message]
	if { $status == 0 } {
	    # The compile succeeded and the output is in result. Report the
	    # output.
	    verbose -log -- $result
	} else {
	    # The compile failed and the output is in message.
	    verbose -log -- $message
	    error "hosttest_compile: failed to compile $source"
	}
    }

    # At this stage all the source files have been compiled, a list of
    # object files has been constructed, and it is known whether or
    # not any of the sources were c++. Time to construct a new command
    # line.
    set commandline ""
    if { $has_cxx_files == 0 } {
	append commandline "$::hosttest_data(CC) $::hosttest_data(CFLAGS) "
    } else {
	append commandline "$::hosttest_data(CXX) $::hosttest_data(CXXFLAGS) "
    }
    set exename [file join $::objdir "testcase" "$name$::hosttest_data(EXEEXT)"]

    # List all of the object files
    foreach obj $object_files {
	append commandline "[file join $::objdir "testcase" $obj] "
    }

    # Now take care of libraries and search paths. This requires different
    # code for gcc and VC++.

    if { [string match "cl*" $::hosttest_data(CC)] } {
	append commandline "-Fe$exename "

	foreach lib $libs {
	    append commandline "${lib}.lib "
	}
	append commandline "-link "
	append commandline "-libpath=[file join [pwd] [file dirname $::objdir]] "
	foreach dir $libdirs {
	    append commandline "-libpath=[file join [pwd] $dir] "
	}
	append commandline "-libpath=[file join [pwd] $::hosttest_data(prefix) lib] "
    } else {
	append commandline "-o $exename "
	append commandline "-L[file join [pwd] [file dirname $::objdir]] "
	foreach dir $libdirs {
	    append commandline "-L[file join [pwd] $dir] "
	}
	append commandline "-L[file join [pwd] $::hosttest_data(prefix) lib] "
	foreach lib $libs {
	    append commandline "-l$lib "
	}
    }

    # We have a command line, go for it.
    verbose -log -- $commandline
    set status [catch { set result [eval exec -keepnewline -- $commandline] } message]
    if { $status == 0 } {
	# The link has succeeded, we have an executable.
	verbose -log -- $result
    } else {
	# The link failed and the output is in message.
	# Report things are per compilation failures
	verbose -log -- $message
	error "hosttest_compile: failed to link $exename"
    }
    
    # There should be a test executable.
}

# ----------------------------------------------------------------------------
# hosttest_clean
#    Clean up a testcase directory.

proc hosttest_clean { } {

    set dirname [file join $::objdir "testcase"]
    if { [file exists $dirname] == 0 } {

        # Something must have gone seriously wrong during the build phase,
        # there is nothing there.
        return
    }

    if { [file isdirectory $dirname] == 0 } {
	error "hosttest_clean: $dirname should be a directory"
    }

    foreach entry [glob -nocomplain -- [file join $dirname "*"]] {
	set filename [file join $dirname $entry]
	if { [file isfile $filename] == 0 } {
	    error "hosttest_clean: $filename is not a file"
	}
	set status [catch { file delete -force -- $filename } message]
	if { $status != 0 } {
	    error "hosttest_clean: unable to delete $filename, $message"
	}
    }
    set status [catch { file delete -force -- $dirname } message]
    if { $status != 0 } {
	error "hosttest_clean: unable to delete directory $dirname, $message"
    }
}

# ----------------------------------------------------------------------------
# Run a test executable, returning the status code and the output.
# The results are returned in variables. It is assumed that some test cases
# will fail, so raising an exception is appropriate only if something
# has gone wrong at the test harness level. The argument list
# should be the name of the test case (from which the executable file name
# can be derived) and a list of arguments.

proc hosttest_run { result_arg output_arg test args } {

    upvar $result_arg result
    upvar $output_arg output

    # Figure out the filename corresponding to the test and make
    # sure it exists.
    set filename [file join $::objdir "testcase" $test]
    append filename $::hosttest_data(EXEEXT)
    if { ([file exists $filename] == 0) || ([file isfile $filename] == 0) } {
	error "hosttest_run: testcase file $filename does not exist"
    }
    
    # There is no need to worry about interacting with the program,
    # just exec it. It is a good idea to do this in the testcase directory,
    # so that any core dumps get cleaned up as well.
    set current_dir [pwd]
    set status [ catch { cd [file join $::objdir "testcase"] } message ]
    if { $status != 0 } {
	error "unable to change directory to [file join $::objdir testcase]\n$message"
    }
    
    verbose -log -- $filename $args
    set status [ catch { set result [eval exec -keepnewline -- $filename $args] } output]
    if { $status == 0 } {
	# The command has succeeded. The exit code is 0 and the output
	# was returned by the exec.
	set output $result
	set result 0
    } else {
	# The command has failed. The exit code is 1 and the output is
	# already in the right place.
	verbose -log -- $output
	set result 1
    }
    set status [catch { cd $current_dir } message]
    if { $status != 0 } {
	error "unable to change directory back to $current_dir"
    }
}

# ----------------------------------------------------------------------------
# Given some test output, look through it for pass and fail messages.
# These should result in appropriate DejaGnu pass and fail calls.
# In addition, if the program exited with a non-zero status code but
# did not report any failures then a special failure is reported.

proc hosttest_handle_output { name result output } {

    set passes 0
    set fails  0
    
    foreach line [split $output "\n"] {

	# The output should be of one of the following forms:
	#    PASS:<message>
	#    FAIL:<message> Line: xx File: xx
	#    Whatever
	#
	# PASS and FAIL messages will be reported via DejaGnu pass and fail
	# calls. Everything else gets passed to verbose, so the user gets
	# to choose how much information gets reported.

	set dummy   ""
	set match1  ""
	set match2  ""

	if { [regexp -- {^PASS:<(.*)>.*$} $line dummy match1] == 1 } {
	    pass $match1
	    incr passes
	} elseif { [regexp -- {^FAIL:<(.*)>(.*)$} $line dummy match1 match2] == 1 } {
	    fail "$match1 $match2"
	    incr fails
	} else {
	    verbose $line
	}
    }

    if { ($result != 0) && ($fails == 0) } {
	fail "program terminated with non-zero exit code but did not report any failures"
    } elseif { ($passes == 0) && ($fails == 0) } {
	unresolved "test case $name did not report any passes or failures"
    }
}

# ----------------------------------------------------------------------------
# hosttest_run_test_with_filter
#    This routines combines the compile, run and clean operations,
#    plus it invokes a supplied callback to filter the output. The
#    callback is passed three arguments: the test name, the exit code,
#    and all of the program output.

proc hosttest_run_test_with_filter { name filter sources incdirs libdirs libs args } {

    set result 0
    set output ""

    set status [ catch { hosttest_compile $name $sources $incdirs $libdirs $libs } message]
    if { $status != 0 } {
	fail "unable to compile test case $name, $message"
	hosttest_clean
	return
    } 
    set status [ catch { hosttest_run result output $name $args } message]
    if { $status != 0 } {
	fail "unable to run test case $name, $message"
	hosttest_clean
	return
    }
    set status [ catch { $filter $name $result $output } message]
    if { $status != 0 } {
	fail "unable to parse output from test case $name"
	hosttest_clean
	return
    }

    hosttest_clean
}

# ----------------------------------------------------------------------------
# hosttest_run_simple_test
#    This routine combines the compile, run, output, and clean operations.
#    The arguments are the same as for compilation, plus an additional
#    list for run-time parameters to the test case.

proc hosttest_run_simple_test { name sources incdirs libdirs libs args } {


    set result 0
    set output ""

    set status [ catch { hosttest_compile $name $sources $incdirs $libdirs $libs } message]
    if { $status != 0 } {
	fail "unable to compile test case $name, $message"
	hosttest_clean
	return
    } 
    set status [ catch { hosttest_run result output $name $args } message]
    if { $status != 0 } {
	fail "unable to run test case $name, $message"
	hosttest_clean
	return
    }
    set status [ catch { hosttest_handle_output $name $result $output } message]
    if { $status != 0 } {
	fail "unable to parse output from test case $name"
	hosttest_clean
	return
    }

    hosttest_clean
}

# ----------------------------------------------------------------------------
# Filename translation. A particular file has been created and must now
# be accessed from Tcl.
#
# Under Unix everything just works.
#
# Under Windows, well there is cygwin and there is the Windows world.
# A file may have come from either. cygtclsh80 and hence DejaGnu is not
# fully integrated with cygwin, for example it does not know about
# cygwin mount points. There are also complications because of
# volume-relative filenames.
#
# The code here tries a number of different ways of finding a file which
# matches the name. It is possible that the result is not actually what
# was intended, but hopefully this case will never arise.

proc hosttest_translate_existing_filename { name } {

    if { $::tcl_platform(platform) == "unix" } {
	# The file should exist. It is worth checking just in case.
	if { [file exists $name] == 0 } {
	    return ""
	} else {
	    return $name
	}
    }

    if { $::tcl_platform(platform) != "windows" } {
	perror "The testing framework does not know about platform $::tcl_platform(platform)"
	return ""
    }

    # Always get rid of any backslashes, they just cause trouble
    regsub -all -- {\\} $name {/} name

    # If the name is already valid, great.
    if { [file exists $name] } {
	return $name
    }

    # OK, try to use cygwin's cygpath utility to convert it.
    set status [catch "exec cygpath -w $name" message]
    if { $status == 0 } {
	set cygwin_name ""
	regsub -all -- {\\} $message {/} cygwin_name
	if { [file exists $cygwin_name] } {
	    return $cygwin_name
	}
    }

    # Is the name volumerelative? If so work out the current volume
    # from the current directory and prepend this.
    if { [file pathtype $name] == "volumerelative" } {
	append fullname [string range [pwd] 0 1] $name
	if { [file exists $fullname] } {
	    return $fullname
	}
    }

    # There are other possibilities, e.g. d:xxx indicating a file
    # relative to the current directory on drive d:. For now such
    # Lovecraftian abominations are ignored.
    return ""
}

# ----------------------------------------------------------------------------
# Support for assertion dumps. The infrastructure allows other subsystems
# to add their own callbacks which get invoked during a panic and which
# can write additional output to the dump file. For example it would be
# possible to output full details of the current configuration. These
# routines make it easier to write test cases for such callbacks.
#
# hosttest_assert_check(result output)
#     Make sure that the test case really triggered an assertion.
#
# hosttest_assert_read_dump(output)
#     Identify the temporary file used for the dump, read it in, delete
#     it (no point in leaving such temporaries lying around when running
#     testcases) and return the contents of the file.
#
# hosttest_assert_extract_callback(dump title)
#     Given a dump output as returned by read_dump, look for a section
#     generated by a callback with the given title. Return the contents
#     of the callback.

proc hosttest_assert_check { result output } {

    if { $result == 0 } {
	return 0
    }

    foreach line [split $output "\n"] {
	if { [string match "Assertion failure*" $line] } {
	    return 1
	}
    }
    return 0
}

# This routine assumes that assert_check has already been called.
proc hosttest_assert_read_dump { output } {

    foreach line [split $output "\n"] {
	set dummy ""
	set match ""

	if { [regexp -nocase -- {^writing additional output to (.*)$} $line dummy match] } {

	    # The filename is in match, but it may not be directly accessible.
	    set filename [hosttest_translate_existing_filename $match]
	    if { $filename == "" } {
		return ""
	    }
	    set status [ catch {
		set fd   [open $filename r]
		set data [read $fd]
		close $fd
		file delete $filename
	    } message]
            if { $status != 0 } {
		unresolved "Unable to process assertion dump file $filename"
		unresolved "File $filename may have to be deleted manually"
		return ""
	    }
	    return $data
	}
    }
    return ""
}

# Look for the appropriate markers. Also clean up blank lines
# at the start and end.
proc hosttest_assert_extract_callback { dump title } {

    set lines [split $dump "\n"]
    set result ""

    while { [llength $lines] > 0 } {
	set line  [lindex $lines 0]
	set lines [lreplace $lines 0 0]

	if { [regexp -nocase -- "^\# \{\{\{.*${title}.*\$" $line] } {

	    # Skip any blank lines at the start
	    while { [llength $lines] > 0 } {
		set line  [lindex $lines 0]
		if { [regexp -- {^ *$} $line] == 0} {
		    break
		}
		set lines [lreplace $lines 0 0]
	    }

	    # Now add any lines until the close marker.
	    # Nested folds are not supported yet.
	    while { [llength $lines] > 0 } {
		set line  [lindex $lines 0]
		set lines [lreplace $lines 0 0]
		if { [regexp -nocase -- {^\# \}\}\}.*$} $line] } {
		    break
		}
		append result $line "\n"
	    }

	    return $result
	}
    }

    return ""
}




