Docs: SPF no longer Experimental
[users/heiko/exim.git] / test / runtest
index 745b360cf5e6e14528cd2f1ee47af3c0c9281c7a..5671fead24ab93a1ee3ff9f7cab4f39bacb6571d 100755 (executable)
@@ -16,9 +16,9 @@
 ###############################################################################
 
 #use strict;
-use 5.010;
-use feature 'state';   # included in 5.010
+use v5.10.1;
 use warnings;
+use if $^V >= v5.19.11, experimental => 'smartmatch';
 
 use Errno;
 use FileHandle;
@@ -26,12 +26,19 @@ use Socket;
 use Time::Local;
 use Cwd;
 use File::Basename;
+use Pod::Usage;
+use Getopt::Long;
 use FindBin qw'$RealBin';
 
 use lib "$RealBin/lib";
 use Exim::Runtest;
+use Exim::Utils qw(uniq numerically);
 
-use if $ENV{DEBUG} && $ENV{DEBUG} =~ /\bruntest\b/ => ('Smart::Comments' => '####');
+use if $ENV{DEBUG} && scalar($ENV{DEBUG} =~ /\bruntest\b/) => 'Smart::Comments' => '####';
+use if $ENV{DEBUG} && scalar($ENV{DEBUG} =~ /\bruntest\b/) => 'Data::Dumper';
+
+use constant TEST_TOP => 8999;
+use constant TEST_SPECIAL_TOP => 9999;
 
 
 # Start by initializing some global variables
@@ -68,11 +75,7 @@ my $have_ipv4 = 1;
 my $have_ipv6 = 1;
 my $have_largefiles = 0;
 
-my $test_start = 1;
-my $test_end = $test_top = 8999;
-my $test_special_top = 9999;
 my @test_list = ();
-my @test_dirs = ();
 
 
 # Networks to use for DNS tests. We need to choose some networks that will
@@ -484,9 +487,13 @@ RESET_AFTER_EXTRA_LINE_READ:
     /Tue, 2 Mar 1999 09:44:33 +0000/gx;
 
   # Date/time in logs and in one instance of a filter test
-  s/^\d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d(\s[+-]\d\d\d\d)?/1999-03-02 09:44:33/gx;
+  s/^\d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d(\s[+-]\d\d\d\d)?\s/1999-03-02 09:44:33 /gx;
+  s/^\d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d\.\d{3}(\s[+-]\d\d\d\d)?\s/2017-07-30 18:51:05.712 /gx;
   s/^Logwrite\s"\d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d/Logwrite "1999-03-02 09:44:33/gx;
 
+  s/((D|[QD]T)=)\d+s/$1qqs/g;
+  s/((D|[QD]T)=)\d\.\d{3}s/$1q.qqqs/g;
+
   # Date/time in message separators
   s/(?:[A-Z][a-z]{2}\s){2}\d\d\s\d\d:\d\d:\d\d\s\d\d\d\d
     /Tue Mar 02 09:44:33 1999/gx;
@@ -514,9 +521,6 @@ RESET_AFTER_EXTRA_LINE_READ:
   # Date/time in exim -bV output
   s/\d\d-[A-Z][a-z]{2}-\d{4}\s\d\d:\d\d:\d\d/07-Mar-2000 12:21:52/g;
 
-  # Time on queue tolerance
-  s/(QT|D)=1s/$1=0s/;
-
   # Eximstats heading
   s/Exim\sstatistics\sfrom\s\d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d\sto\s
     \d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d/Exim statistics from <time> to <time>/x;
@@ -541,16 +545,23 @@ RESET_AFTER_EXTRA_LINE_READ:
   # negotiating TLS 1.2 instead of 1.0.
   # Mail headers (...), log-lines X=..., client-ssl output ...
   # (and \b doesn't match between ' ' and '(' )
