Testsuite: tidying
[users/jgh/exim.git] / test / runtest
index c6d9c729e22c4bc1cb61d6c12e0859f2bdff691b..5cfe1906dd08c33a1bda679d2d27a6f42e30b939 100755 (executable)
@@ -1,4 +1,6 @@
-#! /usr/bin/perl -w
+#! /usr/bin/env perl
+# We use env, because in some environments of our build farm
+# the Perl 5.010 interpreter is only reachable via $PATH
 
 ###############################################################################
 # This is the controlling script for the "new" test suite for Exim. It should #
 ###############################################################################
 
 #use strict;
-require Cwd;
+use 5.010;
+use warnings;
+
 use Errno;
 use FileHandle;
+use IO::Socket::INET;
 use Socket;
 use Time::Local;
+use Cwd;
+use File::Basename;
+use if $ENV{DEBUG} && $ENV{DEBUG} =~ /\bruntest\b/ => ('Smart::Comments' => '####');
 
 
 # Start by initializing some global variables
@@ -36,6 +44,7 @@ $gnutls_dh_bits_normal = 2236;
 $cf = "bin/cf -exact";
 $cr = "\r";
 $debug = 0;
+$flavour = 'FOO';
 $force_continue = 0;
 $force_update = 0;
 $log_failed_filename = "failed-summary.log";
@@ -43,6 +52,7 @@ $more = "less -XF";
 $optargs = "";
 $save_output = 0;
 $server_opts = "";
+$valgrind = 0;
 
 $have_ipv4 = 1;
 $have_ipv6 = 1;
@@ -76,11 +86,19 @@ $parm_port_d = 1225;         # Used for the Exim daemon
 $parm_port_d2 = 1226;        # Additional for daemon
 $parm_port_d3 = 1227;        # Additional for daemon
 $parm_port_d4 = 1228;        # Additional for daemon
+my $dynamic_socket;          # allocated later for PORT_DYNAMIC
 
 # Manually set locale
-$ENV{'LC_ALL'} = 'C';
+$ENV{LC_ALL} = 'C';
 
+# In some environments USER does not exists, but we
+# need it for some test(s)
+$ENV{USER} = getpwuid($>)
+  if not exists $ENV{USER};
 
+my ($parm_configure_owner, $parm_configure_group);
+my ($parm_ipv4, $parm_ipv6);
+my $parm_hostname;
 
 ###############################################################################
 ###############################################################################
@@ -130,6 +148,7 @@ s?\bPORT_S\b?$parm_port_s?g;
 s?\bTESTNUM\b?$_[0]?g;
 s?(\b|_)V4NET([\._])?$1$parm_ipv4_test_net$2?g;
 s?\bV6NET:?$parm_ipv6_test_net:?g;
+s?\bPORT_DYNAMIC\b?$dynamic_socket->sockport()?eg;
 }
 
 
@@ -320,6 +339,8 @@ my($extra) = $_[1];
 my($yield) = 0;
 my(@saved) = ();
 
+local $_;
+
 open(IN, "$file") || tests_exit(-1, "Failed to open $file: $!");
 
 my($is_log) = $file =~ /log/;
@@ -339,7 +360,7 @@ $spid = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
 # that are specific to certain file types, though there are also some of those
 # inline too.
 
-while(<IN>)
+LINE: while(<IN>)
   {
 RESET_AFTER_EXTRA_LINE_READ:
   # Custom munges
@@ -413,10 +434,11 @@ RESET_AFTER_EXTRA_LINE_READ:
 
   # Random local part in callout cache testing
   s/myhost.test.ex-\d+-testing/myhost.test.ex-dddddddd-testing/;
+  s/the.local.host.name-\d+-testing/the.local.host.name-dddddddd-testing/;
 
   # File descriptor numbers may vary
   s/^writing data block fd=\d+/writing data block fd=dddd/;
-  s/running as transport filter: write=\d+ read=\d+/running as transport filter: write=dddd read=dddd/;
+  s/(running as transport filter:) fd_write=\d+ fd_read=\d+/$1 fd_write=dddd fd_read=dddd/;
 
 
   # ======== Dumpdb output ========
@@ -642,6 +664,9 @@ RESET_AFTER_EXTRA_LINE_READ:
   s/waiting for children of \d+/waiting for children of pppp/;
   s/waiting for (\S+) \(\d+\)/waiting for $1 (pppp)/;
 
+  # The spool header file name varies with PID
+  s%^(Writing spool header file: .*/hdr).[0-9]{1,5}%$1.pppp%;
+
   # ======== Port numbers ========
   # Incoming port numbers may vary, but not in daemon startup line.
 
@@ -658,6 +683,10 @@ RESET_AFTER_EXTRA_LINE_READ:
   # Port in host address in spool file output from -Mvh
   s/^-host_address (.*)\.\d+/-host_address $1.9999/;
 
+  if ($dynamic_socket and $dynamic_socket->opened and my $port = $dynamic_socket->sockport) {
+    s/^Connecting to 127\.0\.0\.1 port \K$port/<dynamic port>/;
+  }
+
 
   # ======== Local IP addresses ========
   # The amount of space between "host" and the address in verification output
@@ -703,6 +732,9 @@ RESET_AFTER_EXTRA_LINE_READ:
   # ======== Other error numbers ========
   s/errno=\d+/errno=dd/g;
 
+  # ======== System Error Messages ======
+  # depending on the underlaying file system the error message seems to differ
+  s/(?: is not a regular file)|(?: has too many links \(\d+\))/ not a regular file or too many links/;
 
   # ======== Output from ls ========
   # Different operating systems use different spacing on long output
@@ -731,8 +763,8 @@ RESET_AFTER_EXTRA_LINE_READ:
   s/this message = \d+\b/this message = sss/;
   s/Size of headers = \d+/Size of headers = sss/;
   s/sum=(?!0)\d+/sum=dddd/;
-  s/(?<=sum=dddd )count=(?!0)\d+\b/count=dd/;
-  s/(?<=sum=0 )count=(?!0)\d+\b/count=dd/;
+  s/(?<=sum=dddd )count=\d+\b/count=dd/;
+  s/(?<=sum=0 )count=\d+\b/count=dd/;
   s/,S is \d+\b/,S is ddddd/;
   s/\+0100,\d+;/+0100,ddd;/;
   s/\(\d+ bytes written\)/(ddd bytes written)/;
@@ -758,6 +790,7 @@ RESET_AFTER_EXTRA_LINE_READ:
   # different wording in the error messages, so we cannot compare them.
 
   s/(TLS error on connection (?:from .* )?\(SSL_\w+\): error:)(.*)/$1 <<detail omitted>>/;
+  next if /SSL verify error: depth=0 error=certificate not trusted/;
 
   # ======== Maildir things ========
   # timestamp output in maildir processing
@@ -844,7 +877,11 @@ RESET_AFTER_EXTRA_LINE_READ:
     next if /^SSL info: unknown state/;
     next if /^SSL info: SSLv2\/v3 write client hello A/;
     next if /^SSL info: SSLv3 read server key exchange A/;
+    next if /SSL verify error: depth=0 error=certificate not trusted/;
+    s/SSL3_READ_BYTES/ssl3_read_bytes/;
 
+    # gnutls version variances
+    next if /^Error in the pull function./;
     }
 
   # ======== stderr ========
