#! /usr/bin/perl -W
#
# Run package tests using Fiasco-UX
#
#
#  Adam Lackorzynski <adam@os.inf.tu-dresden.de>
#  Ronald Aigner <ra3@os.inf.tu-dresden.de>
#
# This script is derived from the autocheck script which can be found in
# kernel/fiasco/tools. It has been slightly modified to suit the demands of
# the package tests.
#

# make it pass for now
#exit 0;

use strict;
use Getopt::Long;

my $FIASCOUX;
my $FIASCOUX_PARAM;
my $L4DIR;
my $OBJ_BASE;
my $OBJDIR = 'OBJ-x86_586-l4v2';
my $EXPECTED_OUT = 'expected.txt';
my $TMP_OUT = 'tmp_out.txt';
my $PKGNAME;
my $COMPARE_CMD = 'diff -u $EXPECTED_OUT $TMP_OUT';

my $ALARM_TIMEOUT = 60; # in seconds

my %templs = (
  roottask => '%s/pkg/roottask/server/src/%s/roottask',
  sigma0   => '%s/pkg/sigma0/server/src/%s/sigma0',
);

my $Verbose  = 0;
my $Quiet    = 0;
my $Generate = 0;
my $Memory   = 32;
my $Plainrun = 0;
my $No_fb    = 0;
my $Use_Symbols = 0;
my $Use_Lines = 0;

my %progs;
my %results;
my %output;
my @Baseservers;
my $Client;
my $Server;
my $Fiasco_Dir;

# filehandle for fiasco output
my $Input;
my $Output;

my $Exit_code = 0;
my $pid = 0;

my $FILTER_LEVEL_FIASCO = 0;
my $FILTER_LEVEL_ROOTTASK = 1;
my $FILTER_LEVEL_USER = 2;
my $Filter_state = $FILTER_LEVEL_FIASCO;
my $Filter_level = $FILTER_LEVEL_USER;

sub usage() {
  print <<EOU;
$0 [options] -c <client> -s <server>

 --l4dir, -l path         Path to an L4 directory
 --fiascoux, -f file      Path to the Fiasco-UX binary
 --fiascoux_param, -p params Parameters for Fiasco-UX
 --memory, -m megabyte    Number of Megabytes used by Fiasco. (default: 32)
 --objdir, -O objdir      Object dir, currently: $OBJDIR
 --verbose, -v            Be verbose (e.g. show output of L4 apps)
 --quiet, -q              Tell nothing, just set the exit code
 --generate               Generate the output file instead of comparing to it
 --server, -s file        The server to be tested
 --client, -c file        The file to run as test-application
 --base, -b file          Additional base servers required by the application
                          Can be a comma seperated list. (Only specify the
			  binary names!) (default: log, names, dm_phys)
 --timeout, -t timeout    Time in seconds to wait before shooting down a 
                          runaway fiasco. (default: 60)
 --expectfail		  Expect failure. Return 0 on failure, 1 on success.

 Environment variables:
  L4DIR                   Path to an L4 directory
  OBJ_BASE		  Path to build directory
  FIASCOUX                Path to the Fiasco-UX binary
  EXPECTED_OUT            Filename of the file containing the expected output
  TMP_OUT                 Filename of the tempfile containing Fiasco's output
  COMPARE_CMD		  Command to compare output and expected output

 Notes:
  Environment variables override options!
EOU
}

##
# Check if L4DIR looks like an L4 directory
sub check_for_l4dir() {
  unless (-d "$L4DIR/pkg/l4sys/include" ||
          -d "$L4DIR/../kernel/fiasco/src/kern/ux") {
    die "$L4DIR doesn't look like an L4 directory!";
  }
}

