# This file is part of ltrace.
# Copyright (C) 2012, 2013 Petr Machata, Red Hat Inc.
# Copyright (C) 2006 Yao Qi, IBM Corporation
#
# 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., 51 Franklin St, Fifth Floor, Boston, MA
# 02110-1301 USA

# Generic ltrace test subroutines that should work for any target.  If these
# need to be modified for any target, it can be done with a variable
# or by passing arguments.

source $objdir/env.exp

if [info exists TOOL_EXECUTABLE] {
	set LTRACE $TOOL_EXECUTABLE
} else {
	set LTRACE $objdir/../ltrace
}

if {[info exists VALGRIND] && ![string equal $VALGRIND {}]} {
	verbose "Running under valgrind command: `$VALGRIND'"
	set LTRACE "$VALGRIND $LTRACE"
}

set LTRACE_OPTIONS {}
set LTRACE_ARGS {}
set LTRACE_TEMP_FILES {}

# Pre-8.5 TCL doesn't have lreverse.  The following is taken from:
#  http://www2.tcl.tk/17188

if {[info command lreverse] == ""} {
    proc lreverse l {
        set r {}
        set i [llength $l]
        while {[incr i -1]} {lappend r [lindex $l $i]}
        lappend r [lindex $l 0]
    }
}

# ltrace_compile SOURCE DEST TYPE OPTIONS 
#
# Compile PUT(program under test) by native compiler.   ltrace_compile runs 
# the right compiler, and TCL captures the output, and I evaluate the output.
#
# SOURCE is the name of program under test, with full directory.
# DEST is the name of output of compilation, with full directory.
# TYPE is an enum-like variable to affect the format or result of compiler
#   output.  Values:
#   executable   if output is an executable.
#   object       if output is an object.
# OPTIONS is option to compiler in this compilation.
proc ltrace_compile {source dest type options} {
    global LTRACE_TESTCASE_OPTIONS;

    if {![string equal "object" $type]} {
	# Add platform-specific options if a shared library was specified using
	# "shlib=librarypath" in OPTIONS.
	set new_options ""
	set shlib_found 0

	foreach opt $options {
	    if [regexp {^shlib=(.*)} $opt dummy_var shlib_name] {
		if [test_compiler_info "xlc*"] {
		    # IBM xlc compiler doesn't accept shared library named other
		    # than .so: use "-Wl," to bypass this
		    lappend source "-Wl,$shlib_name"
		} else {
		    lappend source $shlib_name
		}

		if {$shlib_found == 0} {
		    set shlib_found 1

		    if { ([test_compiler_info "gcc-*"]&& ([istarget "powerpc*-*-aix*"]|| [istarget "rs6000*-*-aix*"] ))} {
			lappend options "additional_flags=-L${objdir}/${subdir}"
		    } elseif { [istarget "mips-sgi-irix*"] } {
			lappend options "additional_flags=-rpath ${objdir}/${subdir}"
		    }
		}

	    } else {
		lappend new_options $opt
	    }
	}

	#end of for loop
	set options $new_options
    }

    # dump some information for debug purpose.
    verbose "options are $options"
    verbose "source is $source $dest $type $options"

    # Wipe the DEST file, so that we don't end up running an obsolete
    # version of the binary.
    exec rm -f $dest

    set result [target_compile $source $dest $type $options];
    verbose "result is $result"
    regsub "\[\r\n\]*$" "$result" "" result;
    regsub "^\[\r\n\]*" "$result" "" result;
    if { $result != "" && [lsearch $options quiet] == -1} {
	clone_output "compile failed for ltrace test, $result"
    }
    return $result;
}