@@ -862,7 +899,7 @@ RESET_AFTER_EXTRA_LINE_READ:
     # IP address lookups use gethostbyname() when IPv6 is not supported,
     # and gethostbyname2() or getipnodebyname() when it is.
 
-    s/\bgethostbyname2?|\bgetipnodebyname/get[host|ipnode]byname[2]/;
+    s/\b(gethostbyname2?|\bgetipnodebyname)(\(af=inet\))?/get[host|ipnode]byname[2]/;
 
     # drop gnutls version strings
     next if /GnuTLS compile-time version: \d+[\.\d]+$/;
@@ -940,7 +977,7 @@ RESET_AFTER_EXTRA_LINE_READ:
     # are unset, because tls ain't always there.
 
     next if /in\s(?:tls_advertise_hosts\?|hosts_require_tls\?)
-                \sno\s\(option\sunset\)/x;
+                \sno\s\((option\sunset|end\sof\slist)\)/x;
 
     # Skip auxiliary group lists because they will vary.
 
@@ -985,6 +1022,45 @@ RESET_AFTER_EXTRA_LINE_READ:
       while (<IN>) { last if !/^\s/; }
       }
 
+    # remote port numbers vary
+    s/(Connection request from 127.0.0.1 port) \d{1,5}/$1 sssss/;
+
+    # Skip hosts_require_dane checks when the options
+    # are unset, because dane ain't always there.
+
+    next if /in\shosts_require_dane\?\sno\s\(option\sunset\)/x;
+
+    # SUPPORT_PROXY
+    next if /host in hosts_proxy\?/;
+
+    # Experimental_International
+    next if / in smtputf8_advertise_hosts\? no \(option unset\)/;
+
+    # Environment cleaning
+    next if /\w+ in keep_environment\? (yes|no)/;
+
+    # Sizes vary with test hostname
+    s/^cmd buf flush \d+ bytes$/cmd buf flush ddd bytes/;
+
+    # Spool filesystem free space changes on different systems.
+    s/^((?:spool|log) directory space =) -?\d+K (inodes =)\s*-?\d+/$1 nnnnnK $2 nnnnn/;
+
+    # Non-TLS builds have different expansions for received_header_text
+    if (s/( \{with \$received_protocol)}\} \$\{if def:tls_cipher \{\(\$tls_cipher\)\n$/$1/)
+      {
+      $_ .= <IN>;
+      s/\t(\}\}\{Exim)/$1/;
+      }
+
+    # Not all platforms support TCP Fast Open, and the compile omits the check
+    if (s/\S+ in hosts_try_fastopen\? no \(option unset\)\n$//)
+      {
+      $_ .= <IN>;
+      s/ \.\.\. >>> / ... /;
+      }
+
+    next if /^setsockopt FASTOPEN: Protocol not available$/;
+
     # When Exim is checking the size of directories for maildir, it uses
     # the check_dir_size() function to scan directories. Of course, the order
     # of the files that are obtained using readdir() varies from system to
@@ -1006,11 +1082,6 @@ RESET_AFTER_EXTRA_LINE_READ:
         @saved = ();
         }
 
-    # Skip hosts_require_dane checks when the options
-    # are unset, because dane ain't always there.
-
-    next if /in\shosts_require_dane\?\sno\s\(option\sunset\)/x;
-
       # Skip some lines that Exim puts out at the start of debugging output
       # because they will be different in different binaries.
 
@@ -1025,8 +1096,11 @@ RESET_AFTER_EXTRA_LINE_READ:
                 /^log selectors =/ ||
                 /^cwd=/ ||
                 /^Fixed never_users:/ ||
+               /^Configure owner:/ ||
                 /^Size of off_t:/
                 );
+
+
       }
 
     next;
