#! /usr/bin/perl -w
-# $Cambridge: exim/test/runtest,v 1.26 2007/04/17 13:05:41 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 #
###############################################################################
-require Cwd;
+#use strict;
use Errno;
use FileHandle;
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
-$testversion = "4.67 (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*
+# We also clamp it because of NSS interop, see addition of tls_dh_max_bits.
+# This value is correct as of GnuTLS 2.12.18 as clamped by tls_dh_max_bits.
+# normal = 2432 tls_dh_max_bits = 2236
+$gnutls_dh_bits_normal = 2236;
-$cf = "bin/cf";
+$cf = "bin/cf -exact";
$cr = "\r";
$debug = 0;
+$flavour = 'FOO';
+$force_continue = 0;
$force_update = 0;
+$log_failed_filename = "failed-summary.log";
$more = "less -XF";
$optargs = "";
$save_output = 0;
$server_opts = "";
+$valgrind = 0;
$have_ipv4 = 1;
$have_ipv6 = 1;
$parm_port_d3 = 1227; # Additional for daemon
$parm_port_d4 = 1228; # Additional for daemon
+# Manually set locale
+$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};
###############################################################################
}
+##################################################
+# Any state to be preserved across tests #
+##################################################
+
+my $TEST_STATE = {};
+
##################################################
# Subroutine to tidy up and exit #
# 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 -INT $pid");
+ }
+
if (opendir(DIR, "spool"))
{
my(@spools) = sort readdir(DIR);
chomp($pid = <PID>);
close(PID);
print "Tidyup: killing daemon pid=$pid\n";
- system("sudo rm -f spool/$spool; sudo kill -SIGINT $pid");
+ system("sudo rm -f spool/$spool; sudo kill -INT $pid");
}
}
else
system("sudo /bin/rm -rf ./spool test-* ./dnszones/*")
if ($rc == 0 && !$save_output);
-system("sudo /bin/rm -rf ./eximdir/*");
+system("sudo /bin/rm -rf ./eximdir/*")
+ if (!$save_output);
+
+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";
}
}
-# 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);
}
sub munge {
my($file) = $_[0];
+my($extra) = $_[1];
my($yield) = 0;
my(@saved) = ();
+local $_;
+
open(IN, "$file") || tests_exit(-1, "Failed to open $file: $!");
my($is_log) = $file =~ /log/;
# 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
+ if ($extra)
+ {
+ next if $extra =~ m%^/% && eval $extra;
+ eval $extra if $extra =~ m/^s/;
+ }
+
# Check for "*** truncated ***"
$yield = 1 if /\*\*\* truncated \*\*\*/;
# But convert "name=the.local.host address=127.0.0.1" to use "localhost"
s/name=the\.local\.host address=127\.0\.0\.1/name=localhost address=127.0.0.1/g;
+ # The name of the shell may vary
+ s/\s\Q$parm_shell\E\b/ ENV_SHELL/;
+
# Replace the path to the testsuite directory
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})
s/TlRMTVNTUAACAAAAAAAAAAAoAAABgg[\w+\/]+/TlRMTVNTUAACAAAAAAAAAAAoAAABggAAAEbBRwqFwwIAAAAAAAAAAAAt1sgAAAAA/;
# PRVS values
- s?prvs=([^/]+)/[\da-f]{10}@?prvs=$1/xxxxxxxxxx@?g;
+ s?prvs=([^/]+)/[\da-f]{10}@?prvs=$1/xxxxxxxxxx@?g; # Old form
+ s?prvs=[\da-f]{10}=([^@]+)@?prvs=xxxxxxxxxx=$1@?g; # New form
# Error lines on stdout from SSL contain process id values and file names.
# They also contain a source file name and line number, which may vary from
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/;
# The message for a non-listening FIFO varies
s/:[^:]+: while opening named pipe/: Error: while opening named pipe/;
- # The name of the shell may vary
- s/\s\Q$parm_shell\E\b/ SHELL/;
-
# Debugging output of lists of hosts may have different sort keys
s/sort=\S+/sort=xx/ if /^\S+ (?:\d+\.){3}\d+ mx=\S+ sort=\S+/;
# 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 ========
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=1s/QT=0s/;
+ 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;
-
- # ======== Caller's login, uid, gid, home ========
+ # Treat ECONNRESET the same as ECONNREFUSED. At least some systems give
+ # us the former on a new connection.
+ s/(could not connect to .*: Connection) reset by peer$/$1 refused/;
+
+ # ======== 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:AES128-GCM-SHA256:128
+ # TLSv1:AES256-SHA:256
+ # TLSv1.1:AES256-SHA:256
+ # TLSv1.2:AES256-GCM-SHA384:256
+ # TLSv1.2:DHE-RSA-AES256-SHA:256
+ # TLS1.2:DHE_RSA_AES_128_CBC_SHA1:128
+ # 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\.[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;
+
+ # GnuTLS have seen:
+ # TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256
+ # TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128
+ # TLS1.2:RSA_AES_256_CBC_SHA1:256 (canonical)
+ # TLS1.2:DHE_RSA_AES_128_CBC_SHA1:128
+ #
+ # X=TLS1.2:DHE_RSA_AES_256_CBC_SHA256:256
+ # X=TLS1.2:RSA_AES_256_CBC_SHA1:256
+ # X=TLS1.1:RSA_AES_256_CBC_SHA1:256
+ # X=TLS1.0:DHE_RSA_AES_256_CBC_SHA1:256
+ # and as stand-alone cipher:
+ # ECDHE-RSA-AES256-SHA
+ # DHE-RSA-AES256-SHA256
+ # 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;
+
+ # GnuTLS library error message changes
+ s/No certificate was found/The peer did not send any certificate/g;
+#(dodgy test?) s/\(certificate verification failed\): invalid/\(gnutls_handshake\): The peer did not send any certificate./g;
+ s/\(gnutls_priority_set\): No or insufficient priorities were set/\(gnutls_handshake\): Could not negotiate a supported cipher suite/g;
+
+ # (this new one is a generic channel-read error, but the testsuite
+ # only hits it in one place)
+ s/TLS error on connection \(gnutls_handshake\): Error in the pull function\./a TLS session is required but an attempt to start TLS failed/g;
+
+ # (replace old with new, hoping that old only happens in one situation)
+ s/TLS error on connection to \d{1,3}(.\d{1,3}){3} \[\d{1,3}(.\d{1,3}){3}\] \(gnutls_handshake\): A TLS packet with unexpected length was received./a TLS session is required for ip4.ip4.ip4.ip4 [ip4.ip4.ip4.ip4], but an attempt to start TLS failed/g;
+ s/TLS error on connection from \[127.0.0.1\] \(recv\): A TLS packet with unexpected length was received./TLS error on connection from [127.0.0.1] (recv): The TLS connection was non-properly terminated./g;
+
+ # signature algorithm names
+ s/RSA-SHA1/RSA-SHA/;
+
+
+ # ======== 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!
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"?/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.
# ======== General uids, gids, and pids ========
# Note: this must come after munges for caller's and exim's uid/gid
+ # These are for systems where long int is 64
+ s/\buid=4294967295/uid=-1/;
+ s/\beuid=4294967295/euid=-1/;
+ s/\bgid=4294967295/gid=-1/;
+ s/\begid=4294967295/egid=-1/;
+
s/\bgid=\d+/gid=gggg/;
s/\begid=\d+/egid=gggg/;
s/\bpid=\d+/pid=pppp/;
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.
# 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/;
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 ========
# ======== 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
- 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 =========
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)/;
# numbers, or handle specific bad conditions in different ways, leading to
# different wording in the error messages, so we cannot compare them.
- s/(TLS error on connection (?:from|to) .*? \(SSL_\w+\): error:)(.*)/$1 <<detail omitted>>/;
-
+ 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
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;
+
+ # ==========================================================
+ # MIME boundaries in RFC3461 DSN messages
+ s/\d{8,10}-eximdsn-\d+/NNNNNNNNNN-eximdsn-MMMMMMMMMM/;
+
# ==========================================================
# Some munging is specific to the specific file types
next;
}
}
+
+ # openssl version variances
+ 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 ========
# 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]+$/;
+ 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|Not owner)/;
+
+ # We might not keep this check; rather than change all the tests, just
+ # ignore it as long as it succeeds; then we only need to change the
+ # TLS tests where tls_require_ciphers has been set.
+ if (m{^changed uid/gid: calling tls_validate_require_cipher}) {
+ my $discard = <IN>;
+ next;
+ }
+ next if /^tls_validate_require_cipher child \d+ ended: status=0x0/;
+
+ # 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.
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\)/;
# 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.
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/;
+
# 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
/^Fixed never_users:/ ||
/^Size of off_t:/
);
+
+
}
next;
}
+ # ======== log ========
+
+ elsif ($is_log)
+ {
+ # Berkeley DB version differences
+ next if / Berkeley DB error: /;
+ }
+
# ======== All files other than stderr ========
print MUNGED;
# Arguments: [0] the prompt string
# [1] if there is a U in the prompt and $force_update is true
-# Returns: nothing (it sets $_)
+# [2] if there is a C in the prompt and $force_continue is true
+# Returns: returns the answer
sub interact{
print $_[0];
if ($_[1]) { $_ = "u"; print "... update forced\n"; }
+ elsif ($_[2]) { $_ = "c"; print "... continue forced\n"; }
else { $_ = <T>; }
}
+##################################################
+# Subroutine to log in force_continue mode #
+##################################################
+
+# In force_continue mode, we just want a terse output to a statically
+# named logfile. If multiple files in same batch (stdout, stderr, etc)
+# all have mismatches, it will log multiple times.
+#
+# Arguments: [0] the logfile to append to
+# [1] the testno that failed
+# Returns: nothing
+
+
+
+sub log_failure {
+ my $logfile = shift();
+ my $testno = shift();
+ my $detail = shift() || '';
+ if ( open(my $fh, ">>", $logfile) ) {
+ print $fh "Test $testno $detail failed\n";
+ close $fh;
+ }
+}
+
+
##################################################
# Subroutine to compare one output file #
# [2] where to put the munged copy
# [3] the name of the saved file
# [4] TRUE if this is a log file whose deliveries must be sorted
+# [5] optionally, a custom munge command
#
# 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.
sub check_file{
-my($rf,$rsf,$mf,$sf,$sortfile) = @_;
+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));
for (;;)
{
print "Continue, Show, or Quit? [Q] ";
- $_ = <T>;
+ $_ = $force_continue ? "c" : <T>;
tests_exit(1) if /^q?$/i;
+ log_failure($log_failed_filename, $testno, $rf) if (/^c$/i && $force_continue);
return 0 if /^c$/i;
last if (/^s$/);
}
print "\n";
print "------------ $f -----------\n"
if (defined $rf && -s $rf && defined $rsf && -s $rsf);
- system("$more $f");
+ system("$more '$f'");
}
}
print "\n";
for (;;)
{
- interact("Continue, Update & retry, Quit? [Q] ", $force_update);
+ interact("Continue, Update & retry, Quit? [Q] ", $force_update, $force_continue);
tests_exit(1) if /^q?$/i;
+ log_failure($log_failed_filename, $testno, $rsf) if (/^c$/i && $force_continue);
return 0 if /^c$/i;
last if (/^u$/i);
}
}
+#### $_
+
# 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.
open(MUNGED, ">$mf") || tests_exit(-1, "Failed to open $mf: $!");
-my($truncated) = munge($rf) if -e $rf;
+my($truncated) = munge($rf, $extra) if -e $rf;
if (defined $rsf && -e $rsf)
{
print MUNGED "\n******** SERVER ********\n";
- $truncated |= munge($rsf);
+ $truncated |= munge($rsf, $extra);
}
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
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);
# 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, Update & retry, Quit? [Q] ", $force_update);
+ 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_current) if (/^c$/i && $force_continue);
return 0 if /^c$/i;
- last if (/^u$/i);
+ return 1 if /^r$/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;
}
+##################################################
+# Custom munges
+# keyed by name of munge; value is a ref to a hash
+# which is keyed by file, value a string to look for.
+# Usable files are:
+# paniclog, rejectlog, mainlog, stdout, stderr, msglog, mail
+# Search strings starting with 's' do substitutions;
+# with '/' do line-skips.
+# Triggered by a scriptfile line "munge <name>"
+##################################################
+$munges =
+ { 'dnssec' =>
+ { 'stderr' => '/^Reverse DNS security status: unverified\n/' },
+
+ 'gnutls_unexpected' =>
+ { 'mainlog' => '/\(recv\): A TLS packet with unexpected length was received./' },
+
+ 'gnutls_handshake' =>
+ { 'mainlog' => 's/\(gnutls_handshake\): Error in the push function/\(gnutls_handshake\): A TLS packet with unexpected length was received/' },
+
+ 'optional_events' =>
+ { 'stdout' => '/event_action =/' },
+
+ 'optional_ocsp' =>
+ { 'stderr' => '/127.0.0.1 in hosts_requ(ire|est)_ocsp/' },
+
+ '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/'
+ },
+ };
+
+
##################################################
# Subroutine to check the output of a test #
##################################################
# This function is called when the series of subtests is complete. It makes
-# use of check() file, whose arguments are:
+# use of check_file(), whose arguments are:
#
# [0] the name of the main raw output file
# [1] the name of the server raw output file or undef
# [2] where to put the munged copy
# [3] the name of the saved file
# [4] TRUE if this is a log file whose deliveries must be sorted
+# [5] an optional custom munge command
#
-# Arguments: none
+# Arguments: Optionally, name of a single custom munge to run.
# 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($mungename) = $_[0];
my($yield) = 0;
+my($munge) = $munges->{$mungename} if defined $mungename;
$yield = 1 if check_file("spool/log/paniclog",
"spool/log/serverpaniclog",
"test-paniclog-munged",
- "paniclog/$testno", 0);
+ "paniclog/$testno", 0,
+ $munge->{'paniclog'});
$yield = 1 if check_file("spool/log/rejectlog",
"spool/log/serverrejectlog",
"test-rejectlog-munged",
- "rejectlog/$testno", 0);
+ "rejectlog/$testno", 0,
+ $munge->{'rejectlog'});
$yield = 1 if check_file("spool/log/mainlog",
"spool/log/servermainlog",
"test-mainlog-munged",
- "log/$testno", $sortlog);
+ "log/$testno", $sortlog,
+ $munge->{'mainlog'});
if (!$stdout_skip)
{
$yield = 1 if check_file("test-stdout",
"test-stdout-server",
"test-stdout-munged",
- "stdout/$testno", 0);
+ "stdout/$testno", 0,
+ $munge->{'stdout'});
}
if (!$stderr_skip)
$yield = 1 if check_file("test-stderr",
"test-stderr-server",
"test-stderr-munged",
- "stderr/$testno", 0);
+ "stderr/$testno", 0,
+ $munge->{'stderr'});
}
# Compare any delivered messages, unless this test is skipped.
print ">> COMPARE $mail mail/$testno.$saved_mail\n" if $debug;
$yield = 1 if check_file($mail, undef, "test-mail-munged",
- "mail/$testno.$saved_mail", 0);
+ "mail/$testno.$saved_mail", 0,
+ $munge->{'mail'});
delete $expected_mails{"mail/$testno.$saved_mail"};
}
for (;;)
{
- interact("Continue, Update & retry, or Quit? [Q] ", $force_update);
+ interact("Continue, Update & retry, or Quit? [Q] ", $force_update, $force_continue);
tests_exit(1) if /^q?$/i;
+ log_failure($log_failed_filename, $testno, "missing email") if (/^c$/i && $force_continue);
last if /^c$/i;
# For update, we not only have to unlink the file, but we must also
s/((?:[^\W_]{6}-){2}[^\W_]{2})
/new_value($1, "10Hm%s-0005vi-00", \$next_msgid)/egx;
$yield = 1 if check_file("spool/msglog/$msglog", undef,
- "test-msglog-munged", "msglog/$testno.$munged_msglog", 0);
+ "test-msglog-munged", "msglog/$testno.$munged_msglog", 0,
+ $munge->{'msglog'});
delete $expected_msglogs{"$testno.$munged_msglog"};
}
}
for (;;)
{
- interact("Continue, Update, or Quit? [Q] ", $force_update);
+ interact("Continue, Update, or Quit? [Q] ", $force_update, $force_continue);
tests_exit(1) if /^q?$/i;
+ log_failure($log_failed_filename, $testno, "missing msglog") if (/^c$/i && $force_continue);
last if /^c$/i;
if (/^u$/i)
{
# 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
+# munge: name of a post-script results munger
sub run_command{
my($testno) = $_[0];
my($subtestref) = $_[1];
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];
my(@temp);
print ">> ./eximdir/exim_dumpdb $parm_cwd/spool $which\n" if $debug;
open(IN, "./eximdir/exim_dumpdb $parm_cwd/spool $which |");
- @temp = <IN>;
- close(IN);
- if ($which eq "callout")
+ open(OUT, ">>test-stdout");
+ print OUT "+++++++++++++++++++++++++++\n";
+
+ if ($which eq "retry")
{
+ $/ = "\n ";
+ @temp = <IN>;
+ $/ = "\n";
+
@temp = sort {
- my($aa) = substr $a, 21;
- my($bb) = substr $b, 21;
- return $aa cmp $bb;
+ my($aa) = split(' ', $a);
+ my($bb) = split(' ', $b);
+ return $aa cmp $bb;
} @temp;
+
+ foreach $item (@temp)
+ {
+ $item =~ s/^\s*(.*)\n(.*)\n?\s*$/$1\n$2/m;
+ print OUT " $item\n";
+ }
}
- open(OUT, ">>test-stdout");
- print OUT "+++++++++++++++++++++++++++\n";
- print OUT @temp;
+ else
+ {
+ @temp = <IN>;
+ if ($which eq "callout")
+ {
+ @temp = sort {
+ my($aa) = substr $a, 21;
+ my($bb) = substr $b, 21;
+ return $aa cmp $bb;
+ } @temp;
+ }
+ print OUT @temp;
+ }
+
+ close(IN);
close(OUT);
return 1;
}
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;
}
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 -INT $pid");
+ wait;
+ }
+ } else {
+ $pid = `cat $parm_cwd/spool/exim-daemon.*`;
+ if ($pid)
+ {
+ run_system("sudo /bin/kill -INT $pid");
+ close DAEMONCMD; # Waits for process
+ }
+ }
+ run_system("sudo /bin/rm -f spool/exim-daemon.*");
+ return (1, $return_extra);
}
}
+# The "munge" command selects one of a hardwired set of test-result modifications
+# to be made before result compares are run agains the golden set. This lets
+# us account for test-system dependent things which only affect a few, but known,
+# test-cases.
+# Currently only the last munge takes effect.
+
+if (/^munge\s+(.*)$/)
+ {
+ return (0, { munge => $1 });
+ }
+
+
# The "sleep" command does just that. For sleeps longer than 1 second we
# tell the user what's going on.
# 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|chown|chmod)\s/)
{
run_system("$_ >>test-stdout 2>>test-stderr");
return 1;
# 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);
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;
}
# 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
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")
+ unless $force_continue;
+ }
}
# If -d is specified in $optargs, remove it from $args; i.e. let
$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
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");
# 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/)
+ {
+ 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
+ }
+ }
+
+# 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
else { tests_exit(-1, "Command unrecognized in line $lineno: $_"); }
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 #
+##################################################
+
+umask 022;
+
##################################################
# Check for the "less" command #
##################################################
# 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] =~ ?^/?)? 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 "";
{
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 "-VALGRIND") { $valgrind = 1; next; }
+ if ($arg =~ /^-FLAVOU?R$/) { $flavour = shift; next; }
}
$optargs .= " $arg";
}
# 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
# Find what is in the binary #
##################################################
-open(EXIMINFO, "$parm_exim -C confs/0000 -DDIR=$parm_cwd " .
- "-bP exim_user exim_group|") ||
+# deal with TRUSTED_CONFIG_LIST restrictions
+unlink("$parm_cwd/test-config") if -e "$parm_cwd/test-config";
+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 2>&1|") ||
die "** Cannot run $parm_exim: $!\n";
while(<EXIMINFO>)
{
$parm_eximuser = $1 if /^exim_user = (.*)$/;
$parm_eximgroup = $1 if /^exim_group = (.*)$/;
+ $parm_trusted_config_list = $1 if /^TRUSTED_CONFIG_LIST:.*?"(.*?)"$/;
+ print "$_" if /wrong owner/;
}
close(EXIMINFO);
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)
{
else { $parm_exim_gid = getgrnam($parm_eximgroup); }
}
-open(EXIMINFO, "$parm_exim -bV -C confs/0000 -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";
+ }
+
+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";
{
my(@temp);
- if (/^Exim version/) { print; }
+ if (/^(Exim|Library) version/) { print; }
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: (.*)/)
%parm_support = @temp;
}
- elsif (/^Lookups: (.*)/)
+ elsif (/^Lookups \(built-in\): (.*)/)
{
print;
@temp = split /(\s+)/, $1;
close(EXIMINFO);
print "-" x 78, "\n";
+unlink("$parm_cwd/test-config");
##################################################
# Check for SpamAssassin and ClamAV #
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";
{
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/|
{
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";
}
+##################################################
+# 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 #
##################################################
# 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 "Program caller is $parm_caller ($parm_caller_uid), whose group is $parm_caller_group ($parm_caller_gid)\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/)
$_ =~ /^\s*inet(?:\saddr)?:?\s?(\d+\.\d+\.\d+\.\d+)\s/i)
{
$ip = $1;
- next if ($ip eq "127.0.0.1");
+ next if ($ip =~ /^127\./ || $ip =~ /^10\./);
$parm_ipv4 = $ip;
}
print "\n*** Host name is not fully qualified: this may cause problems ***\n\n";
}
-# Find the user's shell
+if ($parm_hostname =~ /[[:upper:]]/)
+ {
+ print "\n*** Host name has upper case characters: this may cause problems ***\n\n";
+ }
-$parm_shell = $ENV{'SHELL'};
##################################################
# 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'} &&
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)");
+}
+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 ...";
if (($rc = system("sudo bin/checkaccess $parm_cwd/eximdir/exim $parm_eximuser $parm_eximgroup")) != 0)
# 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"
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";
}
}
}
+# Set a user's shell, distinguishable from /bin/sh
+
+symlink("/bin/sh","aux-var/sh");
+$ENV{'SHELL'} = $parm_shell = $parm_cwd . "/aux-var/sh";
##################################################
# Create fake DNS zones for this host #
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;
+ } else {
+ $exp_v6 = $parm_ipv6;
+ }
+ my(@components) = split /:/, $exp_v6;
my(@nibbles) = reverse (split /\s*/, shift @components);
my($sep) = "";
open(T, "/dev/tty") || tests_exit(-1, "Failed to open /dev/tty: $!");
print "\nPress RETURN to run the tests: ";
-$_ = <T>;
+$_ = $force_continue ? "c" : <T>;
print "\n";
$lasttestdir = "";
local($lineno) = 0;
local($commandno) = 0;
local($subtestno) = 0;
- local($testno) = substr($test, -4);
+ (local $testno = $test) =~ s|.*/||;
local($sortlog) = 0;
my($gnutls) = 0;
$stdout_skip = 0;
$rmfiltertest = 0;
$is_ipv6test = 0;
+ $TEST_STATE->{munge} = "";
# Remove the associative arrays used to hold checked mail files and msglogs
undef %expected_msglogs;
# Open the test's script
-
open(SCRIPT, "scripts/$test") ||
tests_exit(-1, "Failed to open \"scripts/$test\": $!");
+ # Run through the script once to set variables which should be global
+ while (<SCRIPT>)
+ {
+ if (/^no_message_check/) { $message_skip = 1; next; }
+ if (/^no_msglog_check/) { $msglog_skip = 1; next; }
+ if (/^no_stderr_check/) { $stderr_skip = 1; next; }
+ if (/^no_stdout_check/) { $stdout_skip = 1; next; }
+ if (/^rmfiltertest/) { $rmfiltertest = 1; next; }
+ if (/^sortlog/) { $sortlog = 1; next; }
+ }
+ # Reset to beginning of file for per test interpreting/processing
+ seek(SCRIPT, 0, 0);
# The first line in the script must be a comment that is used to identify
# the set of tests as a whole.
while (<SCRIPT>)
{
$lineno++;
+ # Could remove these variable settings because they are already
+ # set above, but doesn't hurt to leave them here.
if (/^no_message_check/) { $message_skip = 1; next; }
if (/^no_msglog_check/) { $msglog_skip = 1; next; }
if (/^no_stderr_check/) { $stderr_skip = 1; next; }
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) = "";
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;
+$0 = "[runtest $testno]";
+
+ 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
for (;;)
{
- print "\nshow stdErr, show stdOut, Continue (without file comparison), or Quit? [Q] ";
- $_ = <T>;
+ print "\nshow stdErr, show stdOut, Retry, Continue (without file comparison), or Quit? [Q] ";
+ $_ = $force_continue ? "c" : <T>;
tests_exit(1) if /^q?$/i;
- last if /^c$/i;
+ log_failure($log_failed_filename, $testno, "exit code unexpected") if (/^c$/i && $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)
{
system("$more test-stderr");
}
}
+ $retry = 1 if /^r$/i;
$docheck = 0;
}
for (;;)
{
- print "\nShow server stdout, Continue, or Quit? [Q] ";
- $_ = <T>;
+ print "\nShow server stdout, Retry, Continue, or Quit? [Q] ";
+ $_ = $force_continue ? "c" : <T>;
tests_exit(1) if /^q?$/i;
- last if /^c$/i;
+ log_failure($log_failed_filename, $testno, "exit code unexpected") if (/^c$/i && $force_continue);
+ print "... continue forced\n" if $force_continue;
+ last if /^[rc]$/i;
if (/^s$/i)
{
close(S);
}
}
+ $retry = 1 if /^r$/i;
}
}
}
# 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)
+ {
+ $retry = '0';
+ print (("#" x 79) . "\n");
+ redo;
+ }
+
if ($docheck)
{
- if (check_output() != 0)
+ if (check_output($TEST_STATE->{munge}) != 0)
{
print (("#" x 79) . "\n");
redo;
tests_exit(0);
# End of runtest script
-
+# vim: set sw=2 et :