proc get_compiler_info {binfile args} {
    # For compiler.c and compiler.cc
    global srcdir

    # I am going to play with the log to keep noise out.
    global outdir
    global tool

    # These come from compiler.c or compiler.cc
    global compiler_info

    # Legacy global data symbols.
    #global gcc_compiled

    # Choose which file to preprocess.
    set ifile "${srcdir}/lib/compiler.c"
    if { [llength $args] > 0 && [lindex $args 0] == "c++" } {
	    set ifile "${srcdir}/lib/compiler.cc"
    }

    # Run $ifile through the right preprocessor.
    # Toggle ltrace.log to keep the compiler output out of the log.
    #log_file
    set cppout [ ltrace_compile "${ifile}" "" preprocess [list "$args" quiet] ]
    #log_file -a "$outdir/$tool.log" 

    # Eval the output.
    set unknown 0
    foreach cppline [ split "$cppout" "\n" ] {
	    if { [ regexp "^#" "$cppline" ] } {
	      # line marker
	    } elseif { [ regexp "^\[\n\r\t \]*$" "$cppline" ] } {
	      # blank line
	    } elseif { [ regexp "^\[\n\r\t \]*set\[\n\r\t \]" "$cppline" ] } {
	    # eval this line
	      verbose "get_compiler_info: $cppline" 2
	      eval "$cppline"
	  } else {
	    # unknown line
	    verbose "get_compiler_info: $cppline"
	    set unknown 1
	  }
      }

    # Reset to unknown compiler if any diagnostics happened.
    if { $unknown } {
	    set compiler_info "unknown"
    }
  return 0
}

proc test_compiler_info { {compiler ""} } {
    global compiler_info

     if [string match "" $compiler] {
         if [info exists compiler_info] {
	     verbose "compiler_info=$compiler_info"
	     # if no arg, return the compiler_info string
             return $compiler_info
         } else {
             perror "No compiler info found."
         }
     }

    return [string match $compiler $compiler_info]
}

proc ltrace_compile_shlib {sources dest options} {
    set obj_options $options
    verbose "+++++++ [test_compiler_info]" 
    switch -glob [test_compiler_info] {
	"xlc-*" {
	    lappend obj_options "additional_flags=-qpic"
	}
	"gcc-*" {
	    if { !([istarget "powerpc*-*-aix*"]
		   || [istarget "rs6000*-*-aix*"]) } {
                lappend obj_options "additional_flags=-fpic"
	    }
          }
  "xlc++-*" {
      lappend obj_options "additional_flags=-qpic"
  }

	default {
	    fail "Bad compiler!"
            }
    }

    if {![LtraceCompileObjects $sources $obj_options objects]} {
	return -1
    }

    set link_options $options
    if { [test_compiler_info "xlc-*"] || [test_compiler_info "xlc++-*"]} {
	lappend link_options "additional_flags=-qmkshrobj"
    } else {
	lappend link_options "additional_flags=-shared"
    }
    if {[ltrace_compile "${objects}" "${dest}" executable $link_options] != ""} {
	return -1
    }

    return
}

# WipeFiles --
#
#	Delete each file in the list.
#
# Arguments:
#	files	List of files to delete.
#
# Results:
#	Each of the files is deleted.  Files are deleted in reverse
#	order, so that directories are emptied and can be deleted
#	without using -force.  Returns nothing.

proc WipeFiles {files} {
    verbose "WipeFiles: $files\n"
    foreach f [lreverse $files] {
	file delete $f
    }
}

# LtraceTmpDir --
#
#	Guess what directory to use for temporary files.
#	This was adapted from http://wiki.tcl.tk/772
#
# Results:
#	A temporary directory to use.  The current directory if no
#	other seems to be available.

proc LtraceTmpDir {} {
    set tmpdir [pwd]

    if {[file exists "/tmp"]} {
	set tmpdir "/tmp"
    }

    catch {set tmpdir $::env(TMP)}
    catch {set tmpdir $::env(TEMP)}
    catch {set tmpdir $::env(TMPDIR)}

    return $tmpdir
}

set LTRACE_TEMP_DIR [LtraceTmpDir]

# LtraceTempFile --
#
#	Create a temporary file according to a pattern, and return its
#	name.  This behaves similar to mktemp.  We don't use mktemp
#	directly, because on older systems, mktemp requires that the
#	array of X's be at the very end of the string, while ltrace
#	temporary files need to have suffixes.
#
# Arguments:
#	pat	Pattern to use.  See mktemp for description of its format.
#
# Results:
#	Creates the temporary file and returns its name.  The name is
#	also appended to LTRACE_TEMP_FILES.