@@ -1059,7 +1133,7 @@ return $yield;
 # Arguments: [0] the prompt string
 #            [1] if there is a U in the prompt and $force_update is true
 #            [2] if there is a C in the prompt and $force_continue is true
-# Returns:   nothing (it sets $_)
+# Returns:   returns the answer
 
 sub interact{
 print $_[0];
@@ -1122,7 +1196,13 @@ my($rf,$rsf,$mf,$sf,$sortfile,$extra) = @_;
 # If there is no saved file, the raw files must either not exist, or be
 # empty. The test ! -s is TRUE if the file does not exist or is empty.
 
-if (! -e $sf)
+# we check if there is a flavour specific file, but we remember
+# the original file name as "generic"
+$sf_generic = $sf;
+$sf_flavour = "$sf_generic.$flavour";
+$sf_current = -e $sf_flavour ? $sf_flavour : $sf_generic;
+
+if (! -e $sf_current)
   {
   return 0 if (! -s $rf && (! defined $rsf || ! -s $rsf));
 
@@ -1162,6 +1242,8 @@ if (! -e $sf)
     }
   }
 
+#### $_
+
 # Control reaches here if either (a) there is a saved file ($sf), or (b) there
 # was a request to create a saved file. First, create the munged file from any
 # data that does exist.
@@ -1189,7 +1271,7 @@ close(MUNGED);
 # a result of parallel deliveries. We load the munged file and sort sequences
 # of delivery lines.
 
-if (-e $sf)
+if (-e $sf_current)
   {
   # Deal with truncated text items
 
@@ -1200,7 +1282,7 @@ if (-e $sf)
     open(MUNGED, "$mf") || tests_exit(-1, "Failed to open $mf: $!");
     @munged = <MUNGED>;
     close(MUNGED);
-    open(SAVED, "$sf") || tests_exit(-1, "Failed to open $sf: $!");
+    open(SAVED, $sf_current) || tests_exit(-1, "Failed to open $sf_current: $!");
     @saved = <SAVED>;
     close(SAVED);
 
@@ -1262,31 +1344,46 @@ if (-e $sf)
 
   # Do the comparison
 
-  return 0 if (system("$cf '$mf' '$sf' >test-cf") == 0);
+  return 0 if (system("$cf '$mf' '$sf_current' >test-cf") == 0);
 
   # Handle comparison failure
 
-  print "** Comparison of $mf with $sf failed";
+  print "** Comparison of $mf with $sf_current failed";
   system("$more test-cf");
 
   print "\n";
   for (;;)
     {
-    interact("Continue, Retry, Update & retry, Quit? [Q] ", $force_update, $force_continue);
+    interact("Continue, Retry, Update current"
+       . ($sf_current ne $sf_flavour  ? "/Save for flavour '$flavour'" : "")
+       . " & retry, Quit? [Q] ", $force_update, $force_continue);
     tests_exit(1) if /^q?$/i;
-    log_failure($log_failed_filename, $testno, $sf) if (/^c$/i && $force_continue);
+    log_failure($log_failed_filename, $testno, $sf_current) if (/^c$/i && $force_continue);
     return 0 if /^c$/i;
     return 1 if /^r$/i;
-    last if (/^u$/i);
+    last if (/^[us]$/i);
     }
   }
 
 # 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; }
+  {
+       my $sf = /^u/i ? $sf_current : $sf_flavour;
+               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); }
+  {
+       # if we deal with a flavour file, we can't delete it, because next time the generic
+       # file would be used again
+       if ($sf_current eq $sf_flavour) {
+               open(FOO, ">$sf_current");
+               close(FOO);
+       }
+       else {
+               tests_exit(-1, "Failed to unlink $sf_current") if !unlink($sf_current);
+       }
+  }
 
 return 1;
 }
@@ -1319,12 +1416,52 @@ $munges =
     'optional_ocsp' =>
     { 'stderr' => '/127.0.0.1 in hosts_requ(ire|est)_ocsp/' },
 
-    'no_tpt_filter_epipe' =>
-    { 'stderr' => '/^writing error 32: Broken pipe$/' },
-
     'optional_cert_hostnames' =>
     { 'stderr' => '/in tls_verify_cert_hostnames\? no/' },
 
+    'loopback' =>
+    { 'stdout' => 's/[[](127\.0\.0\.1|::1)]/[IP_LOOPBACK_ADDR]/' },
+
+    'scanfile_size' =>
+    { 'stdout' => 's/(Content-length:) \d\d\d/$1 ddd/' },
+
+    'delay_1500' =>
+    { 'stderr' => 's/(1[5-9]|23\d)\d\d msec/ssss msec/' },
+
+    'tls_anycipher' =>
+    { 'mainlog' => 's/ X=TLS\S+ / X=TLS_proto_and_cipher /' },
+
+    'debug_pid' =>
+    { 'stderr' => 's/(^\s{0,4}|(?<=Process )|(?<=child ))\d{1,5}/ppppp/g' },
+
+    'optional_dsn_info' =>
+    { 'mail' => '/^(X-(Remote-MTA-(smtp-greeting|helo-response)|Exim-Diagnostic|(body|message)-linecount):|Remote-MTA: X-ip;)/'
+    },
+
+    'optional_config' =>
+    { 'stdout' => '/^(
+                  dkim_(canon|domain|private_key|selector|sign_headers|strict)
+                  |gnutls_require_(kx|mac|protocols)
+                  |hosts_(requ(est|ire)|try)_(dane|ocsp)
+                  |hosts_(avoid|nopass|require|verify_avoid)_tls
+                  |socks_proxy
+                  |tls_[^ ]*
+                  )($|[ ]=)/x' },
+
+    'sys_bindir' =>
+    { 'mainlog' => 's%/(usr/)?bin/%SYSBINDIR/%' },
+
+    'sync_check_data' =>
+    { 'mainlog'   => 's/^(.* SMTP protocol synchronization error .* next input=.{8}).*$/$1<suppressed>/',
+      'rejectlog' => 's/^(.* SMTP protocol synchronization error .* next input=.{8}).*$/$1<suppressed>/'},
+
+    'debuglog_stdout' =>
+    { 'stdout' => 's/^\d\d:\d\d:\d\d\s+\d+ //;
+                  s/Process \d+ is ready for new message/Process pppp is ready for new message/'
+    },
+
+    'timeout_errno' =>         # actual errno differs Solaris vs. Linux
+    { 'mainlog' => 's/(host deferral .* errno) <\d+> /$1 <EEE> /' },
   };
 
 