+  #
+  # Retain the authentication algorith field as we want to test that.
 
   s/( (?: (?:\b|\s) [\(=] ) | \s )TLSv1\.[12]:/$1TLSv1:/xg;
-  s/\bAES128-GCM-SHA256:128\b/AES256-SHA:256/g;
-  s/\bAES128-GCM-SHA256\b/AES256-SHA/g;
-  s/\bAES256-GCM-SHA384\b/AES256-SHA/g;
-  s/\bDHE-RSA-AES256-SHA\b/AES256-SHA/g;
+  s/((EC)?DHE-)?(RSA|ECDSA)-AES(128|256)-(GCM-SHA(256|384)|SHA)(?!:)/ke-$3-AES256-SHA/g;
+  s/((EC)?DHE-)?(RSA|ECDSA)-AES(128|256)-(GCM-SHA(256|384)|SHA):(128|256)/ke-$3-AES256-SHA:xxx/g;
 
   # LibreSSL
+  # TLSv1:AES256-GCM-SHA384:256
   # TLSv1:ECDHE-RSA-CHACHA20-POLY1305:256
-  s/\bECDHE-RSA-CHACHA20-POLY1305\b/AES256-SHA/g;
+  #
+  # ECDHE-RSA-CHACHA20-POLY1305
+  # AES256-GCM-SHA384
+
+  s/(?<!-)(AES256-GCM-SHA384)/RSA-$1/;
+  s/((EC)?DHE-)?(RSA|ECDSA)-(AES256|CHACHA20)-(GCM-SHA384|POLY1305)(?!:)/ke-$3-AES256-SHA/g;
+  s/((EC)?DHE-)?(RSA|ECDSA)-(AES256|CHACHA20)-(GCM-SHA384|POLY1305):256/ke-$3-AES256-SHA:xxx/g;
 
   # GnuTLS have seen:
   #   TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256
@@ -568,8 +579,8 @@ RESET_AFTER_EXTRA_LINE_READ:
   #   DHE-RSA-AES256-SHA
   # picking latter as canonical simply because regex easier that way.
   s/\bDHE_RSA_AES_128_CBC_SHA1:128/RSA_AES_256_CBC_SHA1:256/g;
-  s/TLS1.[012]:((EC)?DHE_)?RSA_AES_(256|128)_(CBC|GCM)_SHA(1|256|384):(256|128)/TLS1.x:xxxxRSA_AES_256_CBC_SHAnnn:256/g;
-  s/\b(ECDHE-RSA-AES256-SHA|DHE-RSA-AES256-SHA256)\b/AES256-SHA/g;
+  s/TLS1.[012]:((EC)?DHE_)?(RSA|ECDSA)_AES_(256|128)_(CBC|GCM)_SHA(1|256|384):(256|128)/TLS1.x:ke_$3_AES_256_CBC_SHAnnn:256/g;
+  s/\b(ECDHE-(RSA|ECDSA)-AES256-SHA|DHE-RSA-AES256-SHA256)\b/ke-$2-AES256-SHAxx/g;
 
   # GnuTLS library error message changes
   s/No certificate was found/The peer did not send any certificate/g;
@@ -899,7 +910,8 @@ RESET_AFTER_EXTRA_LINE_READ:
     next if /^SSL info:/;
     next if /SSL verify error: depth=0 error=certificate not trusted/;
     s/SSL3_READ_BYTES/ssl3_read_bytes/i;
-    s/^\d+:error:\d+(:SSL routines:ssl3_read_bytes:[^:]+:).*(:SSL alert number \d\d)$/pppp:error:dddddddd$1\[...\]$2/;
+    s/CONNECT_CR_FINISHED/ssl3_read_bytes/i;
+    s/^\d+:error:\d+(?:E\d+)?(:SSL routines:ssl3_read_bytes:[^:]+:).*(:SSL alert number \d\d)$/pppp:error:dddddddd$1\[...\]$2/;
 
     # gnutls version variances
     next if /^Error in the pull function./;
@@ -907,6 +919,9 @@ RESET_AFTER_EXTRA_LINE_READ:
     # optional IDN2 variant conversions.  Accept either IDN1 or IDN2
     s/conversion  strasse.de/conversion  xn--strae-oqa.de/;
     s/conversion: german.xn--strae-oqa.de/conversion: german.straße.de/;
+
+    # subsecond timstamp info in reported header-files
+    s/^(-received_time_usec \.)\d{6}$/$1uuuuuu/;
     }
 
   # ======== stderr ========
