4.78 -> 4.80
[exim.git] / test / runtest
index 6a4fb2b51f3c072d4f65373f7e53e81db0494a50..9bcace082f332feaa067326812f92edf9e3ecd88 100755 (executable)
@@ -1,7 +1,5 @@
 #! /usr/bin/perl -w
 
-# $Cambridge: exim/test/runtest,v 1.28 2007/07/04 10:37:04 ph10 Exp $
-
 ###############################################################################
 # This is the controlling script for the "new" test suite for Exim. It should #
 # be possible to export this suite for running on a wide variety of hosts, in #
 # Placed in the Exim CVS: 06 February 2006                                    #
 ###############################################################################
 
+#use strict;
 require Cwd;
 use Errno;
 use FileHandle;
 use Socket;
+use Time::Local;
 
 
 # Start by initializing some global variables
 
-$testversion = "4.68 (16-Apr-07)";
+$testversion = "4.80 (08-May-12)";
+
+# This gets embedded in the D-H params filename, and the value comes
+# from asking GnuTLS for "normal", but there appears to be no way to
+# use certtool/... to ask what that value currently is.  *sigh*
+# This value is correct as of GnuTLS 2.12.18.
+#
+$gnutls_dh_bits_normal = 2432;
 
-$cf = "bin/cf";
+$cf = "bin/cf -exact";
 $cr = "\r";
 $debug = 0;
 $force_update = 0;
@@ -120,6 +127,12 @@ s?\bV6NET:?$parm_ipv6_test_net:?g;
 }
 
 
+##################################################
+#     Any state to be preserved across tests     #
+##################################################
+
+my $TEST_STATE = {};
+
 
 ##################################################
 #        Subroutine to tidy up and exit          #
@@ -142,6 +155,13 @@ my($spool);
 # than SIGTERM to stop it outputting "Terminated" to the terminal when not in
 # the background.
 
+if (exists $TEST_STATE->{exim_pid})
+  {
+  $pid = $TEST_STATE->{exim_pid};
+  print "Tidyup: killing wait-mode daemon pid=$pid\n";
+  system("sudo kill -SIGINT $pid");
+  }
+
 if (opendir(DIR, "spool"))
   {
   my(@spools) = sort readdir(DIR);
@@ -168,6 +188,8 @@ system("sudo /bin/rm -rf ./spool test-* ./dnszones/*")
   if ($rc == 0 && !$save_output);
 
 system("sudo /bin/rm -rf ./eximdir/*");
+
+print "\nYou were in test $test at the end there.\n\n" if defined $test;
 exit $rc if ($rc >= 0);
 die "** runtest error: $_[1]\n";
 }
@@ -198,14 +220,26 @@ return $newid;
 }
 
 
-# This is used while munging the output from exim_dumpdb. We cheat by assuming
-# that the  date always the same, and just return the number of seconds since
-# midnight.
+# This is used while munging the output from exim_dumpdb.
+# May go wrong across DST changes.
 
 sub date_seconds {
 my($day,$month,$year,$hour,$min,$sec) =
   $_[0] =~ /^(\d\d)-(\w\w\w)-(\d{4})\s(\d\d):(\d\d):(\d\d)/;
-return $hour * 60 * 60 + $min * 60 + $sec;
+my($mon);
+if   ($month =~ /Jan/) {$mon = 0;}
+elsif($month =~ /Feb/) {$mon = 1;}
+elsif($month =~ /Mar/) {$mon = 2;}
+elsif($month =~ /Apr/) {$mon = 3;}
+elsif($month =~ /May/) {$mon = 4;}
+elsif($month =~ /Jun/) {$mon = 5;}
+elsif($month =~ /Jul/) {$mon = 6;}
+elsif($month =~ /Aug/) {$mon = 7;}
+elsif($month =~ /Sep/) {$mon = 8;}
+elsif($month =~ /Oct/) {$mon = 9;}
+elsif($month =~ /Nov/) {$mon = 10;}
+elsif($month =~ /Dec/) {$mon = 11;}
+return timelocal($sec,$min,$hour,$day,$mon,$year);
 }
 
 