@@ -1342,7 +1479,7 @@ $munges =
 #  [4] TRUE if this is a log file whose deliveries must be sorted
 #  [5] an optional custom munge command
 #
-# Arguments: Optionally, name of a custom munge to run.
+# Arguments: Optionally, name of a single custom munge to run.
 # Returns:   0 if the output compared equal
 #            1 if re-run needed (files may have been updated)
 
@@ -1600,6 +1737,8 @@ my($commandnameref) = $_[3];
 my($aux_info) = $_[4];
 my($yield) = 1;
 
+our %ENV = map { $_ => $ENV{$_} } grep { /^(?:USER|SHELL|PATH|TERM|EXIM_TEST_.*)$/ } keys %ENV;
+
 if (/^(\d+)\s*$/)                # Handle unusual return code
   {
   my($r) = $_[2];
@@ -1865,7 +2004,7 @@ if (/^sleep\s+(.*)$/)
 # Various Unix management commands are recognized
 
 if (/^(ln|ls|du|mkdir|mkfifo|touch|cp|cat)\s/ ||
-    /^sudo (rmdir|rm|chown|chmod)\s/)
+    /^sudo\s(rmdir|rm|mv|chown|chmod)\s/)
   {
   run_system("$_ >>test-stdout 2>>test-stderr");
   return 1;
@@ -1886,10 +2025,12 @@ if (/^(ln|ls|du|mkdir|mkfifo|touch|cp|cat)\s/ ||
 # command, triggered by $server_pid being non-zero. The server sends its output
 # to a different file. The variable $server_opts, if not empty, contains
 # options to disable IPv4 or IPv6 if necessary.
+# This works because "server" swallows its stdin before waiting for a connection.
 
 if (/^server\s+(.*)$/)
   {
-  $cmd = "./bin/server $server_opts $1 >>test-stdout-server";
+  $pidfile = "$parm_cwd/aux-var/server-daemon.pid";
+  $cmd = "./bin/server $server_opts -oP $pidfile $1 >>test-stdout-server";
   print ">> $cmd\n" if ($debug);
   $server_pid = open SERVERCMD, "|$cmd" || tests_exit(-1, "Failed to run $cmd");
   SERVERCMD->autoflush(1);
@@ -1903,10 +2044,9 @@ if (/^server\s+(.*)$/)
   print SERVERCMD "++++\n"; # Send end to server; can't send EOF yet
                             # because close() waits for the process.
 
-  # This gives the server time to get started; otherwise the next
+  # Interlock the server startup; otherwise the next
   # process may not find it there when it expects it.
-
-  select(undef, undef, undef, 0.5);
+  while (! stat("$pidfile") ) { select(undef, undef, undef, 0.3); }
   return 3;
   }
 
@@ -2003,12 +2143,12 @@ if (/^client/ || /^(sudo\s+)?perl\b/)
 # not drop privilege when -C and -D options are present. To run the exim
 # command as root, we use sudo.
 
-elsif (/^([A-Z_]+=\S+\s+)?(\d+)?\s*(sudo\s+)?exim(_\S+)?\s+(.*)$/)
+elsif (/^((?i:[A-Z\d_]+=\S+\s+)+)?(\d+)?\s*(sudo(?:\s+-u\s+(\w+))?\s+)?exim(_\S+)?\s+(.*)$/)
   {
-  $args = $5;
+  $args = $6;
   my($envset) = (defined $1)? $1      : "";
-  my($sudo)   = (defined $3)? "sudo " : "";
-  my($special)= (defined $4)? $4      : "";
+  my($sudo)   = (defined $3)? "sudo " . (defined $4 ? "-u $4 ":"")  : "";
+  my($special)= (defined $5)? $5      : "";
   $wait_time  = (defined $2)? $2      : 0;
 
   # Return 2 rather than 1 afterwards
@@ -2050,8 +2190,7 @@ elsif (/^([A-Z_]+=\S+\s+)?(\d+)?\s*(sudo\s+)?exim(_\S+)?\s+(.*)$/)
 
     # Done backwards just in case there are more than 9
 
-    my($i);
-    for ($i = @msglist; $i > 0; $i--) { $args =~ s/\$msg$i/$msglist[$i-1]/g; }
+    for (my $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")
@@ -2064,11 +2203,13 @@ elsif (/^([A-Z_]+=\S+\s+)?(\d+)?\s*(sudo\s+)?exim(_\S+)?\s+(.*)$/)
 
   $args =~ s/(?:^|\s)-d\S*// if $optargs =~ /(?:^|\s)-d/;
 
-  $cmd = "$envset$sudo$parm_cwd/eximdir/exim$special$optargs " .
+  my $opt_valgrind = $valgrind ? "valgrind --leak-check=yes --suppressions=$parm_cwd/aux-fixed/valgrind.supp " : "";
+
+  $cmd = "$envset$sudo$opt_valgrind" .
+         "$parm_cwd/eximdir/exim$special$optargs " .
          "-DEXIM_PATH=$parm_cwd/eximdir/exim$special " .
          "-C $parm_cwd/test-config $args " .
          ">>test-stdout 2>>test-stderr";
-
   # If the command is starting an Exim daemon, we run it in the same
   # way as the "server" command above, that is, we don't want to wait
   # for the process to finish. That happens when "killdaemon" is obeyed later
@@ -2082,6 +2223,7 @@ elsif (/^([A-Z_]+=\S+\s+)?(\d+)?\s*(sudo\s+)?exim(_\S+)?\s+(.*)$/)
 
   if ($cmd =~ /\s-DSERVER=server\s/ && $cmd !~ /\s-DNOTDAEMON\s/)
     {
+    $pidfile = "$parm_cwd/spool/exim-daemon.pid";
     if ($debug) { printf ">> daemon: $cmd\n"; }
     run_system("sudo mkdir spool/log 2>/dev/null");
     run_system("sudo chown $parm_eximuser:$parm_eximgroup spool/log");
@@ -2092,41 +2234,45 @@ elsif (/^([A-Z_]+=\S+\s+)?(\d+)?\s*(sudo\s+)?exim(_\S+)?\s+(.*)$/)
     # file is written to the spool directory, in case the Exim binary was
     # built with PID_FILE_PATH pointing somewhere else.
 
-    $cmd =~ s!\s-bd\s! -bdf -oP $parm_cwd/spool/exim-daemon.pid !;
+    if ($cmd =~ /\s-oP\s/)
+      {
+      ($pidfile = $cmd) =~ s/^.*-oP ([^ ]+).*$/$1/;
+      $cmd =~ s!\s-bd\s! -bdf !;
+      }
+    else
+      {
+      $pidfile = "$parm_cwd/spool/exim-daemon.pid";
+      $cmd =~ s!\s-bd\s! -bdf -oP $pidfile !;
+      }
     print ">> |${cmd}-server\n" if ($debug);
     open DAEMONCMD, "|${cmd}-server" || tests_exit(-1, "Failed to run $cmd");
     DAEMONCMD->autoflush(1);
     while (<SCRIPT>) { $lineno++; last if /^\*{4}\s*$/; }   # Ignore any input
-    select(undef, undef, undef, 0.3);             # Let the daemon get going
+
+    # Interlock with daemon startup
+    while (! stat("$pidfile") ) { select(undef, undef, undef, 0.3); }
     return 3;                                     # Don't wait
     }
   elsif ($cmd =~ /\s-DSERVER=wait:(\d+)\s/)
     {
+
+    # The port and the $dynamic_socket was already allocated while parsing the
+    # script file, where -DSERVER=wait:PORT_DYNAMIC was encountered.
+
     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);
+      open(STDIN, '<&', $dynamic_socket) or die "** dup sock to stdin failed: $!\n";
+      close($dynamic_socket);
       print "[$$]>> ${cmd}-server\n" if ($debug);
       exec "exec ${cmd}-server";
-      exit(1);
+      die "Can't exec ${cmd}-server: $!\n";
     }
     while (<SCRIPT>) { $lineno++; last if /^\*{4}\s*$/; }   # Ignore any input
     select(undef, undef, undef, 0.3);             # Let the daemon get going
@@ -2134,6 +2280,45 @@ elsif (/^([A-Z_]+=\S+\s+)?(\d+)?\s*(sudo\s+)?exim(_\S+)?\s+(.*)$/)
     }
   }
 
+# The "background" command is run but not waited-for, like exim -DSERVER=server.
+# One script line is read and fork-exec'd.  The PID is stored for a later
+# killdaemon.
+
+elsif (/^background$/)
+  {
+  my $line;
+#  $pidfile = "$parm_cwd/aux-var/server-daemon.pid";
+
+  $_ = <SCRIPT>; $lineno++;
+  chomp;
+  $line = $_;
+  if ($debug) { printf ">> daemon: $line >>test-stdout 2>>test-stderr\n"; }
+
+  my $pid = fork();
+  if (not defined $pid) { die "** fork failed: $!\n" }
+  if (not $pid) {
+    print "[$$]>> ${line}\n" if ($debug);
+    close(STDIN);
+    open(STDIN, "<", "test-stdout");
+    close(STDOUT);
+    open(STDOUT, ">>", "test-stdout");
+    close(STDERR);
+    open(STDERR, ">>", "test-stderr-server");
+    exec "exec ${line}";
+    exit(1);
+  }
+
+#  open(my $fh, ">", $pidfile) ||
+#      tests_exit(-1, "Failed to open $pidfile: $!");
+#  printf($fh, "%d\n", $pid);
+#  close($fh);
+
+  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
+  }
+
+
 
 # Unknown command
 
@@ -2184,7 +2369,7 @@ return $yield;            # Ran command and waited
 ###############################################################################
 ###############################################################################
 
-# Here beginneth the Main Program ...
+# Here begins the Main Program ...
 
 ###############################################################################
 ###############################################################################
@@ -2193,6 +2378,15 @@ return $yield;            # Ran command and waited
 autoflush STDOUT 1;
 print "Exim tester $testversion\n";
 
+# extend the PATH with .../sbin
+# we map all (.../bin) to (.../sbin:.../bin)
+$ENV{PATH} = do {
+  my %seen = map { $_, 1 } split /:/, $ENV{PATH};
+  join ':' => map { m{(.*)/bin$}
+                ? ( $seen{"$1/sbin"} ? () : ("$1/sbin"), $_)
+                : ($_) }
+      split /:/, $ENV{PATH};
+};
 
 ##################################################
 #      Some tests check created file modes       #
@@ -2205,7 +2399,7 @@ umask 022;
 #       Check for the "less" command             #
 ##################################################
 
-$more = "more" if system("which less >/dev/null 2>&1") != 0;
+$more = 'more' if system('which less >/dev/null 2>&1') != 0;
 
 
 
@@ -2214,7 +2408,7 @@ $more = "more" if system("which less >/dev/null 2>&1") != 0;
 ##################################################
 
 print "You need to have sudo access to root to run these tests. Checking ...\n";
-if (system("sudo date >/dev/null") != 0)
+if (system('sudo true >/dev/null') != 0)
   {
   die "** Test for sudo failed: testing abandoned.\n";
   }
@@ -2230,9 +2424,10 @@ else
 ##################################################
 
 # If the first character of the first argument is '/', the argument is taken
-# as the path to the binary.
+# as the path to the binary. If the first argument does not start with a
+# '/' but exists in the file system, it's assumed to be the Exim binary.
 
-$parm_exim = (@ARGV > 0 && $ARGV[0] =~ m?^/?)? shift @ARGV : "";
+$parm_exim = (@ARGV > 0 && (-x $ARGV[0] or $ARGV[0] =~ m?^/?))? Cwd::abs_path(shift @ARGV) : "";
 print "Exim binary is $parm_exim\n" if $parm_exim ne "";
 
 
@@ -2259,6 +2454,8 @@ while (@ARGV > 0 && $ARGV[0] =~ /^-/)
     if ($arg eq "-NOIPV4") { $have_ipv4 = 0; next; }
     if ($arg eq "-NOIPV6") { $have_ipv6 = 0; next; }
     if ($arg eq "-KEEP")   { $save_output = 1; next; }
+    if ($arg eq "-VALGRIND")   { $valgrind = 1; next; }
+    if ($arg =~ /^-FLAVOU?R$/) { $flavour = shift; next; }
     }
   $optargs .= " $arg";
   }