@@ -917,15 +932,19 @@ RESET_AFTER_EXTRA_LINE_READ:
 
     s/^Exim version .*/Exim version x.yz ..../;
 
-    # Debugging lines for Exim terminations
+    # Debugging lines for Exim terminations and process-generation
 
     s/(?<=^>>>>>>>>>>>>>>>> Exim pid=)\d+(?= terminating)/pppp/;
+    s/^(proxy-proc \w{5}-pid) \d+$/$1 pppp/;
 
     # IP address lookups use gethostbyname() when IPv6 is not supported,
     # and gethostbyname2() or getipnodebyname() when it is.
 
     s/\b(gethostbyname2?|\bgetipnodebyname)(\(af=inet\))?/get[host|ipnode]byname[2]/;
 
+    # we don't care what TZ enviroment the testhost was running
+    next if /^Reset TZ to/;
+
     # drop gnutls version strings
     next if /GnuTLS compile-time version: \d+[\.\d]+$/;
     next if /GnuTLS runtime version: \d+[\.\d]+$/;
@@ -1074,9 +1093,9 @@ RESET_AFTER_EXTRA_LINE_READ:
     if (s/(with \$received_protocol)\}\} \$\{if def:tls_cipher \{\(\$tls_cipher\)\n$/$1/)
       {
       $_ .= <IN>;
-      s/\s+\}\}(?=\(Exim )/\}\} /;
+      s/[\s╎]+\}\}(?=\(Exim )/\}\} /;
       }
-    if (/^  condition: def:tls_cipher$/)
+    if (/^ ├──condition: def:tls_cipher$/)
       {
       <IN>; <IN>; <IN>; <IN>; <IN>; <IN>;
       <IN>; <IN>; <IN>; <IN>; <IN>; next;
@@ -1085,6 +1104,9 @@ RESET_AFTER_EXTRA_LINE_READ:
     # Not all platforms build with DKIM enabled
     next if /^PDKIM >> Body data for hash, canonicalized/;
 
+    # Not all platforms have sendfile support
+    next if /^cannot use sendfile for body: no support$/;
+
     #  Parts of DKIM-specific debug output depend on the time/date
     next if /^date:\w+,\{SP\}/;
     next if /^PDKIM \[[^[]+\] (Header hash|b) computed:/;
@@ -1100,6 +1122,10 @@ RESET_AFTER_EXTRA_LINE_READ:
 
     next if /^(ppppp )?setsockopt FASTOPEN: Protocol not available$/;
 
+    # Specific pointer values reported for DB operations change from run to run
+    s/^(returned from EXIM_DBOPEN: )(0x)?[0-9a-f]+/${1}0xAAAAAAAA/;
+    s/^(EXIM_DBCLOSE.)(0x)?[0-9a-f]+/${1}0xAAAAAAAA/;
+
     # 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
@@ -1132,6 +1158,7 @@ RESET_AFTER_EXTRA_LINE_READ:
                 /^Support for:/ ||
                 /^Routers:/ ||
                 /^Transports:/ ||
+                /^Malware:/ ||
                 /^log selectors =/ ||
                 /^cwd=/ ||
                 /^Fixed never_users:/ ||
@@ -1151,6 +1178,13 @@ RESET_AFTER_EXTRA_LINE_READ:
     {
     # Berkeley DB version differences
     next if / Berkeley DB error: /;
+
+    # CHUNKING: exact sizes depend on hostnames in headers
+    s/(=>.* K C="250- \d)\d+ (byte chunk, total \d)\d+/$1nn $2nn/;
+
+    # openssl version variances
+    s/(TLS error on connection [^:]*: error:)[0-9A-F]{8}(:system library):(?:fopen|func\(4095\)):(No such file or directory)$/$1xxxxxxxx$2:fopen:$3/;
+    s/(DANE attempt failed.*error:)[0-9A-F]{8}(:SSL routines:)(ssl3_get_server_certificate|tls_process_server_certificate|CONNECT_CR_CERT)(?=:certificate verify failed$)/$1xxxxxxxx$2ssl3_get_server_certificate/;
     }
 
   # ======== All files other than stderr ========
@@ -1510,13 +1544,14 @@ $munges =
 
     'optional_config' =>
     { 'stdout' => '/^(
-                  dkim_(canon|domain|private_key|selector|sign_headers|strict)
+                  dkim_(canon|domain|private_key|selector|sign_headers|strict|hash|identity)
                   |gnutls_require_(kx|mac|protocols)
                   |hosts_(requ(est|ire)|try)_(dane|ocsp)
-                  |hosts_(avoid|nopass|require|verify_avoid)_tls
+                  |hosts_(avoid|nopass|noproxy|require|verify_avoid)_tls
                   |socks_proxy
                   |tls_[^ ]*
-                  )($|[ ]=)/x' },
+                  )($|[ ]=)/x'
+    },
 
     'sys_bindir' =>
     { 'mainlog' => 's%/(usr/(local/)?)?bin/%SYSBINDIR/%' },
@@ -1532,6 +1567,12 @@ $munges =
 
     'timeout_errno' =>         # actual errno differs Solaris vs. Linux
     { 'mainlog' => 's/(host deferral .* errno) <\d+> /$1 <EEE> /' },
+
+    'peer_terminated_conn' =>  # actual error differs FreedBSD vs. Linux
+    { 'stderr' => 's/^(  SMTP\()Connection reset by peer(\)<<)$/$1closed$2/' },
+
+    'perl_variants' =>         # result of hash-in-scalar-context changed from bucket-fill to keycount
+    { 'stdout' => 's%^> X/X$%> X%' },
   };
 
 