##
# Just check if the supplied binary in $FIASCOUX is really
# a Fiasco UX version. Native versions will just segfault.
sub check_for_fiasco_ux() {

  unless ((-x $FIASCOUX && -f $FIASCOUX) ||
          (-l $FIASCOUX && -x readlink $FIASCOUX && -f readlink $FIASCOUX)) {
    die "$FIASCOUX: Does not exist or isn't an executable file";
  }
  
  system("$FIASCOUX -h >/dev/null 2>&1");
  die "$FIASCOUX doesn't seem to be a UX version." if $?;

  $FIASCOUX =~ /(.*)\/([^\/]*)$/;
  $Fiasco_Dir = $1;
}

# check for user app in binary path of L4
sub check_user_app($)
{
  my $app_with_args = shift;

  # extract arch and api from OBJ-dir
  $OBJDIR =~ /OBJ-([^-]*)-(.*)/;
  my $arch = $1;
  my $api = $2;

  my @check = split(/\s+/, $app_with_args);
  my $app = shift @check;

  my $p = sprintf "%s/bin/%s/%s/%s", $OBJ_BASE, $arch, $api, $app;
  die "There's no $p!" unless -f $p;

  return $p.'\\ '.join('\\ ', @check) if @check;
  $p;
}

##
# Check for userland (roottask, sigma0, ...)
sub check_for_userland() {
  foreach my $t (keys %templs) {
    my $p = sprintf $templs{$t}, $OBJ_BASE, $OBJDIR;
    die "There's no $p!" unless -f $p;
    $progs{$t} = $p;
  }

  # check for base-servers
  my @servers = @Baseservers;
  @Baseservers = {};
  if (scalar(@servers) eq 0) {
    @servers = ( "log", "names", "dm_phys" );
  }
  
  # iterate over servers and test if they exist in the bin dir
  foreach my $s (@servers) {
    push @Baseservers, check_user_app($s);
  }

  # test client and server
  push @Baseservers, check_user_app($Client) if defined($Client);
  push @Baseservers, check_user_app($Server);
}

##
# Does the comparison of the output
sub run_diff {
  print STDERR "Running compare command \"$COMPARE_CMD\".\n" if $Verbose;
  open(TMP, "$COMPARE_CMD |") or die "Cannot run diff: $!";
  my @diff = <TMP>;
  close TMP;
  
  # reset console in interactive mode
  system "if tty -s; then stty echo icrnl icanon; fi";
  
  # check if there are differences between expected and generated output
  if (@diff == 0) {
    print STDERR "Output of test in $PKGNAME ok.\n";
    return $Exit_code;
  }

  print STDERR "Test in $PKGNAME generated unexpected output:\n";
  @diff = splice @diff, 1000 if @diff > 1000;
  print STDERR @diff, "\n";
  return 1 - $Exit_code;
}

##
# Called if second alarm signal received
#
# Now all the output of Fiasco-UX should be through and we can savely kill
# Fiasco-UX. We then check the generated output and terminate ourselves.
sub got_sig_alarm_second {
  print STDERR "Timeout for flushing Fiasco-UX output\n" if $Verbose;
  print STDERR "Sending SIGKILL to $pid and diff output\n" if $Verbose;

  kill KILL => $pid;
  alarm 0;

  # some sanity checks
  if ( $Filter_state < $FILTER_LEVEL_USER ) {
    print $Output "\n";
    print $Output "User tasks did not start. Maybe overlapping modules?\n";
    print $Output "Run \"" . fiascoux_cmdline() . "\" manually to check.\n";
  }
  close $Output;

  # in generate mode, simply return
  if ($Generate) {
    system "stty echo";
    exit 0;
  }
  
  exit run_diff();
}

##
# Called if first alarm signal received
#
# To flush Fiasco Output we send it a SIGINT (^C). Then we set up a second
# timeout and return, so the filter can process the output which we forced to
# be flushed.
sub got_sig_alarm_first {
  print STDERR "Timeout for Fiasco-UX hit!\n" if $Verbose;
  print STDERR "Sending SIGINT to $pid\n" if $Verbose;
  
  kill INT => $pid;
  $SIG{ALRM} = \&got_sig_alarm_second;
  alarm 2; # time to flush output

  # return to keep on filtering the output of Fiasco
}