@@ -2311,7 +2508,7 @@ if ($parm_exim eq "")
     # directory. Thus, we should choose the highest version of Exim that has
     # been compiled.
 
-    if ($f eq "exim4" || $f eq "exim-snapshot")
+    if ($f eq "exim4" || $f eq "exim-snapshot" || $f eq 'src')
       { $srcdir = $f; }
     else
       { $srcdir = $f
@@ -2377,17 +2574,37 @@ if ($parm_exim eq "")
 
 # 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";
+open (IN, "$parm_cwd/confs/0000") ||
+  tests_exit(-1, "Couldn't open $parm_cwd/confs/0000: $!\n");
+open (OUT, ">test-config") ||
+  tests_exit(-1, "Couldn't open test-config: $!\n");
+while (<IN>) { print OUT; }
+close(IN);
+close(OUT);
 
 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|") ||
+               "-bP exim_user exim_group 2>&1|") ||
   die "** Cannot run $parm_exim: $!\n";
 while(<EXIMINFO>)
   {
+  if (my ($version) = /^Exim version (\S+)/) {
+    my $git = `git describe --dirty=-XX --match 'exim-4*'`;
+    if (defined $git and $? == 0) {
+      chomp $git;
+      $version =~ s/^\d+\K\./_/;
+      $git =~ s/^exim-//i;
+      $git =~ s/.*-\Kg([[:xdigit:]]+(?:-XX)?)/$1/;
+      print "\n*** Version mismatch (Exim: $version vs. GIT: $git). ***\n\n"
+        if not $version eq $git;
+    }
+  }
   $parm_eximuser = $1 if /^exim_user = (.*)$/;
   $parm_eximgroup = $1 if /^exim_group = (.*)$/;
+  $parm_trusted_config_list = $1 if /^TRUSTED_CONFIG_LIST:.*?"(.*?)"$/;
+  ($parm_configure_owner, $parm_configure_group) = ($1, $2)
+       if /^Configure owner:\s*(\d+):(\d+)/;
+  print "$_" if /wrong owner/;
   }
 close(EXIMINFO);
 
