-#! /usr/bin/perl -w
+#! /usr/bin/env perl
+# We use env, because in some environments of our build farm
+# the Perl 5.010 interpreter is only reachable via $PATH
###############################################################################
# This is the controlling script for the "new" test suite for Exim. It should #
###############################################################################
#use strict;
-require Cwd;
+use 5.010;
+use feature 'state'; # included in 5.010
+use warnings;
+
use Errno;
use FileHandle;
use Socket;
use Time::Local;
+use Cwd;
+use File::Basename;
+use FindBin qw'$Bin';
+
+use lib "$Bin/lib";
+use Exim::Runtest;
+
+use if $ENV{DEBUG} && $ENV{DEBUG} =~ /\bruntest\b/ => ('Smart::Comments' => '####');
# Start by initializing some global variables
# normal = 2432 tls_dh_max_bits = 2236
$gnutls_dh_bits_normal = 2236;
-$cf = "bin/cf -exact";
+$cf = 'bin/cf -exact';
$cr = "\r";
$debug = 0;
+$flavour = do {
+ my $f = Exim::Runtest::flavour();
+ (grep { $f eq $_ } Exim::Runtest::flavours()) ? $f : 'FOO';
+};
$force_continue = 0;
$force_update = 0;
$log_failed_filename = "failed-summary.log";
$optargs = "";
$save_output = 0;
$server_opts = "";
+$valgrind = 0;
$have_ipv4 = 1;
$have_ipv6 = 1;
$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
+
+# Find a suiteable group name for test (currently only 0001
+# uses a group name. A numeric group id would do
+my $parm_mailgroup = Exim::Runtest::mailgroup('mail');
# 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;
###############################################################################
###############################################################################
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;
+s?\bMAILGROUP\b?$parm_mailgroup?g;
}
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
# 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/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.
# 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
# ======== 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/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)/;
# 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
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/i;
+ # 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]+$/;
# 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/;
+
+ # Spool filesystem free space changes on different systems.
+ s/^((?:spool|log) directory space =) -?\d+K (inodes =)\s*-?\d+/$1 nnnnnK $2 nnnnn/;
+
+ # Non-TLS builds have different expansions for received_header_text
+ if (s/(with \$received_protocol)\}\} \$\{if def:tls_cipher \{\(\$tls_cipher\)\n$/$1/)
+ {
+ $_ .= <IN>;
+ s/\s+\}\}(?=\(Exim )/\}\} /;
+ }
+ if (/^ condition: def:tls_cipher$/)
+ {
+ <IN>; <IN>; <IN>; <IN>; <IN>; <IN>;
+ <IN>; <IN>; <IN>; <IN>; <IN>; next;
+ }
+
+ # Not all platforms build with DKIM enabled
+ next if /^PDKIM >> Body data for hash, canonicalized/;
+
+ # Not all platforms support TCP Fast Open, and the compile omits the check
+ if (s/\S+ in hosts_try_fastopen\? no \(option unset\)\n$//)
+ {
+ $_ .= <IN>;
+ s/ \.\.\. >>> / ... /;
+ }
+
+ next if /^(ppppp )?setsockopt FASTOPEN: Protocol not available$/;
+
# When Exim is checking the size of directories for maildir, it uses
# the check_dir_size() function to scan directories. Of course, the order
# of the files that are obtained using readdir() varies from system to
@saved = ();
}
- # Skip hosts_require_dane checks when the options
- # are unset, because dane ain't always there.
-
- next if /in\shosts_require_dane\?\sno\s\(option\sunset\)/x;
-
# Skip some lines that Exim puts out at the start of debugging output
# because they will be different in different binaries.
/^log selectors =/ ||
/^cwd=/ ||
/^Fixed never_users:/ ||
+ /^Configure owner:/ ||
/^Size of off_t:/
);
+
+
}
next;
# Arguments: [0] the prompt string
# [1] if there is a U in the prompt and $force_update is true
# [2] if there is a C in the prompt and $force_continue is true
-# Returns: nothing (it sets $_)
+# Returns: returns the answer
+
+sub interact {
+ my ($prompt, $have_u, $have_c) = @_;
+
+ print $prompt;
+
+ if ($have_u) {
+ print "... update forced\n";
+ return 'u';
+ }
-sub interact{
-print $_[0];
-if ($_[1]) { $_ = "u"; print "... update forced\n"; }
- elsif ($_[2]) { $_ = "c"; print "... continue forced\n"; }
- else { $_ = <T>; }
+ if ($have_c) {
+ print "... continue forced\n";
+ return 'c';
+ }
+
+ return lc <T>;
}
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;
- }
+ my ($logfile, $testno, $detail) = @_;
+
+ open(my $fh, '>>', $logfile) or return;
+
+ print $fh "Test $testno "
+ . (defined $detail ? "$detail " : '')
+ . "failed\n";
}
# 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] ";
- $_ = $force_continue ? "c" : <T>;
- tests_exit(1) if /^q?$/i;
- log_failure($log_failed_filename, $testno, $rf) if (/^c$/i && $force_continue);
+ $_ = interact('Continue, Show, or Quit? [Q] ', undef, $force_continue);
+ tests_exit(1) if /^q?$/;
+ log_failure($log_failed_filename, $testno, $rf) if (/^c$/ && $force_continue);
return 0 if /^c$/i;
last if (/^s$/);
}
print "\n";
for (;;)
{
- 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);
+ $_ = interact('Continue, Update & retry, Quit? [Q] ', $force_update, $force_continue);
+ tests_exit(1) if /^q?$/;
+ log_failure($log_failed_filename, $testno, $rsf) if (/^c$/ && $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.
# 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, Retry, Update & retry, Quit? [Q] ", $force_update, $force_continue);
- tests_exit(1) if /^q?$/i;
- log_failure($log_failed_filename, $testno, $sf) if (/^c$/i && $force_continue);
+ $_ = 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?$/;
+ log_failure($log_failed_filename, $testno, $sf_current) if (/^c$/i && $force_continue);
return 0 if /^c$/i;
return 1 if /^r$/i;
- last if (/^u$/i);
+ last if (/^[us]$/i);
}
}
# Update or delete the saved file, and give the appropriate return code.
if (-s $mf)
- { tests_exit(-1, "Failed to cp $mf $sf") if system("cp '$mf' '$sf'") != 0; }
+ {
+ my $sf = /^u/i ? $sf_current : $sf_flavour;
+ tests_exit(-1, "Failed to cp $mf $sf") if system("cp '$mf' '$sf'") != 0;
+ }
else
- { tests_exit(-1, "Failed to unlink $sf") if !unlink($sf); }
+ {
+ # if we deal with a flavour file, we can't delete it, because next time the generic
+ # file would be used again
+ if ($sf_current eq $sf_flavour) {
+ open(FOO, ">$sf_current");
+ close(FOO);
+ }
+ else {
+ tests_exit(-1, "Failed to unlink $sf_current") if !unlink($sf_current);
+ }
+ }
return 1;
}
'optional_ocsp' =>
{ 'stderr' => '/127.0.0.1 in hosts_requ(ire|est)_ocsp/' },
- 'no_tpt_filter_epipe' =>
- { 'stderr' => '/^writing error 32: Broken pipe$/' },
-
'optional_cert_hostnames' =>
{ 'stderr' => '/in tls_verify_cert_hostnames\? no/' },
'loopback' =>
{ 'stdout' => 's/[[](127\.0\.0\.1|::1)]/[IP_LOOPBACK_ADDR]/' },
+ 'scanfile_size' =>
+ { 'stdout' => 's/(Content-length:) \d\d\d/$1 ddd/' },
+
+ 'delay_1500' =>
+ { 'stderr' => 's/(1[5-9]|23\d)\d\d msec/ssss msec/' },
+
+ 'tls_anycipher' =>
+ { 'mainlog' => 's/ X=TLS\S+ / X=TLS_proto_and_cipher /' },
+
+ 'debug_pid' =>
+ { 'stderr' => 's/(^\s{0,4}|(?<=Process )|(?<=child ))\d{1,5}/ppppp/g' },
+
+ 'optional_dsn_info' =>
+ { 'mail' => '/^(X-(Remote-MTA-(smtp-greeting|helo-response)|Exim-Diagnostic|(body|message)-linecount):|Remote-MTA: X-ip;)/'
+ },
+
+ 'optional_config' =>
+ { 'stdout' => '/^(
+ dkim_(canon|domain|private_key|selector|sign_headers|strict)
+ |gnutls_require_(kx|mac|protocols)
+ |hosts_(requ(est|ire)|try)_(dane|ocsp)
+ |hosts_(avoid|nopass|require|verify_avoid)_tls
+ |socks_proxy
+ |tls_[^ ]*
+ )($|[ ]=)/x' },
+
+ 'sys_bindir' =>
+ { 'mainlog' => 's%/(usr/)?bin/%SYSBINDIR/%' },
+
+ 'sync_check_data' =>
+ { 'mainlog' => 's/^(.* SMTP protocol synchronization error .* next input=.{8}).*$/$1<suppressed>/',
+ 'rejectlog' => 's/^(.* SMTP protocol synchronization error .* next input=.{8}).*$/$1<suppressed>/'},
+
+ 'debuglog_stdout' =>
+ { 'stdout' => 's/^\d\d:\d\d:\d\d\s+\d+ //;
+ s/Process \d+ is ready for new message/Process pppp is ready for new message/'
+ },
+
+ 'timeout_errno' => # actual errno differs Solaris vs. Linux
+ { 'mainlog' => 's/(host deferral .* errno) <\d+> /$1 <EEE> /' },
};
for (;;)
{
- 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;
+ $_ = interact('Continue, Update & retry, or Quit? [Q] ', $force_update, $force_continue);
+ tests_exit(1) if /^q?$/;
+ log_failure($log_failed_filename, $testno, "missing email") if (/^c$/ && $force_continue);
+ last if /^c$/;
# For update, we not only have to unlink the file, but we must also
# remove it from the @oldmails vector, as otherwise it will still be
# checked for when we re-run the test.
- if (/^u$/i)
+ if (/^u$/)
{
foreach $key (keys %expected_mails)
{
for (;;)
{
- 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)
+ $_ = interact('Continue, Update, or Quit? [Q] ', $force_update, $force_continue);
+ tests_exit(1) if /^q?$/;
+ log_failure($log_failed_filename, $testno, "missing msglog") if (/^c$/ && $force_continue);
+ last if /^c$/;
+ if (/^u$/)
{
foreach $key (keys %expected_msglogs)
{
# Returns: nothing
sub run_system {
-my($cmd) = $_[0];
-if ($debug)
- {
- my($prcmd) = $cmd;
- $prcmd =~ s/; /;\n>> /;
- print ">> $prcmd\n";
- }
-system("$cmd");
+ my $cmd = shift;
+ print '>> ' . $cmd =~ s/; /;\n>>/r . "\n" if $debug;
+ system $cmd;
}
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];
# 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;
# 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
# Done backwards just in case there are more than 9
- my($i);
- for ($i = @msglist; $i > 0; $i--) { $args =~ s/\$msg$i/$msglist[$i-1]/g; }
+ for (my $i = @msglist; $i > 0; $i--) { $args =~ s/\$msg$i/$msglist[$i-1]/g; }
if ( $args =~ /\$msg\d/ )
{
tests_exit(-1, "Not enough messages in spool, for test $testno line $lineno\n")
$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 mkdir spool/log 2>/dev/null');
run_system("sudo chown $parm_eximuser:$parm_eximgroup spool/log");
# Before running the command, convert the -bd option into -bdf so that an
# file is written to the spool directory, in case the Exim binary was
# built with PID_FILE_PATH pointing somewhere else.
- $cmd =~ s!\s-bd\s! -bdf -oP $parm_cwd/spool/exim-daemon.pid !;
+ if ($cmd =~ /\s-oP\s/)
+ {
+ ($pidfile = $cmd) =~ s/^.*-oP ([^ ]+).*$/$1/;
+ $cmd =~ s!\s-bd\s! -bdf !;
+ }
+ else
+ {
+ $pidfile = "$parm_cwd/spool/exim-daemon.pid";
+ $cmd =~ s!\s-bd\s! -bdf -oP $pidfile !;
+ }
print ">> |${cmd}-server\n" if ($debug);
open DAEMONCMD, "|${cmd}-server" || tests_exit(-1, "Failed to run $cmd");
DAEMONCMD->autoflush(1);
while (<SCRIPT>) { $lineno++; last if /^\*{4}\s*$/; } # Ignore any input
- select(undef, undef, undef, 0.3); # Let the daemon get going
+
+ # Interlock with daemon startup
+ while (! stat("$pidfile") ) { select(undef, undef, undef, 0.3); }
return 3; # Don't wait
}
elsif ($cmd =~ /\s-DSERVER=wait:(\d+)\s/)
{
+
+ # The port and the $dynamic_socket was already allocated while parsing the
+ # script file, where -DSERVER=wait:PORT_DYNAMIC was encountered.
+
my $listen_port = $1;
- my $waitmode_sock = new FileHandle;
if ($debug) { printf ">> wait-mode daemon: $cmd\n"; }
- run_system("sudo mkdir spool/log 2>/dev/null");
+ run_system('sudo 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
}
}
+# 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
###############################################################################
###############################################################################
-# Here beginneth the Main Program ...
+# Here begins the Main Program ...
###############################################################################
###############################################################################
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 #
# 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;
##################################################
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";
}
##################################################
# If the first character of the first argument is '/', the argument is taken
-# as the path to the binary.
+# as the path to the binary. If the first argument does not start with a
+# '/' but exists in the file system, it's assumed to be the Exim binary.
-$parm_exim = (@ARGV > 0 && $ARGV[0] =~ m?^/?)? shift @ARGV : "";
+($parm_exim, @ARGV) = Exim::Runtest::exim_binary(@ARGV);
print "Exim binary is $parm_exim\n" if $parm_exim ne "";
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";
}
# takes precedence; otherwise exim-snapshot takes precedence over any numbered
# releases.
-if ($parm_exim eq "")
- {
- my($use_srcdir) = "";
-
- opendir DIR, ".." || die "** Failed to opendir \"..\": $!\n";
- while ($f = readdir(DIR))
- {
- my($srcdir);
-
- # Try this directory if it is "exim4" or if it is exim-snapshot or exim-n.m
- # possibly followed by -RCx where n.m is greater than any previously tried
- # directory. Thus, we should choose the highest version of Exim that has
- # been compiled.
-
- if ($f eq "exim4" || $f eq "exim-snapshot")
- { $srcdir = $f; }
- else
- { $srcdir = $f
- if ($f =~ /^exim-\d+\.\d+(-RC\d+)?$/ && $f gt $use_srcdir); }
-
- # Look for a build directory with a binary in it. If we find a binary,
- # accept this source directory.
-
- if ($srcdir)
- {
- opendir SRCDIR, "../$srcdir" ||
- die "** Failed to opendir \"$cwd/../$srcdir\": $!\n";
- while ($f = readdir(SRCDIR))
- {
- if ($f =~ /^build-/ && -e "../$srcdir/$f/exim")
- {
- $use_srcdir = $srcdir;
- $parm_exim = "$cwd/../$srcdir/$f/exim";
- $parm_exim =~ s'/[^/]+/\.\./'/';
- last;
- }
- }
- closedir(SRCDIR);
- }
-
- # If we have found "exim4" or "exim-snapshot", that takes precedence.
- # Otherwise, continue to see if there's a later version.
-
- last if $use_srcdir eq "exim4" || $use_srcdir eq "exim-snapshot";
- }
- closedir(DIR);
- print "Exim binary found in $parm_exim\n" if $parm_exim ne "";
- }
-
# If $parm_exim is still empty, ask the caller
if ($parm_exim eq "")
# deal with TRUSTED_CONFIG_LIST restrictions
unlink("$parm_cwd/test-config") if -e "$parm_cwd/test-config";
-symlink("$parm_cwd/confs/0000", "$parm_cwd/test-config")
- or die "Unable to link initial config into place: $!\n";
+open (IN, "$parm_cwd/confs/0000") ||
+ tests_exit(-1, "Couldn't open $parm_cwd/confs/0000: $!\n");
+open (OUT, ">test-config") ||
+ tests_exit(-1, "Couldn't open test-config: $!\n");
+while (<IN>) { print OUT; }
+close(IN);
+close(OUT);
print("Probing with config file: $parm_cwd/test-config\n");
open(EXIMINFO, "$parm_exim -d -C $parm_cwd/test-config -DDIR=$parm_cwd " .
- "-bP exim_user exim_group|") ||
+ "-bP exim_user exim_group 2>&1|") ||
die "** Cannot run $parm_exim: $!\n";
while(<EXIMINFO>)
{
+ if (my ($version) = /^Exim version (\S+)/) {
+ my $git = `git describe --dirty=-XX --match 'exim-4*'`;
+ if (defined $git and $? == 0) {
+ chomp $git;
+ $version =~ s/^\d+\K\./_/;
+ $git =~ s/^exim-//i;
+ $git =~ s/.*-\Kg([[:xdigit:]]+(?:-XX)?)/$1/;
+ print <<___
+
+*** Version mismatch
+*** Exim binary: $version
+*** Git : $git
+
+___
+ 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);
print "Unable to extract exim_user from binary.\n";
print "Check if Exim refused to run; if so, consider:\n";
print " TRUSTED_CONFIG_LIST ALT_CONFIG_PREFIX WHITELIST_D_MACROS\n";
+ print "If debug permission denied, are you in the exim group?\n";
die "Failing to get information from binary.\n";
}
else { $parm_exim_gid = getgrnam($parm_eximgroup); }
}
-open(EXIMINFO, "$parm_exim -bV -C $parm_cwd/test-config -DDIR=$parm_cwd |") ||
+# check the permissions on the TRUSTED_CONFIG_LIST
+if (defined $parm_trusted_config_list)
+ {
+ die "TRUSTED_CONFIG_LIST: $parm_trusted_config_list: $!\n"
+ if not -f $parm_trusted_config_list;
+
+ die "TRUSTED_CONFIG_LIST $parm_trusted_config_list must not be world writable!\n"
+ if 02 & (stat _)[2];
+
+ die sprintf "TRUSTED_CONFIG_LIST: $parm_trusted_config_list %d is group writable, but not owned by group '%s' or '%s'.\n",
+ (stat _)[1],
+ scalar(getgrgid 0), scalar(getgrgid $>)
+ if (020 & (stat _)[2]) and not ((stat _)[5] == $> or (stat _)[5] == 0);
+
+ die sprintf "TRUSTED_CONFIG_LIST: $parm_trusted_config_list is not owned by user '%s' or '%s'.\n",
+ scalar(getpwuid 0), scalar(getpwuid $>)
+ if (not (-o _ or (stat _)[4] == 0));
+
+ open(TCL, $parm_trusted_config_list) or die "Can't open $parm_trusted_config_list: $!\n";
+ my $test_config = getcwd() . '/test-config';
+ die "Can't find '$test_config' in TRUSTED_CONFIG_LIST $parm_trusted_config_list."
+ if not grep { /^$test_config$/ } <TCL>;
+ }
+else
+ {
+ die "Unable to check the TRUSTED_CONFIG_LIST, seems to be empty?\n";
+ }
+
+die "CONFIGURE_OWNER ($parm_configure_owner) does not match the user invoking $0 ($>)\n"
+ if $parm_configure_owner != $>;
+
+die "CONFIGURE_GROUP ($parm_configure_group) does not match the group invoking $0 ($))\n"
+ if 0020 & (stat "$parm_cwd/test-config")[2]
+ and $parm_configure_group != $);
+
+
+open(EXIMINFO, "$parm_exim -d-all+transport -bV -C $parm_cwd/test-config -DDIR=$parm_cwd |") ||
die "** Cannot run $parm_exim: $!\n";
print "-" x 78, "\n";
{
my(@temp);
- if (/^Exim version/) { print; }
+ if (/^(Exim|Library) version/) { print; }
elsif (/^Size of off_t: (\d+)/)
{
}
+##################################################
+# 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 this host's IP addresses - there may be many, of course, but we keep
# one of each type (IPv4 and IPv6).
-$parm_ipv4 = "";
-$parm_ipv6 = "";
-
-$local_ipv4 = "";
-$local_ipv6 = "";
-
-open(IFCONFIG, "ifconfig -a|") || die "** Cannot run \"ifconfig\": $!\n";
-while (($parm_ipv4 eq "" || $parm_ipv6 eq "") && ($_ = <IFCONFIG>))
+open(IFCONFIG, '-|', (grep { -x "$_/ip" } split /:/, $ENV{PATH}) ? 'ip address' : 'ifconfig -a')
+ or die "** Cannot run 'ip address' or 'ifconfig -a'\n";
+while (not ($parm_ipv4 and $parm_ipv6) and defined($_ = <IFCONFIG>))
{
- my($ip);
- if ($parm_ipv4 eq "" &&
- $_ =~ /^\s*inet(?:\saddr)?:?\s?(\d+\.\d+\.\d+\.\d+)\s/i)
+ if (not $parm_ipv4 and /^\s*inet(?:\saddr)?:?\s?(\d+\.\d+\.\d+\.\d+)(?:\/\d+)?\s/i)
{
- $ip = $1;
- next if ($ip =~ /^127\./);
- $parm_ipv4 = $ip;
+ next if $1 =~ /^(?:127|10)\./;
+ $parm_ipv4 = $1;
}
- if ($parm_ipv6 eq "" &&
- $_ =~ /^\s*inet6(?:\saddr)?:?\s?([abcdef\d:]+)/i)
+ if (not $parm_ipv6 and /^\s*inet6(?:\saddr)?:?\s?([abcdef\d:]+)(?:\/\d+)/i)
{
- $ip = $1;
- next if ($ip eq "::1" || $ip =~ /^fe80/i);
- $parm_ipv6 = $ip;
+ next if $1 eq '::1' or $1 =~ /^fe80/i;
+ $parm_ipv6 = $1;
}
}
close(IFCONFIG);
# Use private IP addresses if there are no public ones.
-$parm_ipv4 = $local_ipv4 if ($parm_ipv4 eq "");
-$parm_ipv6 = $local_ipv6 if ($parm_ipv6 eq "");
-
# If either type of IP address is missing, we need to set the value to
# something other than empty, because that wrecks the substitutions. The value
# is reflected, so use a meaningful string. Set appropriate options for the
# 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>";
$parm_running{"IPv4"} = " ";
}
-if ($parm_ipv6 eq "")
+if (not $parm_ipv6)
{
$have_ipv6 = 0;
$parm_ipv6 = "<no IPv6 address found>";
# Find the host name, fully qualified.
chomp($temp = `hostname`);
-$parm_hostname = (gethostbyname($temp))[0];
-$parm_hostname = "no.host.name.found" if $parm_hostname eq "";
+die "'hostname' didn't return anything\n" unless defined $temp and length $temp;
+if ($temp =~ /\./)
+ {
+ $parm_hostname = $temp;
+ }
+else
+ {
+ $parm_hostname = (gethostbyname($temp))[0];
+ $parm_hostname = "no.host.name.found" unless defined $parm_hostname and length $parm_hostname;
+ }
print "Hostname is $parm_hostname\n";
if ($parm_hostname !~ /\./)
# that was done above. Furthermore, we ensure that the binary is deleted at the
# end of the test. First ensure the directory exists.
-if (-d "eximdir")
- { unlink "eximdir/exim"; } # Just in case
-else
- {
- mkdir("eximdir", 0710) || die "** Unable to mkdir $parm_cwd/eximdir: $!\n";
- system("sudo chgrp $parm_eximgroup eximdir");
- }
+unlink 'eximdir/exim'; # Just in case
+-d 'eximdir' or mkdir('eximdir', 0710) or die "** Unable to mkdir $parm_cwd/eximdir: $!\n";
+system("sudo chgrp $parm_eximgroup eximdir");
# The construction of the patched binary must be done as root, so we use
# a separate script. As well as indicating that this is a test-harness binary,
# tests_exit(), so that suitable cleaning up can be done when required.
# Arrange to catch interrupting signals, to assist with this.
-$SIG{'INT'} = \&inthandler;
-$SIG{'PIPE'} = \&pipehandler;
+$SIG{INT} = \&inthandler;
+$SIG{PIPE} = \&pipehandler;
# For some tests, we need another copy of the binary that is setuid exim rather
# than root.
-system("sudo cp eximdir/exim eximdir/exim_exim;" .
+system('sudo cp eximdir/exim eximdir/exim_exim;' .
"sudo chown $parm_eximuser eximdir/exim_exim;" .
"sudo chgrp $parm_eximgroup eximdir/exim_exim;" .
- "sudo chmod 06755 eximdir/exim_exim");
+ 'sudo chmod 06755 eximdir/exim_exim');
##################################################
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 ...";
# 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";
}
# contains ****. We open input from the terminal so that we can read responses
# to prompts.
-open(T, "/dev/tty") || tests_exit(-1, "Failed to open /dev/tty: $!");
+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: $!");
+}
+
print "\nPress RETURN to run the tests: ";
$_ = $force_continue ? "c" : <T>;
local($lineno) = 0;
local($commandno) = 0;
local($subtestno) = 0;
- local($testno) = substr($test, -4);
+ (local $testno = $test) =~ s|.*/||;
local($sortlog) = 0;
my($gnutls) = 0;
my($docheck) = 1;
my($thistestdir) = substr($test, 0, -5);
+ $dynamic_socket->close() if $dynamic_socket;
+
if ($lasttestdir ne $thistestdir)
{
$gnutls = 0;
if (/^no_stdout_check/) { $stdout_skip = 1; next; }
if (/^rmfiltertest/) { $rmfiltertest = 1; next; }
if (/^sortlog/) { $sortlog = 1; next; }
+ if (/\bPORT_DYNAMIC\b/) { $dynamic_socket = Exim::Runtest::dynamic_socket(); next; }
}
# Reset to beginning of file for per test interpreting/processing
seek(SCRIPT, 0, 0);
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($rc, $run_extra) = run_command($testno, \$subtestno, \$expectrc, \$commandname, $TEST_STATE);
my($cmdrc) = $?;
+$0 = "[runtest $testno]";
+
if ($debug) {
print ">> rc=$rc cmdrc=$cmdrc\n";
if (defined $run_extra) {
$_ = $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)
{
# 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)
tests_exit(0);
# End of runtest script
-# vim: set sw=2 et :