##
# Runs the timer and kills fiasco if runaway
#
# - sets the signal handler
# - initializes the timeout
sub set_alarm {
  $SIG{ALRM} = \&got_sig_alarm_first;
  alarm $ALARM_TIMEOUT;
  print STDERR "Set alarm to $ALARM_TIMEOUT seconds\n" if $Verbose;
}

##
# Build the fiasco command line
#
# Adds the binaries with the appropriate parameters.
sub fiascoux_cmdline() {
  (my $p = $FIASCOUX) =~ s/\/[^\/]+$//;
  my $cmdline = "$FIASCOUX";
  $cmdline .= " $FIASCOUX_PARAM" if defined $FIASCOUX_PARAM;
  $cmdline .= " -symbols $Fiasco_Dir/Symbols" if $Use_Symbols;
  $cmdline .= " -lines $Fiasco_Dir/Lines" if $Use_Lines;
  $cmdline .= " -m $Memory";
  $cmdline .= " -R $progs{roottask}"; # -quiet";
  $cmdline .= "\"" if $Use_Symbols || $Use_Lines;
  $cmdline .= " -symbols" if $Use_Symbols;
  $cmdline .= " -lines" if $Use_Lines;
  $cmdline .= "\"" if $Use_Symbols || $Use_Lines;
  $cmdline .= " -S $progs{sigma0}"; # --quiet";
  # when we change this to not cd into Fiasco dir, then
  # prepend $p to $irq0
  $cmdline .= " -I ".$p."/irq0";
  if ($No_fb eq 1) {
    $cmdline .= " -F /bin/true";
  } else {
    $cmdline .= " -F ".$p."/con_ux";
  }
  # add -l to base-servers, which already contains client and server
  for my $s (@Baseservers) {
    if (ref($s) ne "HASH") { # just checking for Hashes that smuggled in
      $cmdline .= " -l ".$s;
    }
  }
  $cmdline;
}