@@ -2401,6 +2618,7 @@ 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";
+  print "If debug permission denied, are you in the exim group?\n";
   die "Failing to get information from binary.\n";
   }
 
@@ -2410,7 +2628,43 @@ if (defined $parm_eximgroup)
     else { $parm_exim_gid = getgrnam($parm_eximgroup); }
   }
 
-open(EXIMINFO, "$parm_exim -bV -C $parm_cwd/test-config -DDIR=$parm_cwd |") ||
+# check the permissions on the TRUSTED_CONFIG_LIST
+if (defined $parm_trusted_config_list)
+  {
+  die "TRUSTED_CONFIG_LIST: $parm_trusted_config_list: $!\n"
+    if not -f $parm_trusted_config_list;
+
+  die "TRUSTED_CONFIG_LIST $parm_trusted_config_list must not be world writable!\n"
+    if 02 & (stat _)[2];
+
+  die sprintf "TRUSTED_CONFIG_LIST: $parm_trusted_config_list %d is group writable, but not owned by group '%s' or '%s'.\n",
+  (stat _)[1],
+    scalar(getgrgid 0), scalar(getgrgid $>)
+    if (020 & (stat _)[2]) and not ((stat _)[5] == $> or (stat _)[5] == 0);
+
+  die sprintf "TRUSTED_CONFIG_LIST: $parm_trusted_config_list is not owned by user '%s' or '%s'.\n",
+  scalar(getpwuid 0), scalar(getpwuid $>)
+     if (not (-o _ or (stat _)[4] == 0));
+
+  open(TCL, $parm_trusted_config_list) or die "Can't open $parm_trusted_config_list: $!\n";
+  my $test_config = getcwd() . '/test-config';
+  die "Can't find '$test_config' in TRUSTED_CONFIG_LIST $parm_trusted_config_list."
+  if not grep { /^$test_config$/ } <TCL>;
+  }
+else
+  {
+  die "Unable to check the TRUSTED_CONFIG_LIST, seems to be empty?\n";
+  }
+
+die "CONFIGURE_OWNER ($parm_configure_owner) does not match the user invoking $0 ($>)\n"
+       if $parm_configure_owner != $>;
+
+die "CONFIGURE_GROUP ($parm_configure_group) does not match the group invoking $0 ($))\n"
+       if 0020 & (stat "$parm_cwd/test-config")[2]
+       and $parm_configure_group != $);
+
+
+open(EXIMINFO, "$parm_exim -d-all+transport -bV -C $parm_cwd/test-config -DDIR=$parm_cwd |") ||
   die "** Cannot run $parm_exim: $!\n";
 
 print "-" x 78, "\n";