proc LtraceTempFile {pat} {
    global LTRACE_TEMP_FILES
    global LTRACE_TEMP_DIR

    set letters "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    set numLetters [string length $letters]

    if {![regexp -indices {(X{3,})} $pat m]} {
	send_error -- "Pattern $pat contains insufficient number of X's."
	return {}
    }

    set start [lindex $m 0]
    set end [lindex $m 1]
    set len [expr {$end - $start + 1}]

    for {set j 0} {$j < 10} {incr j} {

	# First, generate a random name.

	set randstr {}
	for {set i 0} {$i < $len} {incr i} {
	    set r [expr {int(rand() * $numLetters)}]
	    append randstr [string index $letters $r]
	}
	set prefix [string range $pat 0 [expr {$start - 1}]]
	set suffix [string range $pat [expr {$end + 1}] end]
	set name [file join $LTRACE_TEMP_DIR "$prefix$randstr$suffix"]

	# Now check that it's free.  This is of course racy, but this
	# is a test suite, not anything used in actual production.

	if {[file exists $name]} {
	    continue
	}

	# We don't bother attempting to open the file.  Downstream
	# code can do it itself.

	lappend LTRACE_TEMP_FILES $name
	return $name
    }

    send_error -- "Couldn't create a temporary file for pattern $pat."
    return
}

# ltraceNamedSource --
#
#	Create a file named FILENAME, and prime it with TEXT.  If
#	REMEMBERTEMP, add the file into LTRACE_TEMP_FILES, so that
#	ltraceDone (or rather WipeFiles) erases it later.
#
# Arguments:
#	filename	Name of the file to create.
#
#	text	Contents of the new file.
#
#	rememberTemp	Whether to add filename to LTRACE_TEMP_FILES.
#
# Results:
#	Returns $filename, which now refers to a file with contents
#	given by TEXT.

proc ltraceNamedSource {filename text {rememberTemp 1}} {
    global LTRACE_TEMP_FILES

    set chan [open $filename w]
    puts $chan $text
    close $chan

    if $rememberTemp {
	lappend LTRACE_TEMP_FILES $filename
    }

    return $filename
}

# ltraceSource --
#
#	Create a temporary file with a given suffix and prime it with
#	contents given in text.
#
# Arguments:
#	suffix	Suffix of the temp file to be created.
#
#	text	Contents of the new file.
#
# Results:
#	Returns file name of created file.

proc ltraceSource {suffix text} {
    return [ltraceNamedSource \
		[LtraceTempFile "lt-XXXXXXXXXX.$suffix"] $text 0]
}

# ltraceDir --
#
#	Create a temporary directory.
#
# Arguments:
#
# Results:
#	Returns name of created directory.

proc ltraceDir {} {
    set ret [LtraceTempFile "lt-XXXXXXXXXX.dir"]
    file mkdir $ret
    return $ret
}

# LtraceCompileObjects --
#
#	Compile each source file into an object file.  ltrace_compile
#	is called to perform actual compilation.
#
# Arguments:
#	sources	List of source files.
#
#	options	Options for ltrace_compile.
#
#	retName Variable where the resulting list of object names is
#		to be placed.
# Results:
#	Returns true or false depending on whether there were any
#	errors.  If it returns true, then variable referenced by
#	retName contains list of object files, produced by compiling
#	files in sources list.

proc LtraceCompileObjects {sources options retName} {
    global LTRACE_TEMP_FILES
    upvar $retName ret
    set ret {}

    foreach source $sources {
	set sourcebase [file tail $source]
	set dest $source.o
	lappend LTRACE_TEMP_FILES $dest
	verbose "LtraceCompileObjects: $source -> $dest"
	if {[ltrace_compile $source $dest object $options] != ""} {
	    return false
	}
	lappend ret $dest
    }

    return true
}