@@ -299,6 +333,7 @@ $spid = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
 
 while(<IN>)
   {
+RESET_AFTER_EXTRA_LINE_READ:
   # Check for "*** truncated ***"
   $yield = 1 if /\*\*\* truncated \*\*\*/;
 
@@ -312,7 +347,8 @@ while(<IN>)
   s?\Q$parm_cwd\E?TESTSUITE?g;
 
   # Replace the Exim version number (may appear in various places)
-  s/Exim \d+\.\d+[\w-]*/Exim x.yz/i;
+  # patchexim should have fixed this for us
+  #s/(Exim) \d+\.\d+[\w_-]*/$1 x.yz/i;
 
   # Replace Exim message ids by a unique series
   s/((?:[^\W_]{6}-){2}[^\W_]{2})
@@ -339,6 +375,9 @@ while(<IN>)
   s/^\d+:error:/pppp:error:/;
   s/:(?:\/[^\s:]+\/)?([^\/\s]+\.c):\d+:/:$1:dddd:/;
 
+  # There are differences in error messages between OpenSSL versions
+  s/SSL_CTX_set_cipher_list/SSL_connect/;
+
   # One error test in expansions mentions base 62 or 36
   s/is not a base (36|62) number/is not a base 36\/62 number/;
 
@@ -438,7 +477,24 @@ while(<IN>)
     \d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d/Exim statistics from <time> to <time>/x;
 
 
-  # ======== Caller's login, uid, gid, home ========
+  # ======== TLS certificate algorithms ========
+  # Test machines might have various different TLS library versions supporting
+  # different protocols; can't rely upon TLS 1.2's AES256-GCM-SHA384, so we
+  # treat the standard algorithms the same.
+  # So far, have seen:
+  #   TLSv1:AES256-SHA:256
+  #   TLSv1.2:AES256-GCM-SHA384:256
+  # We also need to handle the ciphersuite without the TLS part present, for
+  # client-ssl's output.  We also see some older forced ciphersuites, but
+  # negotiating TLS 1.2 instead of 1.0.
+  # Mail headers (...), log-lines X=..., client-ssl output ...
+  # (and \b doesn't match between ' ' and '(' )
+
+  s/( (?: (?:\b|\s) [\(=] ) | \s )TLSv1\.2:/$1TLSv1:/xg;
+  s/\bAES256-GCM-SHA384\b/AES256-SHA/g;
+
+
+  # ======== Caller's login, uid, gid, home, gecos ========
 
   s/\Q$parm_caller_home\E/CALLER_HOME/g;   # NOTE: these must be done
   s/\b\Q$parm_caller\E\b/CALLER/g;         #       in this order!
@@ -450,6 +506,8 @@ while(<IN>)
   s/\buid=$parm_caller_uid\b/uid=CALLER_UID/g;
   s/\bgid=$parm_caller_gid\b/gid=CALLER_GID/g;
 
+  s/\bname=$parm_caller_gecos\b/name=CALLER_GECOS/g;
+
   # When looking at spool files with -Mvh, we will find not only the caller
   # login, but also the uid and gid. It seems that $) in some Perls gives all
   # the auxiliary gids as well, so don't bother checking for that.
@@ -545,6 +603,9 @@ while(<IN>)
   # The amount of space between "host" and the address in verification output
   # depends on the length of the host name. We therefore reduce it to one space
   # for all of them.
+  # Also, the length of space at the end of the host line is dependent
+  # on the length of the longest line, so strip it also on otherwise
+  # un-rewritten lines like localhost
 
   s/^\s+host\s(\S+)\s+(\S+)/  host $1 $2/;
   s/^\s+(host\s\S+\s\S+)\s+(port=.*)/  host $1 $2/;
@@ -552,9 +613,10 @@ while(<IN>)
   s/host\s\Q$parm_ipv4\E\s\[\Q$parm_ipv4\E\]/host ipv4.ipv4.ipv4.ipv4 [ipv4.ipv4.ipv4.ipv4]/;
   s/host\s\Q$parm_ipv6\E\s\[\Q$parm_ipv6\E\]/host ip6:ip6:ip6:ip6:ip6:ip6:ip6:ip6 [ip6:ip6:ip6:ip6:ip6:ip6:ip6:ip6]/;
   s/\b\Q$parm_ipv4\E\b/ip4.ip4.ip4.ip4/g;
-  s/\b\Q$parm_ipv6\E\b/ip6:ip6:ip6:ip6:ip6:ip6:ip6:ip6/g;
+  s/(^|\W)\K\Q$parm_ipv6\E/ip6:ip6:ip6:ip6:ip6:ip6:ip6:ip6/g;
   s/\b\Q$parm_ipv4r\E\b/ip4-reverse/g;
-  s/\b\Q$parm_ipv6r\E\b/ip6-reverse/g;
+  s/(^|\W)\K\Q$parm_ipv6r\E/ip6-reverse/g;
+  s/^(\s+host\s\S+\s+\[\S+\]) +$/$1 /;
 
 
   # ======== Test network IP addresses ========
@@ -584,7 +646,14 @@ while(<IN>)
 
   # ======== Output from ls ========
   # Different operating systems use different spacing on long output
-  s/ +/ /g if /^[-rwd]{10} /;
+  #s/ +/ /g if /^[-rwd]{10} /;
+  # (Bug 1226) SUSv3 allows a trailing printable char for modified access method control.
+  # Handle only the Gnu and MacOS space, dot, plus and at-sign.  A full [[:graph:]]
+  # unfortunately matches a non-ls linefull of dashes.
+  # Allow the case where we've already picked out the file protection bits.
+  if (s/^([-d](?:[-r][-w][-SsTtx]){3})[.+@]?( +|$)/$1$2/) {
+    s/ +/ /g;
+  }
 
 
   # ======== Message sizes =========
@@ -673,6 +742,12 @@ while(<IN>)
   s/^\d\d\d(?=[PFS*])/ddd/;
 
 
+  # ========= Exim lookups ==================
+  # Lookups have a char which depends on the number of lookup types compiled in,
+  # in stderr output.  Replace with a "0".  Recognising this while avoiding
+  # other output is fragile; perhaps the debug output should be revised instead.
+  s%(?<!sqlite)(?<!lsearch\*@)(?<!lsearch\*)(?<!lsearch)[0-?]TESTSUITE/aux-fixed/%0TESTSUITE/aux-fixed/%g;
+
   # ==========================================================
   # Some munging is specific to the specific file types
 
@@ -720,6 +795,46 @@ while(<IN>)
 
     s/\bgethostbyname2?|\bgetipnodebyname/get[host|ipnode]byname[2]/;
 
+    # drop gnutls version strings
+    next if /GnuTLS compile-time version: \d+[\.\d]+$/;
+    next if /GnuTLS runtime version: \d+[\.\d]+$/;
+
+    # drop openssl version strings
+    next if /OpenSSL compile-time version: OpenSSL \d+[\.\da-z]+/;
+    next if /OpenSSL runtime version: OpenSSL \d+[\.\da-z]+/;
+
+    # drop lookups
+    next if /^Lookups \(built-in\):/;
+    next if /^Loading lookup modules from/;
+    next if /^Loaded \d+ lookup modules/;
+    next if /^Total \d+ lookups/;
+
+    # drop compiler information
+    next if /^Compiler:/;
+
+    # and the ugly bit
+    # different libraries will have different numbers (possibly 0) of follow-up
+    # lines, indenting with more data
+    if (/^Library version:/) {
+      while (1) {
+       $_ = <IN>;
+       next if /^\s/;
+       goto RESET_AFTER_EXTRA_LINE_READ;
+      }
+    }
+
+    # drop other build-time controls emitted for debugging
+    next if /^WHITELIST_D_MACROS:/;
+    next if /^TRUSTED_CONFIG_LIST:/;
+
+    # As of Exim 4.74, we log when a setgid fails; because we invoke Exim
+    # with -be, privileges will have been dropped, so this will always
+    # be the case
+    next if /^changing group to \d+ failed: Operation not permitted/;
+
+    # We invoke Exim with -D, so we hit this new messag as of Exim 4.73:
+    next if /^macros_trusted overridden to true by whitelisting/;
+
     # We have to omit the localhost ::1 address so that all is well in
     # the IPv4-only case.
 
@@ -727,6 +842,9 @@ while(<IN>)
       if (/looked up these IP addresses/);
     next if /name=localhost address=::1/;
 
+    # drop pdkim debugging header
+    next if /^PDKIM <<<<<<<<<<<<<<<<<<<<<<<<<<<<<+$/;
+
     # Various other IPv6 lines must be omitted too
 
     next if /using host_fake_gethostbyname for \S+ \(IPv6\)/;
@@ -875,7 +993,7 @@ if ($_[1]) { $_ = "u"; print "... update forced\n"; }
 #             [4] TRUE if this is a log file whose deliveries must be sorted
 #
 # Returns:    0 comparison succeeded or differences to be ignored
-#             1 comparison failed; files were updated (=> re-compare)
+#             1 comparison failed; files may have been updated (=> re-compare)
 #
 # Does not return if the user replies "Q" to a prompt.
 
@@ -909,7 +1027,7 @@ if (! -e $sf)
       print "\n";
       print "------------ $f -----------\n"
         if (defined $rf && -s $rf && defined $rsf && -s $rsf);
-      system("$more $f");
+      system("$more '$f'");
       }
     }
 