@@ -2419,7 +2673,7 @@ while (<EXIMINFO>)
   {
   my(@temp);
 
-  if (/^Exim version/) { print; }
+  if (/^(Exim|Library) version/) { print; }
 
   elsif (/^Size of off_t: (\d+)/)
     {
@@ -2688,6 +2942,22 @@ if (defined $parm_support{'Content_Scanning'})
   }
 
 
+##################################################
+#       Check for redis                          #
+##################################################
+if (defined $parm_lookups{'redis'})
+  {
+  if (system("redis-server -v 2>/dev/null >/dev/null") == 0)
+    {
+    print "The redis-server command works\n";
+    $parm_running{'redis'} = ' ';
+    }
+  else
+    {
+    print "The redis-server command failed: assume Redis not installed\n";
+    }
+  }
+
 ##################################################
 #         Test for the basic requirements        #
 ##################################################
@@ -2792,39 +3062,26 @@ else
 # Find this host's IP addresses - there may be many, of course, but we keep
 # one of each type (IPv4 and IPv6).
 
-$parm_ipv4 = "";
-$parm_ipv6 = "";
-
-$local_ipv4 = "";
-$local_ipv6 = "";
-
-open(IFCONFIG, "ifconfig -a|") || die "** Cannot run \"ifconfig\": $!\n";
-while (($parm_ipv4 eq "" || $parm_ipv6 eq "") && ($_ = <IFCONFIG>))
+open(IFCONFIG, '-|', (grep { -x "$_/ip" } split /:/, $ENV{PATH}) ? 'ip address' : 'ifconfig -a')
+  or die "** Cannot run 'ip address' or 'ifconfig -a'\n";
+while (not ($parm_ipv4 and $parm_ipv6) and defined($_ = <IFCONFIG>))
   {
-  my($ip);
-  if ($parm_ipv4 eq "" &&
-      $_ =~ /^\s*inet(?:\saddr)?:?\s?(\d+\.\d+\.\d+\.\d+)\s/i)
+  if (not $parm_ipv4 and /^\s*inet(?:\saddr)?:?\s?(\d+\.\d+\.\d+\.\d+)(?:\/\d+)?\s/i)
     {
-    $ip = $1;
-    next if ($ip =~ /^127\./);
-    $parm_ipv4 = $ip;
+    next if $1 =~ /^(?:127|10)\./;
+    $parm_ipv4 = $1;
     }
 
-  if ($parm_ipv6 eq "" &&
-      $_ =~ /^\s*inet6(?:\saddr)?:?\s?([abcdef\d:]+)/i)
+  if (not $parm_ipv6 and /^\s*inet6(?:\saddr)?:?\s?([abcdef\d:]+)(?:\/\d+)/i)
     {
-    $ip = $1;
-    next if ($ip eq "::1" || $ip =~ /^fe80/i);
-    $parm_ipv6 = $ip;
+    next if $1 eq '::1' or $1 =~ /^fe80/i;
+    $parm_ipv6 = $1;
     }
   }
 close(IFCONFIG);
 
 # Use private IP addresses if there are no public ones.
 
-$parm_ipv4 = $local_ipv4 if ($parm_ipv4 eq "");
-$parm_ipv6 = $local_ipv6 if ($parm_ipv6 eq "");
-
 # If either type of IP address is missing, we need to set the value to
 # something other than empty, because that wrecks the substitutions. The value
 # is reflected, so use a meaningful string. Set appropriate options for the
@@ -2833,7 +3090,7 @@ $parm_ipv6 = $local_ipv6 if ($parm_ipv6 eq "");
 # of IPV4 or IPv6 can be simulated by command options, which force $have_ipv4
 # and $have_ipv6 false.
 
-if ($parm_ipv4 eq "")
+if (not $parm_ipv4)
   {
   $have_ipv4 = 0;
   $parm_ipv4 = "<no IPv4 address found>";
@@ -2849,7 +3106,7 @@ else
   $parm_running{"IPv4"} = " ";
   }
 
-if ($parm_ipv6 eq "")
+if (not $parm_ipv6)
   {
   $have_ipv6 = 0;
   $parm_ipv6 = "<no IPv6 address found>";
@@ -2897,8 +3154,16 @@ if ($parm_ipv6 =~ /^[\da-f]/)
 # Find the host name, fully qualified.
 
 chomp($temp = `hostname`);
-$parm_hostname = (gethostbyname($temp))[0];
-$parm_hostname = "no.host.name.found" if $parm_hostname eq "";
+die "'hostname' didn't return anything\n" unless defined $temp and length $temp;
+if ($temp =~ /\./)
+  {
+  $parm_hostname = $temp;
+  }
+else
+  {
+  $parm_hostname = (gethostbyname($temp))[0];
+  $parm_hostname = "no.host.name.found" unless defined $parm_hostname and length $parm_hostname;
+  }
 print "Hostname is $parm_hostname\n";
 
 if ($parm_hostname !~ /\./)
@@ -3022,6 +3287,11 @@ 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)");
 }