##
# filters the output of fiasco
#
# We want to see everything once roottask are finished loading.
# Therefore, we need some state machine to skip everything before and while
# roottask is running.
sub filter_fiasco_out
{
  $_ = shift;

  # filter escape sequences
  s/[[:cntrl:]]\[(\d;)?\d{0,2}m//g;
  s/
//g;
  s/[[:cntrl:]]\[K//g;
  s/[[:cntrl:]]\[\d+;\d+[Hfr]//g;

  # check if we have to change state
  if (/^Roottask: Loading \d+ modules\./ &&
      $Filter_state eq $FILTER_LEVEL_FIASCO) {
    $Filter_state = $FILTER_LEVEL_ROOTTASK;
    print STDERR "Changed state to Roottask-Output\n" if $Verbose;
  }
  if ( /^$/ && $Filter_state eq $FILTER_LEVEL_ROOTTASK) {
    $Filter_state = $FILTER_LEVEL_USER;
    print STDERR "Changed state to Userland-Output\n" if $Verbose;
  }

  # skip everything before and from roottask
  return undef if $Filter_state < $Filter_level;

  # if we are not supposed to filter anything, then return the line
  return $_ if $Filter_level eq $FILTER_LEVEL_FIASCO;

  # filter empty lines
  return undef if /^$/;

  # filter JDB warning and prompt
  return undef if /^Terminal probably too small, should be at least/;
  return undef if /^\([a-f0-9]\.\d\d\) jdb:/;
  return undef if /^--.*ESP:.*EIP:.*/;

  # filter memory dump
  return undef if /^([a-f0-9]){8}:/;
  
  $_;
}

##
# call_test
sub run_fiasco {
  my $cmdline = fiascoux_cmdline();
  print "Calling: $cmdline\n" if $Verbose;

  $pid = open($Input, "$cmdline|");
  die "Can't start Fiasco: $!" unless defined $pid;
  print "Run Fiasco-UX in $pid\n" if $Verbose;
  
  # if in generate mode, we redirect output to EXPECTED_OUT
  # otherwise we redirect stdout to the TMP_OUT file
  my $filename = $Generate ? $EXPECTED_OUT : $TMP_OUT;
  open($Output, ">$filename") || die "Cannot open output file $filename";
  my $oldfh = select($Output); $| = 1; select($oldfh);
  print STDERR "Opened $filename, now setting timer\n" if $Verbose;

  # the parent sets up the timer (it will eventually call the run_diff sub)
  set_alarm();

  my $o;
  while (<$Input>) {
    # we have to strip some lines
    $o = filter_fiasco_out($_);
    next unless defined $o;
    print $Output $o;
  }
  print STDERR "Fiasco terminated.\n" if $Verbose;
  close $Input;
  close $Output;
  # when we drop out of this loop fiasco terminated
  alarm 0;

  # in generate mode, simply return
  exit 0 if $Generate;
  
  exit run_diff();
}

##
# Plain run of Fiasco UX
#
# Not timer, no filter, simply run UX
sub run_plain_fiasco {
  my $cmdline = fiascoux_cmdline();
  print "Running UX: \"$cmdline\"\n";
  exec($cmdline) or die "Can't start Fiasco: $!";
  exit 1;
}
 

# -------------------------------------------------------------

unless (GetOptions("help|h", sub { usage(); exit(0); },
                   "l4dir|l=s", \$L4DIR,
		   "builddir=s", \$OBJ_BASE,
		   "fiascoux|f=s", \$FIASCOUX,
		   "fiascoux_param|p=s", \$FIASCOUX_PARAM,
		   "memory|m=s", \$Memory,
		   "objdir|O=s", \$OBJDIR,
		   "verbose|v!", \$Verbose,
		   "quiet|q!", \$Quiet,
		   "generate!", \$Generate,
		   "expectfail!", \$Exit_code,
		   "client|c=s", \$Client,
		   "server|s=s", \$Server,
		   "base|b=s", \@Baseservers,
		   "timeout|t=s", \$ALARM_TIMEOUT,
		   "plainrun!", \$Plainrun,
		   "filterlevel=i", \$Filter_level,
		   "nofb!", \$No_fb,
		   "symbols!", \$Use_Symbols,
		   "lines!", \$Use_Lines,
		   )) {
  usage();
  exit(1);
}

@Baseservers = split(/,/,join(',',@Baseservers));

$L4DIR = $ENV{L4DIR}       || die "Need an L4DIR set!" unless $L4DIR;
$OBJ_BASE = $ENV{OBJ_BASE} || die "Need an builddir (OBJ_BASE) set!" unless $OBJ_BASE;
$FIASCOUX = $ENV{FIASCOUX} || die "Need a Fiasco-UX path!" unless $FIASCOUX;
if ($ENV{EXPECTED_OUT}) { $EXPECTED_OUT = $ENV{EXPECTED_OUT}; }
die "Need filename of expected output!" unless $EXPECTED_OUT;
if ($ENV{TMP_OUT}) { $TMP_OUT = $ENV{TMP_OUT}; }
die "No valid temporary file set!" unless $TMP_OUT;
$PKGNAME = $ENV{PKGNAME} || $Server 
	|| die "No package-name set!" unless $PKGNAME;
if ($ENV{COMPARE_CMD}) { $COMPARE_CMD = $ENV{COMPARE_CMD}; }
die "No compare command set!" unless $COMPARE_CMD;

check_for_l4dir();
check_for_fiasco_ux();
check_for_userland();

##
# The package test script:
# This script forks off Fiasco and sets up a timeout. When the timeout
# strikes, it sends a SIGINT to Fiasco to force a flush of the output buffer.
# Then it sends a SIGKILL to terminate Fiasco.
print "Main called by $$.\n" if $Verbose;

# if plainrun, start run_plain_fiasco (it never returns)
run_plain_fiasco() if $Plainrun;

# this function forks off fiasco
run_fiasco();

# when we reach this point, something terribly went wrong
die "Oops, error in script!\n";