@@ -1023,7 +1141,7 @@ if (-e $sf)
 
   # Do the comparison
 
-  return 0 if (system("$cf $mf $sf >test-cf") == 0);
+  return 0 if (system("$cf '$mf' '$sf' >test-cf") == 0);
 
   # Handle comparison failure
 
@@ -1033,9 +1151,10 @@ if (-e $sf)
   print "\n";
   for (;;)
     {
-    interact("Continue, Update & retry, Quit? [Q] ", $force_update);
+    interact("Continue, Retry, Update & retry, Quit? [Q] ", $force_update);
     tests_exit(1) if /^q?$/i;
     return 0 if /^c$/i;
+    return 1 if /^r$/i;
     last if (/^u$/i);
     }
   }
@@ -1043,7 +1162,7 @@ if (-e $sf)
 # Update or delete the saved file, and give the appropriate return code.
 
 if (-s $mf)
-  { tests_exit(-1, "Failed to cp $mf $sf") if system("cp $mf $sf") != 0; }
+  { tests_exit(-1, "Failed to cp $mf $sf") if system("cp '$mf' '$sf'") != 0; }
 else
   { tests_exit(-1, "Failed to unlink $sf") if !unlink($sf); }
 
@@ -1067,7 +1186,7 @@ return 1;
 #
 # Arguments: none
 # Returns:   0 if the output compared equal
