Testsuite: Add PORT_DYNAMIC (Bug 1775)
[exim.git] / test / runtest
index 724ccd9edfd8372376fa8038d71aefaacad80932..a66c589f822a18ba796040152e6c91bb9d4f4d46 100755 (executable)
 ###############################################################################
 
 #use strict;
+#use 5.010;
 use Errno;
 use FileHandle;
+use IO::Socket::INET;
 use Socket;
 use Time::Local;
 use Cwd;
@@ -38,6 +40,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";
@@ -45,7 +48,7 @@ $more = "less -XF";
 $optargs = "";
 $save_output = 0;
 $server_opts = "";
-$flavour = 'FOO';
+$valgrind = 0;
 
 $have_ipv4 = 1;
 $have_ipv6 = 1;
@@ -79,15 +82,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;
 
 ###############################################################################
 ###############################################################################
@@ -137,6 +144,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;
 }
 
 
@@ -348,7 +356,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
@@ -426,7 +434,7 @@ RESET_AFTER_EXTRA_LINE_READ:
 
   # 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 ========
@@ -652,6 +660,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.
 
@@ -668,6 +679,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
@@ -771,6 +786,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
@@ -1002,6 +1018,29 @@ 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/;
+
     # 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
@@ -1023,20 +1062,6 @@ RESET_AFTER_EXTRA_LINE_READ:
         @saved = ();
         }
 
-    # 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\)/;
-
       # Skip some lines that Exim puts out at the start of debugging output
       # because they will be different in different binaries.
 
@@ -1051,6 +1076,7 @@ RESET_AFTER_EXTRA_LINE_READ:
                 /^log selectors =/ ||
                 /^cwd=/ ||
                 /^Fixed never_users:/ ||
+               /^Configure owner:/ ||
                 /^Size of off_t:/
                 );
 
@@ -1392,9 +1418,27 @@ $munges =
     { '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/'
+    },
   };
 
 
@@ -1670,6 +1714,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];
@@ -1935,7 +1981,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;
@@ -1956,6 +2002,7 @@ 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+(.*)$/)
   {
@@ -2073,7 +2120,7 @@ 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+-u\s+(\w+))?\s+)?exim(_\S+)?\s+(.*)$/)
+elsif (/^((?i:[A-Z\d_]+=\S+\s+)+)?(\d+)?\s*(sudo(?:\s+-u\s+(\w+))?\s+)?exim(_\S+)?\s+(.*)$/)
   {
   $args = $6;
   my($envset) = (defined $1)? $1      : "";
@@ -2134,7 +2181,10 @@ elsif (/^([A-Z_]+=\S+\s+)?(\d+)?\s*(sudo(?:\s+-u\s+(\w+))?\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";
@@ -2183,31 +2233,24 @@ elsif (/^([A-Z_]+=\S+\s+)?(\d+)?\s*(sudo(?:\s+-u\s+(\w+))?\s+)?exim(_\S+)?\s+(.*
     }
   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
@@ -2304,7 +2347,7 @@ return $yield;            # Ran command and waited
 ###############################################################################
 ###############################################################################
 
-# Here beginneth the Main Program ...
+# Here begins the Main Program ...
 
 ###############################################################################
 ###############################################################################
@@ -2334,7 +2377,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;
 
 
 
@@ -2343,7 +2386,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";
   }
@@ -2389,6 +2432,7 @@ 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";
@@ -2522,9 +2566,22 @@ open(EXIMINFO, "$parm_exim -d -C $parm_cwd/test-config -DDIR=$parm_cwd " .
   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);
@@ -2576,7 +2633,15 @@ else
   die "Unable to check the TRUSTED_CONFIG_LIST, seems to be empty?\n";
   }
 
-open(EXIMINFO, "$parm_exim -bV -C $parm_cwd/test-config -DDIR=$parm_cwd |") ||
+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";
@@ -2585,7 +2650,7 @@ while (<EXIMINFO>)
   {
   my(@temp);
 
-  if (/^Exim version/) { print; }
+  if (/^(Exim|Library) version/) { print; }
 
   elsif (/^Size of off_t: (\d+)/)
     {
@@ -2857,7 +2922,7 @@ if (defined $parm_support{'Content_Scanning'})
 ##################################################
 #       Check for redis                          #
 ##################################################
-if (defined $parm_support{'Experimental_Redis'})
+if (defined $parm_lookups{'redis'})
   {
   if (system("redis-server -v 2>/dev/null >/dev/null") == 0)
     {
@@ -2974,39 +3039,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\./ || $ip =~ /^10\./);
-    $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
@@ -3015,7 +3067,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>";
@@ -3031,7 +3083,7 @@ else
   $parm_running{"IPv4"} = " ";
   }
 
-if ($parm_ipv6 eq "")
+if (not $parm_ipv6)
   {
   $have_ipv6 = 0;
   $parm_ipv6 = "<no IPv6 address found>";
@@ -3079,8 +3131,9 @@ if ($parm_ipv6 =~ /^[\da-f]/)
 # Find the host name, fully qualified.
 
 chomp($temp = `hostname`);
+die "'hostname' didn't return anything\n" unless defined $temp and length $temp;
 $parm_hostname = (gethostbyname($temp))[0];
-$parm_hostname = "no.host.name.found" if $parm_hostname eq "";
+$parm_hostname = "no.host.name.found" unless defined $parm_hostname and length $parm_hostname;
 print "Hostname is $parm_hostname\n";
 
 if ($parm_hostname !~ /\./)
@@ -3204,6 +3257,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 ...";
 
@@ -3538,6 +3596,8 @@ foreach $test (@test_list)
   my($docheck) = 1;
   my($thistestdir) = substr($test, 0, -5);
 
+  $dynamic_socket->close() if $dynamic_socket;
+
   if ($lasttestdir ne $thistestdir)
     {
     $gnutls = 0;
@@ -3606,6 +3666,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);
@@ -3687,8 +3757,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) = "";
@@ -3751,7 +3822,15 @@ $0 = "[runtest $testno]";
         $_ = $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)
           {