+if ($parm_caller_gid eq $parm_exim_gid) {
+  tests_exit(-1, "Exim group ($parm_eximgroup,$parm_exim_gid) cannot be "
+                ."the same as caller's ($parm_caller) group as it confuses "
+                ."results analysis");
+}
 
 print "The Exim user needs access to the test suite directory. Checking ...";
 
@@ -3057,7 +3327,7 @@ else
 # because the current binary does not support the right facilities, and also
 # those that are outside the numerical range selected.
 
-print "\nTest range is $test_start to $test_end\n";
+print "\nTest range is $test_start to $test_end (flavour $flavour)\n";
 print "Omitting \${dlfunc expansion tests (loadable module not present)\n"
   if $dlfunc_deleted;
 print "Omitting dbm tests (unable to copy exim_dbmbuild)\n"
@@ -3163,7 +3433,7 @@ for ($i = 0; $i < @test_dirs; $i++)
 
   foreach $test (@testlist)
     {
-    next if $test !~ /^\d{4}$/;
+    next if $test !~ /^\d{4}(?:\.\d+)?$/;
     next if $test < $test_start || $test > $test_end;
     push @test_list, "$testdir/$test";
     }
@@ -3349,13 +3619,15 @@ foreach $test (@test_list)
   local($lineno) = 0;
   local($commandno) = 0;
   local($subtestno) = 0;
-  local($testno) = substr($test, -4);
+  (local $testno = $test) =~ s|.*/||;
   local($sortlog) = 0;
 
   my($gnutls) = 0;
   my($docheck) = 1;
   my($thistestdir) = substr($test, 0, -5);
 
+  $dynamic_socket->close() if $dynamic_socket;
+
   if ($lasttestdir ne $thistestdir)
     {
     $gnutls = 0;
@@ -3405,6 +3677,7 @@ foreach $test (@test_list)
   $stdout_skip = 0;
   $rmfiltertest = 0;
   $is_ipv6test = 0;
+  $TEST_STATE->{munge} = "";
 
   # Remove the associative arrays used to hold checked mail files and msglogs
 
@@ -3423,6 +3696,16 @@ foreach $test (@test_list)
     if (/^no_stdout_check/)  { $stdout_skip = 1; next; }
     if (/^rmfiltertest/)     { $rmfiltertest = 1; next; }
     if (/^sortlog/)          { $sortlog = 1; next; }
+    if (/\bPORT_DYNAMIC\b/)  {
+      for (my $port = 1024; $port < 65000; $port++) {
+        $dynamic_socket = IO::Socket::INET->new(
+          LocalHost => '127.0.0.1',
+          LocalPort => $port,
+          Listen => 10,
+          ReuseAddr => 1,
+        ) and last;
+      }
+      }
     }
   # Reset to beginning of file for per test interpreting/processing
   seek(SCRIPT, 0, 0);
@@ -3504,8 +3787,9 @@ foreach $test (@test_list)
 
     my($subtest_startline) = $lineno;
 
-    # Now run the command. The function returns 0 if exim was run and waited
-    # for, 1 if any other command was run and waited for, and 2 if a command
+    # Now run the command. The function returns 0 for an inline command,
+    # 1 if a non-exim command was run and waited for, 2 if an exim
+    # command was run and waited for, and 3 if a command
     # was run and not waited for (usually a daemon or server startup).
 
     my($commandname) = "";
@@ -3513,6 +3797,8 @@ foreach $test (@test_list)
     my($rc, $run_extra) = run_command($testno, \$subtestno, \$expectrc, \$commandname, $TEST_STATE);
     my($cmdrc) = $?;
 
+$0 = "[runtest $testno]";
+
     if ($debug) {
       print ">> rc=$rc cmdrc=$cmdrc\n";
       if (defined $run_extra) {
@@ -3566,7 +3852,15 @@ foreach $test (@test_list)
         $_ = $force_continue ? "c" : <T>;
         tests_exit(1) if /^q?$/i;
         log_failure($log_failed_filename, $testno, "exit code unexpected") if (/^c$/i && $force_continue);
-        print "... continue forced\n" if $force_continue;
+        if ($force_continue)
+          {
+          print "\nstderr tail:\n";
+          print "===================\n";
+          system("tail -20 test-stderr");
+          print "===================\n";
+          print "... continue forced\n";
+          }
+
         last if /^[rc]$/i;
         if (/^e$/i)
           {
@@ -3625,6 +3919,7 @@ foreach $test (@test_list)
 
   # The script has finished. Check the all the output that was generated. The
   # function returns 0 if all is well, 1 if we should rerun the test (the files
+  # 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)
@@ -3657,4 +3952,3 @@ tests_exit(-1, "No runnable tests selected") if @test_list == 0;
 tests_exit(0);
 
 # End of runtest script
-# vim: set sw=2 et :