-#            1 if files were updated and the test must be re-run
+#            1 if re-run needed (files may have been updated)
 
 sub check_output{
 my($yield) = 0;
@@ -1294,17 +1413,21 @@ system("$cmd");
 #            reference to the subtest number, holding previous value
 #            reference to the expected return code value
 #            reference to where to put the command name (for messages)
+#            auxilliary information returned from a previous run
 #
 # Returns:   0 the commmand was executed inline, no subprocess was run
 #            1 a non-exim command was run and waited for
 #            2 an exim command was run and waited for
 #            3 a command was run and not waited for (daemon, server, exim_lock)
 #            4 EOF was encountered after an initial return code line
+# Optionally alse a second parameter, a hash-ref, with auxilliary information:
+#            exim_pid: pid of a run process
 
 sub run_command{
 my($testno) = $_[0];
 my($subtestref) = $_[1];
 my($commandnameref) = $_[3];
+my($aux_info) = $_[4];
 my($yield) = 1;
 
 if (/^(\d+)\s*$/)                # Handle unusual return code
@@ -1462,9 +1585,10 @@ if (/^eximstats\s+(.*)/)
 
 if (/^gnutls/)
   {
-  run_system "sudo cp -p aux-fixed/gnutls-params spool/gnutls-params;" .
-         "sudo chown $parm_eximuser:$parm_eximgroup spool/gnutls-params;" .
-         "sudo chmod 0400 spool/gnutls-params";
+  my $gen_fn = "spool/gnutls-params-$gnutls_dh_bits_normal";
+  run_system "sudo cp -p aux-fixed/gnutls-params $gen_fn;" .
+         "sudo chown $parm_eximuser:$parm_eximgroup $gen_fn;" .
+         "sudo chmod 0400 $gen_fn";
   return 1;
   }
 
@@ -1475,11 +1599,27 @@ if (/^gnutls/)
 
 if (/^killdaemon/)
   {
-  $pid = `cat $parm_cwd/spool/exim-daemon.*`;
-  run_system("sudo /bin/kill -SIGINT $pid");
-  close DAEMONCMD;                                   # Waits for process
-  run_system("sudo /bin/rm -f spool/exim-daemon.*");
-  return 1;
+  my $return_extra = {};
+  if (exists $aux_info->{exim_pid})
+    {
+    $pid = $aux_info->{exim_pid};
+    $return_extra->{exim_pid} = undef;
+    print ">> killdaemon: recovered pid $pid\n" if $debug;
+    if ($pid)
+      {
+      run_system("sudo /bin/kill -SIGINT $pid");
+      wait;
+      }
+    } else {
+    $pid = `cat $parm_cwd/spool/exim-daemon.*`;
+    if ($pid)
+      {
+      run_system("sudo /bin/kill -SIGINT $pid");
+      close DAEMONCMD;                                   # Waits for process
+      }
+    }
+    run_system("sudo /bin/rm -f spool/exim-daemon.*");
+  return (1, $return_extra);
   }
 
 
@@ -1707,6 +1847,10 @@ elsif (/^([A-Z_]+=\S+\s+)?(\d+)?\s*(sudo\s+)?exim(_\S+)?\s+(.*)$/)
 
     my($i);
     for ($i = @msglist; $i > 0; $i--) { $args =~ s/\$msg$i/$msglist[$i-1]/g; }
+    if ( $args =~ /\$msg\d/ )
+      {
+      tests_exit(-1, "Not enough messages in spool, for test $testno line $lineno\n");
+      }
     }
 
   # If -d is specified in $optargs, remove it from $args; i.e. let
@@ -1750,6 +1894,38 @@ elsif (/^([A-Z_]+=\S+\s+)?(\d+)?\s*(sudo\s+)?exim(_\S+)?\s+(.*)$/)
     select(undef, undef, undef, 0.3);             # Let the daemon get going
     return 3;                                     # Don't wait
     }