@@ -2320,7 +2361,6 @@ elsif (/^((?i:[A-Z\d_]+=\S+\s+)+)?(\d+)?\s*(sudo(?:\s+-u\s+(\w+))?\s+)?exim(_\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");
@@ -2347,7 +2387,8 @@ elsif (/^((?i:[A-Z\d_]+=\S+\s+)+)?(\d+)?\s*(sudo(?:\s+-u\s+(\w+))?\s+)?exim(_\S+
     while (<SCRIPT>) { $lineno++; last if /^\*{4}\s*$/; }   # Ignore any input
 
     # Interlock with daemon startup
-    while (! stat("$pidfile") ) { select(undef, undef, undef, 0.3); }
+    for (my $count = 0; ! stat("$pidfile") && $count < 30; $count++ )
+      { select(undef, undef, undef, 0.3); }
     return 3;                                     # Don't wait
     }
   elsif ($cmd =~ /\s-DSERVER=wait:(\d+)\s/)
@@ -2388,6 +2429,7 @@ elsif (/^background$/)
 
   $_ = <SCRIPT>; $lineno++;
   chomp;
+  do_substitute($testno);
   $line = $_;
   if ($debug) { printf ">> daemon: $line >>test-stdout 2>>test-stderr\n"; }
 
@@ -2500,22 +2542,6 @@ $more = 'more' if system('which less >/dev/null 2>&1') != 0;
 
 
 
-##################################################
-#        Check for sudo access to root           #
-##################################################
-
-print "You need to have sudo access to root to run these tests. Checking ...\n";
-if (system('sudo true >/dev/null') != 0)
-  {
-  die "** Test for sudo failed: testing abandoned.\n";
-  }
-else
-  {
-  print "Test for sudo OK\n";
-  }
-
-
-
 ##################################################
 #      See if an Exim binary has been given      #
 ##################################################
@@ -2524,10 +2550,6 @@ else
 # 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) = Exim::Runtest::exim_binary(@ARGV);
-print "Exim binary is $parm_exim\n" if $parm_exim ne '';
-
-
 
 ##################################################
 # Sort out options and which tests are to be run #
@@ -2537,39 +2559,60 @@ print "Exim binary is $parm_exim\n" if $parm_exim ne '';
 # options are passed on to Exim calls within the tests. Typically, this is used
 # to turn on Exim debugging while setting up a test.
 
-while (@ARGV > 0 && $ARGV[0] =~ /^-/)
-  {
-  my($arg) = shift @ARGV;
-  if ($optargs eq '')
-    {
-    if ($arg eq "-DEBUG")  { $debug = 1; $cr = "\n"; next; }
-    if ($arg eq "-DIFF")   { $cf = "diff -u"; next; }
-    if ($arg eq "-CONTINUE"){$force_continue = 1;
-                             $more = "cat";
-                             next; }
-    if ($arg eq "-UPDATE") { $force_update = 1; next; }
-    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 "-SLOW")   { $slow = 1; next; }
-    if ($arg eq "-VALGRIND")   { $valgrind = 1; next; }
-    if ($arg =~ /^-FLAVOU?R$/) { $flavour = shift; next; }
-    }
-  $optargs .= " $arg";
-  }
+Getopt::Long::Configure qw(no_getopt_compat);
+GetOptions(
+    'debug'    => sub { $debug          = 1; $cr   = "\n" },
+    'diff'     => sub { $cf             = 'diff -u' },
+    'continue' => sub { $force_continue = 1; $more = 'cat' },
+    'update'   => \$force_update,
+    'ipv4!'    => \$have_ipv4,
+    'ipv6!'    => \$have_ipv6,
+    'keep'     => \$save_output,
+    'slow'     => \$slow,
+    'valgrind' => \$valgrind,
+    'range=s{2}'       => \my @range_wanted,
+    'test=i@'          => \my @tests_wanted,
+    'flavor|flavour=s' => $flavour,
+    'help'             => sub { pod2usage(-exit => 0) },
+    'man'              => sub {
+        pod2usage(
+            -exit      => 0,
+            -verbose   => 2,
+            -noperldoc => system('perldoc -V 2>/dev/null 1>&2')
+        );
+    },
+) or pod2usage;
+
+($parm_exim, @ARGV) = Exim::Runtest::exim_binary(@ARGV);
+print "Exim binary is `$parm_exim'\n" if defined $parm_exim;
+
 
-# Any subsequent arguments are a range of test numbers.
+my @wanted = sort numerically uniq
+  @tests_wanted ? @tests_wanted : (),
+  @range_wanted ? $range_wanted[0] .. $range_wanted[1] : (),
+  @ARGV ? @ARGV == 1 ? $ARGV[0] :
+          $ARGV[1] eq '+' ? $ARGV[0]..($ARGV[0] >= 9000 ? TEST_SPECIAL_TOP : TEST_TOP) :
+          0+$ARGV[0]..0+$ARGV[1]    # add 0 to cope with test numbers starting with zero
+        : ();
+@wanted = 1..TEST_TOP if not @wanted;
+
+##################################################
+#        Check for sudo access to root           #
+##################################################
 
-if (@ARGV > 0)
+print "You need to have sudo access to root to run these tests. Checking ...\n";
+if (system('sudo true >/dev/null') != 0)
+  {
+  die "** Test for sudo failed: testing abandoned.\n";
+  }
+else
   {
-  $test_end = $test_start = $ARGV[0];
-  $test_end = $ARGV[1] if (@ARGV > 1);
-  $test_end = ($test_start >= 9000)? $test_special_top : $test_top
-    if $test_end eq "+";
-  die "** Test numbers out of order\n" if ($test_end < $test_start);
+  print "Test for sudo OK\n";
   }
 
 
+
+
 ##################################################
 #      Make the command's directory current      #
 ##################################################
@@ -2594,7 +2637,7 @@ $parm_cwd = Cwd::getcwd();
 
 # If $parm_exim is still empty, ask the caller
 
-if ($parm_exim eq '')
+if (not $parm_exim)
   {
   print "** Did not find an Exim binary to test\n";
   for ($i = 0; $i < 5; $i++)
@@ -2707,7 +2750,7 @@ if (defined $parm_trusted_config_list)
   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>;
+  if not grep { /^\Q$test_config\E$/ } <TCL>;
   }
 else
   {
@@ -2721,6 +2764,9 @@ die "CONFIGURE_GROUP ($parm_configure_group) does not match the group invoking $
        if 0020 & (stat "$parm_cwd/test-config")[2]
        and $parm_configure_group != $);
 
+die "aux-fixed file is world-writeable; best to strip them all, recursively\n"
+       if 0020 & (stat "aux-fixed/0037.f-1")[2];
+
 
 open(EXIMINFO, "$parm_exim -d-all+transport -bV -C $parm_cwd/test-config -DDIR=$parm_cwd |") ||
   die "** Cannot run $parm_exim: $!\n";
@@ -2795,6 +2841,15 @@ while (<EXIMINFO>)
         }
       }
     }