# ltraceCompile --
#
#	This attempts to compile a binary from sources given in ARGS.
#
# Arguments:
#	dest	A binary to be produced.  If this is called lib*.so, then
#		the resulting binary will be a library, if *.pie, it
#		will be a PIE, otherwise it will be an executable.  In
#		theory this could also be *.o for "object" and *.i for
#		"preprocess" for cases with one source file, but that
#		is not supported at the moment.  The binary will be
#		placed in $objdir/$subdir.
#
#	args	List of options and source files.
#
#		Options are arguments that start with a dash.  Options
#		(sans the dash) are passed to ltrace_compile.
#
#		Source files named lib*.so are libraries.  Those are
#		passed to ltrace_compile as options shlib=X.  Source
#		files named *.o are objects.  The remaining source
#		files are first compiled (by LtraceCompileObjects) and
#		then together with other objects passed to
#		ltrace_compile to produce resulting binary.
#
#		Any argument that is empty string prompts the function
#		to fail.  This is done so that errors caused by
#		ltraceSource (or similar) distribute naturally
#		upwards.
#
# Results:
#	This compiles given source files into a binary.  Full file name
#	of that binary is returned.  Empty string is returned in case
#	of a failure.

proc ltraceCompile {dest args} {
    global objdir
    global subdir

    get_compiler_info {} c
    get_compiler_info {} c++

    if {[string match "lib*.so" $dest]} {
	set type "library"
	set extraObjOptions "additional_flags=-fpic"
	set extraOptions "additional_flags=-shared"
    } elseif {[string match "*.pie" $dest]} {
	set type "executable"
	set extraObjOptions "additional_flags=-fpic"
	set extraOptions "additional_flags=-pie"
    } else {
	set type "executable"
	set extraObjOptions {}
	set extraOptions {}
    }

    set options {}
    set sources {}
    set objects {}
    foreach a $args {
	if {[string match "-l*" $a]} {
	    lappend options "shlib=$a"
	} elseif {[string match "-?*" $a]} {
	    lappend options [string range $a 1 end]
	} elseif {[string match "*.so" $a]} {
	    lappend options "shlib=$a"
	} elseif {[string match "*.o" $a]} {
	    lappend objects $a
	} else {
	    lappend sources $a
	}
    }

    if {[string equal $dest {}]} {
	set dest [LtraceTempFile "exe-XXXXXXXXXX"]
    } elseif {[string equal $dest ".pie"]} {
	set dest [LtraceTempFile "pie-XXXXXXXXXX"]
    } else {
	set dest $objdir/$subdir/$dest
    }

    verbose "ltraceCompile: dest $dest"
    verbose "             : options $options"
    verbose "             : sources $sources"
    verbose "             : objects $objects"

    if {![LtraceCompileObjects $sources \
	      [concat $options $extraObjOptions] newObjects]} {
	return {}
    }
    set objects [concat $objects $newObjects]

    verbose "ltraceCompile: objects $objects"

    if {[ltrace_compile $objects $dest $type \
	     [concat $options $extraOptions]] != ""} {
	return {}
    }

    return $dest
}

# ltraceRun --
#
#	Invoke command identified by LTRACE global variable with given
#	ARGS.  A logfile redirection is automatically ordered by
#	passing -o and a temporary file name.
#
# Arguments:
#	args	Arguments to ltrace binary.
#
# Results:
#	Returns name of logfile.  The "exec" command that it uses
#	under the hood fails loudly if the process exits with a
#	non-zero exit status, or uses stderr in any way.

proc ltraceRun {args} {
    global LTRACE
    global objdir
    global subdir

    set LdPath [ld_library_path $objdir/$subdir]
    set logfile [ltraceSource ltrace {}]

    # Run ltrace.  expect will show an error if this doesn't exit with
    # zero exit status (i.e. ltrace fails, valgrind finds errors,
    # etc.).

    set command "exec env LD_LIBRARY_PATH=$LdPath $LTRACE -o $logfile $args"
    verbose $command
    if {[catch {eval $command}] } {
	fail "test case execution failed"
	send_error -- $command
	send_error -- $::errorInfo
    }

    return $logfile
}