+  elsif ($cmd =~ /\s-DSERVER=wait:(\d+)\s/)
+    {
+    my $listen_port = $1;
+    my $waitmode_sock = new FileHandle;
+    if ($debug) { printf ">> wait-mode daemon: $cmd\n"; }
+    run_system("sudo mkdir spool/log 2>/dev/null");
+    run_system("sudo chown $parm_eximuser:$parm_eximgroup spool/log");
+
+    my ($s_ip,$s_port) = ('127.0.0.1', $listen_port);
+    my $sin = sockaddr_in($s_port, inet_aton($s_ip))
+        or die "** Failed packing $s_ip:$s_port\n";
+    socket($waitmode_sock, PF_INET, SOCK_STREAM, getprotobyname('tcp'))
+        or die "** Unable to open socket $s_ip:$s_port: $!\n";
+    setsockopt($waitmode_sock, SOL_SOCKET, SO_REUSEADDR, 1)
+        or die "** Unable to setsockopt(SO_REUSEADDR): $!\n";
+    bind($waitmode_sock, $sin)
+        or die "** Unable to bind socket ($s_port): $!\n";
+    listen($waitmode_sock, 5);
+    my $pid = fork();
+    if (not defined $pid) { die "** fork failed: $!\n" }
+    if (not $pid) {
+      close(STDIN);
+      open(STDIN, "<&", $waitmode_sock) or die "** dup sock to stdin failed: $!\n";
+      close($waitmode_sock);
+      print "[$$]>> ${cmd}-server\n" if ($debug);
+      exec "exec ${cmd}-server";
+      exit(1);
+    }
+    while (<SCRIPT>) { $lineno++; last if /^\*{4}\s*$/; }   # Ignore any input
+    select(undef, undef, undef, 0.3);             # Let the daemon get going
+    return (3, { exim_pid => $pid });             # Don't wait
+    }
   }
 
 
@@ -1812,6 +1988,13 @@ autoflush STDOUT 1;
 print "Exim tester $testversion\n";
 
 
+##################################################
+#      Some tests check created file modes       #
+##################################################
+
+umask 022;
+
+
 ##################################################
 #       Check for the "less" command             #
 ##################################################
@@ -1843,7 +2026,7 @@ else
 # If the first character of the first argument is '/', the argument is taken
 # as the path to the binary.
 
-$parm_exim = (@ARGV > 0 && $ARGV[0] =~ ?^/?)? shift @ARGV : "";
+$parm_exim = (@ARGV > 0 && $ARGV[0] =~ m?^/?)? shift @ARGV : "";
 print "Exim binary is $parm_exim\n" if $parm_exim ne "";
 
 