+
+  elsif (/^Malware: (.*)/)
+    {
+    print;
+    @temp = split /(\s+)/, $1;
+    push(@temp, ' ');
+    %parm_malware = @temp;
+    }
+
   }
 close(EXIMINFO);
 print "-" x 78, "\n";
@@ -3105,6 +3160,12 @@ unless (defined $parm_eximgroup)
   die "** ABANDONING.\n";
   }
 
+if ($parm_caller_home eq $parm_cwd)
+  {
+  print "will confuse working dir with homedir; change homedir\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/)
@@ -3287,7 +3348,6 @@ system("sudo cp eximdir/exim eximdir/exim_exim;" .
        "sudo chgrp $parm_eximgroup eximdir/exim_exim;" .
        "sudo chmod 06755 eximdir/exim_exim");
 
-
 ##################################################
 #     Make copies of utilities we might need     #
 ##################################################
@@ -3330,6 +3390,15 @@ if (system("cp $parm_exim_dir/eximstats eximdir") != 0)
   tests_exit(-1, "Failed to make a copy of eximstats: $!");
   }
 
+# Collect some version information
+print '-' x 78, "\n";
+print "Perl version for runtest: $]\n";
+foreach (map { "./eximdir/$_" } qw(exigrep exinext eximstats)) {
+  # fold (or unfold?) multiline output into a one-liner
+  print join(', ', map { chomp; $_ } `$_ --version`), "\n";
+}
+print '-' x 78, "\n";
+
 
 ##################################################
 #    Check that the Exim user can access stuff   #