# ltraceDone --
#
#	Wipes or dumps all temporary files after a test suite has
#	finished.
#
# Results:
#	Doesn't return anything.  Wipes all files gathered in
#	LTRACE_TEMP_FILES.  If SAVE_TEMPS is defined and true, the
#	temporary files are not wiped, but their names are dumped
#	instead.  Contents of LTRACE_TEMP_FILES are deleted in any
#	case.

proc ltraceDone {} {
    global SAVE_TEMPS
    global LTRACE_TEMP_FILES

    if {[info exists SAVE_TEMPS] && $SAVE_TEMPS} {
	foreach tmp $LTRACE_TEMP_FILES {
	    send_user "$tmp\n"
	}
    } else {
	WipeFiles $LTRACE_TEMP_FILES
    }

    set LTRACE_TEMP_FILES {}
    return
}

# Grep --
#
#	Return number of lines in a given file, matching a given
#	regular expression.
#
# Arguments:
#	logfile	File to search through.
#
#	re	Regular expression to match.
#
# Results:
#	Returns number of matching lines.

proc Grep {logfile re} {
    set count 0
    set fp [open $logfile]
    while {[gets $fp line] >= 0} {
	if [regexp -- $re $line] {
	    incr count
	}
    }
    close $fp
    return $count
}

# ltraceMatch1 --
#
#	Look for a pattern in a given logfile, comparing number of
#	occurences of the pattern with expectation.
#
# Arguments:
#	logfile	The name of file where to look for patterns.
#
#	pattern	Regular expression pattern to look for.
#
#	op	Operator to compare number of occurences.
#
#	expect	Second operand to op, the first being number of
#		occurences of pattern.
#
# Results:
#	Doesn't return anything, but calls fail or pass depending on
#	whether the patterns matches expectation.

proc ltraceMatch1 {logfile pattern {op ==} {expect 1}} {
    set count [Grep $logfile $pattern]
    set msgMain "$pattern appears in $logfile $count times"
    set msgExpect ", expected $op $expect"

    if {[eval expr $count $op $expect]} {
	pass $msgMain
    } else {
	fail $msgMain$msgExpect
    }
    return
}

# ltraceMatch --
#
#	Look for series of patterns in a given logfile, comparing
#	number of occurences of each pattern with expectations.
#
# Arguments:
#	logfile	The name of file where to look for patterns.
#
#	patterns List of patterns to look for.  ltraceMatch1 is called
#		on each of these in turn.
#
# Results:
#
#	Doesn't return anything, but calls fail or pass depending on
#	whether each of the patterns holds.

proc ltraceMatch {logfile patterns} {
    foreach pat $patterns {
	eval ltraceMatch1 [linsert $pat 0 $logfile]
    }
    return
}

# ltraceLibTest --
#
#	Generate a binary, a library (liblib.so) and a config file.
#	Run the binary using ltraceRun, passing it -F to load the
#	config file.
#
# Arguments:
#	conf	Contents of ltrace config file.
#
#	cdecl	Contents of header file.
#
#	libcode	Contents of library implementation file.
#
#	maincode	Contents of function "main".
#
#	params	Additional parameters to pass to ltraceRun.
#
# Results:
#
#	Returns whatever ltraceRun returns.

proc ltraceLibTest {conf cdecl libcode maincode {params ""}} {
    set conffile [ltraceSource conf $conf]
    set lib [ltraceCompile liblib.so [ltraceSource c [concat $cdecl $libcode]]]
    set bin [ltraceCompile {} $lib \
		 [ltraceSource c \
		      [concat $cdecl "int main(void) {" $maincode "}"]]]

    return [eval [concat "ltraceRun -F $conffile " $params "-- $bin"]]
}

#
# ltrace_options OPTIONS_LIST
# Pass ltrace commandline options.
# 
proc ltrace_options { args } {
	
	global LTRACE_OPTIONS
	set LTRACE_OPTIONS $args
}

#
# ltrace_args ARGS_LIST
# Pass ltrace'd program its own commandline options.
#
proc ltrace_args { args } {

	global LTRACE_ARGS
	set LTRACE_ARGS $args
}