@@ -1983,7 +2166,13 @@ if ($parm_exim eq "")
 #          Find what is in the binary            #
 ##################################################
 
-open(EXIMINFO, "$parm_exim -C confs/0000 -DDIR=$parm_cwd " .
+# deal with TRUSTED_CONFIG_LIST restrictions
+unlink("$parm_cwd/test-config") if -e "$parm_cwd/test-config";
+symlink("$parm_cwd/confs/0000", "$parm_cwd/test-config")
+  or die "Unable to link initial config into place: $!\n";
+
+print("Probing with config file: $parm_cwd/test-config\n");
+open(EXIMINFO, "$parm_exim -d -C $parm_cwd/test-config -DDIR=$parm_cwd " .
                "-bP exim_user exim_group|") ||
   die "** Cannot run $parm_exim: $!\n";
 while(<EXIMINFO>)
@@ -1998,6 +2187,13 @@ if (defined $parm_eximuser)
   if ($parm_eximuser =~ /^\d+$/) { $parm_exim_uid = $parm_eximuser; }
     else { $parm_exim_uid = getpwnam($parm_eximuser); }
   }
+else
+  {
+  print "Unable to extract exim_user from binary.\n";
+  print "Check if Exim refused to run; if so, consider:\n";
+  print "  TRUSTED_CONFIG_LIST ALT_CONFIG_PREFIX WHITELIST_D_MACROS\n";
+  die "Failing to get information from binary.\n";
+  }
 
 if (defined $parm_eximgroup)
   {
@@ -2005,7 +2201,7 @@ if (defined $parm_eximgroup)
     else { $parm_exim_gid = getgrnam($parm_eximgroup); }
   }
 
-open(EXIMINFO, "$parm_exim -bV -C confs/0000 -DDIR=$parm_cwd |") ||
+open(EXIMINFO, "$parm_exim -bV -C $parm_cwd/test-config -DDIR=$parm_cwd |") ||
   die "** Cannot run $parm_exim: $!\n";
 
 print "-" x 78, "\n";
@@ -2018,7 +2214,10 @@ while (<EXIMINFO>)
 
   elsif (/^Size of off_t: (\d+)/)
     {
+    print;
     $have_largefiles = 1 if $1 > 4;
+    die "** Size of off_t > 32 which seems improbable, not running tests\n"
+        if ($1 > 32);
     }
 
   elsif (/^Support for: (.*)/)
@@ -2029,7 +2228,7 @@ while (<EXIMINFO>)
     %parm_support = @temp;
     }
 