@@ -3372,6 +3441,8 @@ else
   print " OK\n";
   }
 
+tests_exit(-1, "Failed to unlink $log_summary_filename: $!")
+  if not unlink($log_summary_filename) and -e $log_summary_filename;
 
 ##################################################
 #        Create a list of available tests        #
@@ -3385,33 +3456,21 @@ 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 (flavour $flavour)\n";
+printf "\nWill run %d tests between %d and %d for flavour %s\n",
+  scalar(@wanted), $wanted[0], $wanted[-1], $flavour;
+
 print "Omitting \${dlfunc expansion tests (loadable module not present)\n"
   if $dlfunc_deleted;
 print "Omitting dbm tests (unable to copy exim_dbmbuild)\n"
   if $dbm_build_deleted;
 
-opendir(DIR, "scripts") || tests_exit(-1, "Failed to opendir(\"scripts\"): $!");
-@test_dirs = sort readdir(DIR);
-closedir(DIR);
-
-# Remove . and .. and CVS from the list.
 
-for ($i = 0; $i < @test_dirs; $i++)
-  {
-  my($d) = $test_dirs[$i];
-  if ($d eq "." || $d eq ".." || $d eq "CVS")
-    {
-    splice @test_dirs, $i, 1;
-    $i--;
-    }
-  }
+my @test_dirs = grep { not /^CVS$/ } map { basename $_ } glob 'scripts/*'
+  or die tests_exit(-1, "Failed to find test scripts in 'scripts/*`: $!");
 
 # Scan for relevant tests