#
# handle run-time library paths
#
proc ld_library_path { args } {

	set ALL_LIBRARY_PATHS { }
	if [info exists LD_LIBRARY_PATH] {
		lappend ALL_LIBRARY_PATHS $LD_LIBRARY_PATH
	}
	global libelf_LD_LIBRARY_PATH
	if {[string length $libelf_LD_LIBRARY_PATH] > 0} {
		lappend ALL_LIBRARY_PATHS $libelf_LD_LIBRARY_PATH
	}
	global elfutils_LD_LIBRARY_PATH
	if {[string length $elfutils_LD_LIBRARY_PATH] > 0} {
		lappend ALL_LIBRARY_PATHS $elfutils_LD_LIBRARY_PATH
	}
	global libunwind_LD_LIBRARY_PATH
	if {[string length $libunwind_LD_LIBRARY_PATH] > 0} {
		lappend ALL_LIBRARY_PATHS $libunwind_LD_LIBRARY_PATH
	}
	lappend ALL_LIBRARY_PATHS $args
	join $ALL_LIBRARY_PATHS ":"
}

#
# ltrace_runtest LD_LIBRARY_PATH BIN FILE
# Trace the execution of BIN and return result.
#
# BIN is program-under-test.
# LD_LIBRARY_PATH is the env for program-under-test to run.
# FILE is to save the output from ltrace with default name $BIN.ltrace.
# Retrun output from ltrace.
#
proc ltrace_runtest { args } {

	global LTRACE
	global LTRACE_OPTIONS
	global LTRACE_ARGS

	verbose "LTRACE = $LTRACE"
	
	set LD_LIBRARY_PATH_ [ld_library_path [lindex $args 0]]
	set BIN [lindex $args 1]

	# specify the output file, the default one is $BIN.ltrace
	if [llength $args]==3 then {
		set file [lindex $args 2]
	} else {
		set file $BIN.ltrace
	}

	# Remove the file first.  If ltrace fails to overwrite it, we
	# would be comparing output to an obsolete run.
	exec rm -f $file

	# append this option to LTRACE_OPTIONS.
	lappend LTRACE_OPTIONS "-o"
	lappend LTRACE_OPTIONS "$file"
	verbose "LTRACE_OPTIONS = $LTRACE_OPTIONS"
	set command "exec sh -c {export LD_LIBRARY_PATH=$LD_LIBRARY_PATH_; \
		$LTRACE $LTRACE_OPTIONS $BIN $LTRACE_ARGS;exit}"
	#ltrace the PUT.
	if {[catch $command output]} {
	    fail "test case execution failed"
	    send_error -- $command
	    send_error -- $::errorInfo
	}

	# return output from ltrace.
	return $output
}

#
# ltrace_verify_output FILE_TO_SEARCH PATTERN MAX_LINE
# Verify the ltrace output by comparing the number of PATTERN in 
# FILE_TO_SEARCH with INSTANCE_NO.  Do not specify INSTANCE_NO if
# instance number is ignored in this test.
# Reutrn:
#      0 = number of PATTERN in FILE_TO_SEARCH inqual to INSTANCE_NO.
#      1 = number of PATTERN in FILE_TO_SEARCH qual to INSTANCE_NO.
#
proc ltrace_verify_output { file_to_search pattern {instance_no 0} {grep_command "grep"}} {

	# compute the number of PATTERN in FILE_TO_SEARCH by grep and wc.
	catch "exec sh -c {$grep_command \"$pattern\" $file_to_search | wc -l ;exit}" output
	verbose "output = $output"

	if [ regexp "syntax error" $output ] then {
		fail "Invalid regular expression $pattern"
        } elseif { $instance_no == 0 } then {
		if { $output == 0 } then {
			fail "Fail to find $pattern in $file_to_search"
		} else {
			pass "$pattern in $file_to_search"
		}
	} elseif { $output >= $instance_no } then {
		pass "$pattern in $file_to_search for $output times"
	} else {
		fail "$pattern in $file_to_search for $output times, should be $instance_no"
	}
}