-  elsif (/^Lookups: (.*)/)
+  elsif (/^Lookups \(built-in\): (.*)/)
     {
     print;
     @temp = split /(\s+)/, $1;
@@ -2079,6 +2278,7 @@ while (<EXIMINFO>)
 close(EXIMINFO);
 print "-" x 78, "\n";
 
+unlink("$parm_cwd/test-config");
 
 ##################################################
 #    Check for SpamAssassin and ClamAV           #
@@ -2089,6 +2289,8 @@ print "-" x 78, "\n";
 
 if (defined $parm_support{'Content_Scanning'})
   {
+  my $sock = new FileHandle;
+
   if (system("spamc -h 2>/dev/null >/dev/null") == 0)
     {
     print "The spamc command works:\n";
@@ -2105,23 +2307,23 @@ if (defined $parm_support{'Content_Scanning'})
       {
       my $sin = sockaddr_in($sport, inet_aton($sint))
           or die "** Failed packing $sint:$sport\n";
-      socket(SOCK, PF_INET, SOCK_STREAM, getprotobyname('tcp'))
+      socket($sock, PF_INET, SOCK_STREAM, getprotobyname('tcp'))
           or die "** Unable to open socket $sint:$sport\n";
 
       local $SIG{ALRM} =
           sub { die "** Timeout while connecting to socket $sint:$sport\n"; };
       alarm(5);
-      connect(SOCK, $sin)
+      connect($sock, $sin)
           or die "** Unable to connect to socket $sint:$sport\n";
       alarm(0);
 
-      select((select(SOCK), $| = 1)[0]);
-      print SOCK "bad command\r\n";
+      select((select($sock), $| = 1)[0]);
+      print $sock "bad command\r\n";
 
       $SIG{ALRM} =
           sub { die "** Timeout while reading from socket $sint:$sport\n"; };
       alarm(10);
-      my $res = <SOCK>;
+      my $res = <$sock>;
       alarm(0);
 
       $res =~ m|^SPAMD/|
@@ -2232,18 +2434,18 @@ if (defined $parm_support{'Content_Scanning'})
             {
             die "** Unknown socket domain '$socket_domain' (should not happen)\n";
             }
-          socket(SOCK, $socket_domain, SOCK_STREAM, 0) or die "** Unable to open socket '$parm_clamsocket'\n";
+          socket($sock, $socket_domain, SOCK_STREAM, 0) or die "** Unable to open socket '$parm_clamsocket'\n";
           local $SIG{ALRM} = sub { die "** Timeout while connecting to socket '$parm_clamsocket'\n"; };
           alarm(5);
-          connect(SOCK, $socket) or die "** Unable to connect to socket '$parm_clamsocket'\n";
+          connect($sock, $socket) or die "** Unable to connect to socket '$parm_clamsocket'\n";
           alarm(0);
 
-          my $ofh = select SOCK; $| = 1; select $ofh;
-          print SOCK "PING\n";
+          my $ofh = select $sock; $| = 1; select $ofh;
+          print $sock "PING\n";
 
           $SIG{ALRM} = sub { die "** Timeout while reading from socket '$parm_clamsocket'\n"; };
           alarm(10);
-          my $res = <SOCK>;
+          my $res = <$sock>;
           alarm(0);
 
           $res =~ /PONG/ or die "** Did not get PONG from socket '$parm_clamsocket'. It said: $res\n";
@@ -2349,18 +2551,23 @@ if (defined $parm_support{'Expand_dlfunc'} && !-e "bin/loaded")
 # Find the caller of this program.
 
 ($parm_caller,$pwpw,$parm_caller_uid,$parm_caller_gid,$pwquota,$pwcomm,
- $pwgecos, $parm_caller_home) = getpwuid($>);
+ $parm_caller_gecos, $parm_caller_home) = getpwuid($>);
 
 $pwpw = $pwpw;       # Kill Perl warnings
 $pwquota = $pwquota;
 $pwcomm = $pwcomm;
-$pwgecos = $pwgecos;
 
 $parm_caller_group = getgrgid($parm_caller_gid);
 
 print "Program caller is $parm_caller, whose group is $parm_caller_group\n";
 print "Home directory is $parm_caller_home\n";
 
+unless (defined $parm_eximgroup)
+  {
+  print "Unable to derive \$parm_eximgroup.\n";
+  die "** ABANDONING.\n";
+  }
+
 print "You need to be in the Exim group to run these tests. Checking ...";
 
 if (`groups` =~ /\b\Q$parm_eximgroup\E\b/)
@@ -2554,7 +2761,7 @@ system("sudo cp eximdir/exim eximdir/exim_exim;" .
 # Certain of the tests make use of some of Exim's utilities. We do not need
 # to be root to copy these.
 
-($parm_exim_dir) = $parm_exim =~ ?^(.*)/exim?;
+($parm_exim_dir) = $parm_exim =~ m?^(.*)/exim?;
 
 $dbm_build_deleted = 0;
 if (defined $parm_lookups{'dbm'} &&
@@ -2599,6 +2806,12 @@ if (system("cp $parm_exim_dir/eximstats eximdir") != 0)
 
 print "Exim user is $parm_eximuser ($parm_exim_uid)\n";
 print "Exim group is $parm_eximgroup ($parm_exim_gid)\n";
+
+if ($parm_caller_uid eq $parm_exim_uid) {
+  tests_exit(-1, "Exim user ($parm_eximuser,$parm_exim_uid) cannot be "
+                ."the same as caller ($parm_caller,$parm_caller_uid)");
+}
+
 print "The Exim user needs access to the test suite directory. Checking ...";
 
 if (($rc = system("sudo bin/checkaccess $parm_cwd/eximdir/exim $parm_eximuser $parm_eximgroup")) != 0)
@@ -2846,7 +3059,16 @@ if ($have_ipv4 && $parm_ipv4 ne "127.0.0.1")
 
 if ($have_ipv6 && $parm_ipv6 ne "::1")
   {
-  my(@components) = split /:/, $parm_ipv6;
+  my($exp_v6) = $parm_ipv6;
+  $exp_v6 =~ s/[^:]//g;
+  if ( $parm_ipv6 =~ /^([^:].+)::$/ ) {
+    $exp_v6 = $1 . ':0' x (9-length($exp_v6));
+  } elsif ( $parm_ipv6 =~ /^(.+)::(.+)$/ ) {
+    $exp_v6 = $1 . ':0' x (8-length($exp_v6)) . ':' . $2;
+  } elsif ( $parm_ipv6 =~ /^::(.+[^:])$/ ) {
+    $exp_v6 = '0:' x (9-length($exp_v6)) . $1;
+  }
+  my(@components) = split /:/, $exp_v6;
   my(@nibbles) = reverse (split /\s*/, shift @components);
   my($sep) =  "";
 
@@ -3058,10 +3280,30 @@ foreach $test (@test_list)
 
     my($commandname) = "";
     my($expectrc) = 0;
-    my($rc) = run_command($testno, \$subtestno, \$expectrc, \$commandname);
+    my($rc, $run_extra) = run_command($testno, \$subtestno, \$expectrc, \$commandname, $TEST_STATE);
     my($cmdrc) = $?;
 
-    print ">> rc=$rc cmdrc=$cmdrc\n" if $debug;
+    if ($debug) {
+      print ">> rc=$rc cmdrc=$cmdrc\n";
+      if (defined $run_extra) {
+        foreach my $k (keys %$run_extra) {
+          my $v = defined $run_extra->{$k} ? qq!"$run_extra->{$k}"! : '<undef>';
+          print ">>   $k -> $v\n";
+        }
+      }
+    }
+    $run_extra = {} unless defined $run_extra;
+    foreach my $k (keys %$run_extra) {
+      if (exists $TEST_STATE->{$k}) {
+        my $nv = defined $run_extra->{$k} ? qq!"$run_extra->{$k}"! : 'removed';
+        print ">> override of $k; was $TEST_STATE->{$k}, now $nv\n" if $debug;
+      }
+      if (defined $run_extra->{$k}) {
+        $TEST_STATE->{$k} = $run_extra->{$k};
+      } elsif (exists $TEST_STATE->{$k}) {
+        delete $TEST_STATE->{$k};
+      }
+    }
 
     # Hit EOF after an initial return code number
 
@@ -3090,10 +3332,10 @@ foreach $test (@test_list)
 
       for (;;)
         {
-        print "\nshow stdErr, show stdOut, Continue (without file comparison), or Quit? [Q] ";
+        print "\nshow stdErr, show stdOut, Retry, Continue (without file comparison), or Quit? [Q] ";
         $_ = <T>;
         tests_exit(1) if /^q?$/i;
-        last if /^c$/i;
+        last if /^[rc]$/i;
         if (/^e$/i)
           {
           system("$more test-stderr");
@@ -3104,6 +3346,7 @@ foreach $test (@test_list)
           }
         }
 
+      $retry = 1 if /^r$/i;
       $docheck = 0;
       }
 
@@ -3126,10 +3369,10 @@ foreach $test (@test_list)
 
         for (;;)
           {
-          print "\nShow server stdout, Continue, or Quit? [Q] ";
+          print "\nShow server stdout, Retry, Continue, or Quit? [Q] ";
           $_ = <T>;
           tests_exit(1) if /^q?$/i;
-          last if /^c$/i;
+          last if /^[rc]$/i;
 
           if (/^s$/i)
             {
@@ -3139,6 +3382,7 @@ foreach $test (@test_list)
             close(S);
             }
           }
+        $retry = 1 if /^r$/i;
         }
       }
     }
@@ -3149,6 +3393,13 @@ foreach $test (@test_list)
   # function returns 0 if all is well, 1 if we should rerun the test (the files
   # have been updated). It does not return if the user responds Q to a prompt.
 
+  if ($retry)
+    {
+    $retry = '0';
+    print (("#" x 79) . "\n");
+    redo;
+    }
+
   if ($docheck)
     {
     if (check_output() != 0)
@@ -3172,4 +3423,4 @@ tests_exit(-1, "No runnable tests selected") if @test_list == 0;
 tests_exit(0);
 
 # End of runtest script
-
+# vim: set sw=2 et :