-
-tests_exit(-1, "Failed to unlink $log_summary_filename")
-  if (-e $log_summary_filename && !unlink($log_summary_filename));
-for ($i = 0; $i < @test_dirs; $i++)
+# HS12: Needs to be reworked.
+DIR: for (my $i = 0; $i < @test_dirs; $i++)
   {
   my($testdir) = $test_dirs[$i];
   my($wantthis) = 1;
@@ -3421,19 +3480,19 @@ for ($i = 0; $i < @test_dirs; $i++)
   # Skip this directory if the first test is equal or greater than the first
   # test in the next directory.
 
-  next if ($i < @test_dirs - 1) &&
-          ($test_start >= substr($test_dirs[$i+1], 0, 4));
+  next DIR if ($i < @test_dirs - 1) &&
+          ($wanted[0] >= substr($test_dirs[$i+1], 0, 4));
 
   # No need to carry on if the end test is less than the first test in this
   # subdirectory.
 
-  last if $test_end < substr($testdir, 0, 4);
+  last DIR if $wanted[-1] < substr($testdir, 0, 4);
 
   # Check requirements, if any.
 
-  if (open(REQUIRES, "scripts/$testdir/REQUIRES"))
+  if (open(my $requires, "scripts/$testdir/REQUIRES"))
     {
-    while (<REQUIRES>)
+    while (<$requires>)
       {
       next if /^\s*$/;
       s/\s+$//;
@@ -3461,12 +3520,15 @@ for ($i = 0; $i < @test_dirs; $i++)
         {
         if (!defined $parm_transports{$1}) { $wantthis = 0; last; }
         }
+      elsif (/^malware (.*)$/)
+        {
+        if (!defined $parm_malware{$1}) { $wantthis = 0; last; }
+        }
       else
         {
         tests_exit(-1, "Unknown line in \"scripts/$testdir/REQUIRES\": \"$_\"");
         }
       }
-    close(REQUIRES);
     }
   else
     {
@@ -3485,15 +3547,13 @@ for ($i = 0; $i < @test_dirs; $i++)
   # We want the tests from this subdirectory, provided they are in the
   # range that was selected.
 
-  opendir(SUBDIR, "scripts/$testdir") ||
-    tests_exit(-1, "Failed to opendir(\"scripts/$testdir\"): $!");
-  @testlist = sort readdir(SUBDIR);
-  close(SUBDIR);
+  @testlist = grep { $_ ~~ @wanted } grep { /^\d+(?:\.\d+)?$/ } map { basename $_ } glob "scripts/$testdir/*";
+  tests_exit(-1, "Failed to read test scripts from `scripts/$testdir/*': $!")
+    if not @testlist;
 
   foreach $test (@testlist)
     {
-    next if ($test !~ /^\d{4}(?:\.\d+)?$/);
-    if (!$wantthis || $test < $test_start || $test > $test_end)
+    if (!$wantthis)
       {
       log_test($log_summary_filename, $test, '.');
       }
@@ -3504,7 +3564,7 @@ for ($i = 0; $i < @test_dirs; $i++)
     }
   }
 
-print ">>Test List: @test_list\n", if $debug;
+print ">>Test List:\n", join "\n", @test_list, '' if $debug;
 
 
 ##################################################
@@ -3674,27 +3734,31 @@ closedir(DIR);
 if (not $force_continue) {
   # runtest needs to interact if we're not in continue
   # mode. It does so by communicate to /dev/tty
-  open(T, "/dev/tty") or tests_exit(-1, "Failed to open /dev/tty: $!");
+  open(T, '<', '/dev/tty') or tests_exit(-1, "Failed to open /dev/tty: $!");
+  print "\nPress RETURN to run the tests: ";
+  <T>;
 }
 
 
-print "\nPress RETURN to run the tests: ";
-$_ = $force_continue ? "c" : <T>;
-print "\n";
-
-$lasttestdir = '';
-
 foreach $test (@test_list)
   {
-  local($lineno) = 0;
-  local($commandno) = 0;
-  local($subtestno) = 0;
+  state $lasttestdir = '';
+
+  local $lineno     = 0;
+  local $commandno  = 0;
+  local $subtestno  = 0;
+  local $sortlog    = 0;
+
   (local $testno = $test) =~ s|.*/||;
-  local($sortlog) = 0;
 
-  my($gnutls) = 0;
-  my($docheck) = 1;
-  my($thistestdir) = substr($test, 0, -5);
+  # Leaving traces in the process table and in the environment
+  # gives us a chance to identify hanging processes (exim daemons)
+  local $0 = "[runtest $testno]";
+  local $ENV{EXIM_TEST_NUMBER} = $testno;
+
+  my $gnutls   = 0;
+  my $docheck  = 1;
+  my $thistestdir  = substr($test, 0, -5);
 
   $dynamic_socket->close() if $dynamic_socket;
 
@@ -3703,20 +3767,19 @@ foreach $test (@test_list)
     $gnutls = 0;
     if (-s "scripts/$thistestdir/REQUIRES")
       {
-      my($indent) = '';
+      my $indent = '';
       print "\n>>> The following tests require: ";
-      open(IN, "scripts/$thistestdir/REQUIRES") ||
-        tests_exit(-1, "Failed to open scripts/$thistestdir/REQUIRES: $1");
-      while (<IN>)
+      open(my $requires, '<', "scripts/$thistestdir/REQUIRES") ||
+        tests_exit(-1, "Failed to open scripts/$thistestdir/REQUIRES: $!");
+      while (<$requires>)
         {
         $gnutls = 1 if /^support GnuTLS/;
         print $indent, $_;
         $indent = ">>>                              ";
         }
-      close(IN);
       }
+      $lasttestdir = $thistestdir;
     }
-  $lasttestdir = $thistestdir;
 
   # Remove any debris in the spool directory and the test-mail directory
   # and also the files for collecting stdout and stderr. Then put back
@@ -3853,8 +3916,6 @@ foreach $test (@test_list)
     # command was run and waited for, and 3 if a command
     # was run and not waited for (usually a daemon or server startup).
 
-    $0 = "[runtest $testno]";
-
     my($commandname) = '';
     my($expectrc) = 0;
     my($rc, $run_extra) = run_command($testno, \$subtestno, \$expectrc, \$commandname, $TEST_STATE);
@@ -3918,8 +3979,12 @@ foreach $test (@test_list)
        }
         if ($force_continue)
           {
-          print "\nstderr tail:\n";
+          print "\nstdout tail:\n";
+          print "==================>\n";
+          system("tail -20 test-stdout");
           print "===================\n";
+          print "stderr tail:\n";
+          print "==================>\n";
           system("tail -20 test-stderr");
           print "===================\n";
           print "... continue forced\n";
@@ -4019,7 +4084,84 @@ foreach $test (@test_list)
 #         Exit from the test script              #
 ##################################################
 
-tests_exit(-1, "No runnable tests selected") if @test_list == 0;
+tests_exit(-1, "No runnable tests selected") if not @test_list;
 tests_exit(0);
 
+__END__
+
+=head1 NAME
+
+ runtest - run the exim testsuite
+
+=head1 SYNOPSIS
+
+ runtest [exim-path] [options] [test0 [test1]]
+
+=head1 DESCRIPTION
+
+B<runtest> runs the Exim testsuite.
+
+=head1 OPTIONS
+
+For legacy reasons the options are not case sensitive.
+
+=over
+
+=item B<--continue>
+
+Do not stop for user interaction or on errors. (default: off)
+
+=item B<--debug>
+
+This option enables the output of debug information when running the
+various test commands. (default: off)
+
+=item B<--diff>
+
+Use C<diff -u> for comparing the expected output with the produced
+output. (default: use a built-in routine)
+
+=item B<--flavor>|B<--flavour> I<flavour>
+
+Override the expected results for results for a specific (OS) flavour.
+(default: unused)
+
+=item B<--[no]ipv4>
+
+Skip IPv4 related setup and tests (default: use ipv4)
+
+=item B<--[no]ipv6>
+
+Skip IPv6 related setup and tests (default: use ipv6)
+
+=item B<--keep>
+
+Keep the various output files produced during a test run. (default: don't keep)
+
+=item B<--range> I<n0> I<n1>
+
+Run tests between (including) I<n0> and I<n1>. A "+" may be used to specify the "last
+test available".
+
+=item B<--slow>
+
+Insert some delays to compensate for a slow host system. (default: off)
+
+=item B<--test> I<n>
+
+Run the specified test. This option may used multiple times.
+
+=item B<--update>
+
+Automatically update the recorded (expected) data on mismatch. (default: off)
+
+=item B<--valgrind>
+
+Start Exim wrapped by I<valgrind>. (default: don't use valgrind)
+
+=back
+
+=cut
+
+
 # End of runtest script