use
POSIX
qw(WIFEXITED WIFSIGNALED WIFSTOPPED WEXITSTATUS WTERMSIG WSTOPSIG)
;
use
vars
qw($RUNNING_ON_WINDOWS $SSL_AVAILABLE
$SKIP_SPAMD_TESTS $SKIP_SPAMC_TESTS $NO_SPAMC_EXE
$SKIP_SETUID_NOBODY_TESTS $SKIP_DNSBL_TESTS
$have_inet4 $have_inet6 $spamdhost $spamdport
$workdir $siterules $localrules $userrules $userstate
$keep_workdir $mainpid $spamd_pidfile)
;
my
$sa_code_dir
;
BEGIN {
use
vars
qw(@ISA @EXPORT @EXPORT_OK)
;
@ISA
=
qw(Exporter)
;
@EXPORT
=
qw($have_inet4 $have_inet6 $spamdhost $spamdport)
;
$RUNNING_ON_WINDOWS
= ($^O =~ /^(mswin|dos|os2)/oi);
$SKIP_SPAMD_TESTS
=
$RUNNING_ON_WINDOWS
||
(
$ENV
{
'SPAMD_HOST'
} && !(
$ENV
{
'SPAMD_HOST'
} eq
'127.0.0.1'
||
$ENV
{
'SPAMD_HOST'
} eq
'::1'
||
$ENV
{
'SPAMD_HOST'
} eq
'localhost'
) );
$SKIP_SETUID_NOBODY_TESTS
= 0;
$SKIP_DNSBL_TESTS
= 0;
$have_inet4
=
eval
{
my
$sock
= IO::Socket::INET->new(
LocalAddr
=>
'127.0.0.1'
,
Proto
=>
'udp'
);
$sock
->
close
or
die
"error closing inet socket: $!"
if
$sock
;
$sock
? 1 :
undef
;
};
$have_inet6
=
eval
{
my
$sock
= IO::Socket::INET6->new(
LocalAddr
=>
'::1'
,
Proto
=>
'udp'
);
$sock
->
close
or
die
"error closing inet6 socket: $!"
if
$sock
;
$sock
? 1 :
undef
;
};
if
(!
$RUNNING_ON_WINDOWS
) {
$ENV
{
'PATH'
} =
'/bin:/usr/bin:/usr/local/bin'
;
delete
@ENV
{
qw(IFS CDPATH ENV BASH_ENV)
};
}
else
{
my
@pathdirs
=
split
(
';'
,
$ENV
{
'PATH'
});
$ENV
{
'PATH'
} =
join
(
';'
,
map
{
my
$pathdir
=
$_
;
$pathdir
=~ s/\\*\z//;
my
$abspathdir
= File::Spec->canonpath(Cwd::realpath(
$pathdir
))
if
(-d
$pathdir
);
if
(
defined
$abspathdir
) {
$abspathdir
=~ /^(.*)\z/s;
$abspathdir
= $1;
}
((
defined
$abspathdir
) and (
lc
$pathdir
eq
lc
$abspathdir
))?(
$abspathdir
):()
}
@pathdirs
);
}
if
(-e
't/test_dir'
) {
$sa_code_dir
=
'blib/lib'
; }
elsif
(-e
'test_dir'
) {
$sa_code_dir
=
'../blib/lib'
; }
else
{
die
"FATAL: not in or below test directory?\n"
; }
File::Spec->rel2abs(
$sa_code_dir
) =~ /^(.*)\z/s;
$sa_code_dir
= $1;
if
(not -d
$sa_code_dir
) {
die
"FATAL: not in expected directory relative to built code tree?\n"
;
}
}
sub
sa_t_init {
my
$tname
=
shift
;
$mainpid
= $$;
if
(
$config
{PERL_PATH}) {
$perl_path
=
$config
{PERL_PATH};
}
elsif
($^X =~ m|^/|) {
$perl_path
= $^X;
}
else
{
$perl_path
=
$Config
{perlpath};
$perl_path
=~ s|/[^/]*$|/$^X|;
}
$perl_cmd
=
$perl_path
;
if
(
$ENV
{
'PERL5OPT'
}) {
my
$o
=
$ENV
{
'PERL5OPT'
};
if
(
$o
=~ /(Devel::Cover)/) {
warn
"# setting TEST_PERL_TAINT=no to avoid lack of taint-safety in $1\n"
;
$ENV
{
'TEST_PERL_TAINT'
} =
'no'
;
}
$perl_cmd
.=
" \"$o\""
;
}
$perl_cmd
.=
" -T"
if
!
defined
(
$ENV
{
'TEST_PERL_TAINT'
}) or
$ENV
{
'TEST_PERL_TAINT'
} ne
'no'
;
$perl_cmd
.=
" -w"
if
!
defined
(
$ENV
{
'TEST_PERL_WARN'
}) or
$ENV
{
'TEST_PERL_WARN'
} ne
'no'
;
my
@pathdirs
=
@INC
;
if
(
$ENV
{
'PERL5LIB'
}) {
@pathdirs
=
split
(
$Config
{path_sep},
$ENV
{
'PERL5LIB'
});
}
my
$inc_opts
=
join
(
' -I'
,
map
{
my
$pathdir
=
$_
;
my
$canonpathdir
= File::Spec->canonpath(Cwd::realpath(
$pathdir
))
if
((-d
$pathdir
) and File::Spec->file_name_is_absolute(
$pathdir
));
if
(
defined
$canonpathdir
) {
$canonpathdir
=~ /^(.*)\z/s;
$canonpathdir
= $1;
}
((
defined
$canonpathdir
))?(
$canonpathdir
):()
}
@pathdirs
);
$perl_cmd
.=
" -I$inc_opts"
if
(
$inc_opts
);
$scr
=
$ENV
{
'SPAMASSASSIN_SCRIPT'
};
$scr
||=
"$perl_cmd ../spamassassin.raw"
;
$spamd
=
$ENV
{
'SPAMD_SCRIPT'
};
$spamd
||=
"$perl_cmd ../spamd/spamd.raw"
;
$spamc
=
$ENV
{
'SPAMC_SCRIPT'
};
$spamc
||=
"../spamc/spamc"
;
$salearn
=
$ENV
{
'SALEARN_SCRIPT'
};
$salearn
||=
"$perl_cmd ../sa-learn.raw"
;
$saawl
=
$ENV
{
'SAAWL_SCRIPT'
};
$saawl
||=
"$perl_cmd ../sa-awl"
;
$sacheckspamd
=
$ENV
{
'SACHECKSPAMD_SCRIPT'
};
$sacheckspamd
||=
"$perl_cmd ../sa-check_spamd"
;
$spamdlocalhost
=
$ENV
{
'SPAMD_LOCALHOST'
};
if
(!
$spamdlocalhost
) {
$spamdlocalhost
=
$have_inet4
|| !
$have_inet6
?
'127.0.0.1'
:
'::1'
;
}
$spamdhost
=
$ENV
{
'SPAMD_HOST'
};
$spamdhost
||=
$spamdlocalhost
;
if
(
$tname
!~ /spam[cd]/) {
$TEST_DOES_NOT_RUN_SPAMC_OR_D
= 1;
}
else
{
$spamdport
=
$ENV
{
'SPAMD_PORT'
};
$spamdport
||= probably_unused_spamd_port();
}
(-f
"t/test_dir"
) &&
chdir
(
"t"
);
-f
"test_dir"
or
die
"FATAL: not in test directory?\n"
;
mkdir
(
"log"
, 0755);
-d
"log"
or
die
"FATAL: failed to create log dir\n"
;
chmod
(0755,
"log"
);
if
(!
$RUNNING_ON_WINDOWS
) {
untaint_system(
"chacl -B log 2>/dev/null || setfacl -b log 2>/dev/null"
);
}
if
(
defined
$workdir
) {
if
(!
$keep_workdir
) {
rmtree(
$workdir
);
}
}
$workdir
= tempdir(
"$tname.XXXXXX"
,
DIR
=>
"log"
);
die
"FATAL: failed to create workdir: $!"
unless
-d
$workdir
;
chmod
(0755,
$workdir
);
$keep_workdir
= 0;
$siterules
=
"$workdir/siterules"
;
$localrules
=
"$workdir/localrules"
;
$userrules
=
"$workdir/user.cf"
;
$userstate
=
"$workdir/user_state"
;
mkdir
(
$siterules
) or
die
"FATAL: failed to create $siterules\n"
;
mkdir
(
$localrules
) or
die
"FATAL: failed to create $localrules\n"
;
open
(OUT,
">$userrules"
) or
die
"FATAL: failed to create $userrules\n"
;
close
(OUT);
mkdir
(
$userstate
) or
die
"FATAL: failed to create $userstate\n"
;
$spamd_pidfile
=
"$workdir/spamd.pid"
;
$spamd_cf_args
=
"-C $localrules"
;
$spamd_localrules_args
=
" --siteconfigpath $siterules"
;
$scr_localrules_args
=
" --siteconfigpath $siterules"
;
$salearn_localrules_args
=
" --siteconfigpath $siterules"
;
$scr_cf_args
=
"-C $localrules"
;
$scr_pref_args
=
"-p $userrules"
;
$salearn_cf_args
=
"-C $localrules"
;
$salearn_pref_args
=
"-p $userrules"
;
$scr_test_args
=
""
;
$salearn_test_args
=
""
;
$set_user_prefs
= 0;
$default_cf_lines
= "
bayes_path ./
$userstate
/bayes
auto_welcomelist_path ./
$userstate
/auto-welcomelist
";
read_config();
if
($> == 0) {
$tmp_dir_mode
= 0777;
umask
022;
$SKIP_SETUID_NOBODY_TESTS
= 1;
}
else
{
$tmp_dir_mode
= 0755;
}
$NO_SPAMC_EXE
=
$TEST_DOES_NOT_RUN_SPAMC_OR_D
||
(
$RUNNING_ON_WINDOWS
&&
!
$ENV
{
'SPAMC_SCRIPT'
} &&
!(-e
"../spamc/spamc.exe"
));
$SKIP_SPAMC_TESTS
= (
$NO_SPAMC_EXE
||
(
$RUNNING_ON_WINDOWS
&& !
$ENV
{
'SPAMD_HOST'
}));
$SSL_AVAILABLE
= (!
$TEST_DOES_NOT_RUN_SPAMC_OR_D
) &&
(!
$SKIP_SPAMC_TESTS
) &&
(!
$SKIP_SPAMD_TESTS
) &&
(untaint_cmd(
"$spamc -V"
) =~ /
with
SSL support/) &&
(untaint_cmd(
"$spamd --version"
) =~ /
with
SSL support/);
for
$tainted
(<../rules/*.pm>, <../rules/*.pre>, <../rules/languages>) {
$tainted
=~ /(.*)/;
my
$file
= $1;
$base
= basename
$file
;
copy (
$file
,
"$siterules/$base"
)
or
warn
"cannot copy $file to $siterules/$base: $!"
;
}
for
$tainted
(<../rules/*.cf>) {
$tainted
=~ /(.*)/;
my
$file
= $1;
$base
= basename
$file
;
copy (
$file
,
"$localrules/$base"
)
or
warn
"cannot copy $file to $localrules/$base: $!"
;
}
copy (
"data/01_test_rules.pre"
,
"$localrules/01_test_rules.pre"
)
or
warn
"cannot copy data/01_test_rules.cf to $localrules/01_test_rules.pre: $!"
;
copy (
"data/01_test_rules.cf"
,
"$localrules/01_test_rules.cf"
)
or
warn
"cannot copy data/01_test_rules.cf to $localrules/01_test_rules.cf: $!"
;
open
(PREFS,
">>$localrules/99_test_default.cf"
)
or
die
"cannot append to $localrules/99_test_default.cf: $!"
;
print
PREFS
$default_cf_lines
or
die
"error writing to $localrules/99_test_default.cf: $!"
;
close
PREFS
or
die
"error closing $localrules/99_test_default.cf: $!"
;
$home
=
$ENV
{
'HOME'
};
$home
||=
$ENV
{
'WINDIR'
}
if
(
defined
$ENV
{
'WINDIR'
});
$cwd
= getcwd;
$ENV
{
'TEST_DIR'
} =
$cwd
;
$testname
=
$tname
;
$spamd_run_as_user
= (
$RUNNING_ON_WINDOWS
|| ($> == 0)) ?
"nobody"
: (
getpwuid
($>))[0] ;
}
sub
clear_localrules {
for
$tainted
(<
$localrules
/*.cf>) {
$tainted
=~ /(.*)/;
my
$file
= $1;
next
if
$file
=~ /10_default_prefs.cf$/;
next
if
$file
=~ /20_aux_tlds.cf$/;
next
if
$file
=~ /99_test_prefs.cf$/;
next
if
$file
=~ /99_test_rules.cf$/;
unlink
$file
;
}
}
sub
probably_unused_spamd_port {
return
0
if
$SKIP_SPAMD_TESTS
;
my
$port
;
my
@nstat
;
if
(!
open
(NSTAT,
"netstat -a -n 2>&1 |"
)) {
}
else
{
@nstat
=
grep
(/^\s
*tcp
/i, <NSTAT>);
close
(NSTAT);
}
for
(1..20) {
$port
= 40000 +
int
(
rand
(65500-40000));
last
unless
(
getservbyport
(
$port
,
"tcp"
) ||
grep
(/[:.]
$port
\s/,
@nstat
));
}
return
$port
;
}
sub
locate_command {
my
(
$command
) =
@_
;
my
@path
= File::Spec->path();
push
(
@path
,
'/usr/bin'
)
if
!
grep
{ m@/usr/bin/?$@ }
@path
;
for
my
$path
(
@path
) {
$location
=
"$path/$command"
;
$location
=~ s@//@/
@g
;
return
$location
if
-x
$location
;
}
return
0;
}
sub
sa_t_finish {
}
sub
tstfile {
my
$file
=
shift
;
open
(OUT,
">$workdir/mail.txt"
) or
die
;
print
OUT
$file
;
close
OUT;
}
sub
tstprefs {
my
$lines
=
shift
;
open
(OUT,
">$localrules/99_test_prefs.cf"
) or
die
;
print
OUT
$lines
;
close
OUT;
}
sub
tstlocalrules {
my
$lines
=
shift
;
open
(OUT,
">$localrules/99_test_rules.cf"
) or
die
;
print
OUT
$lines
;
close
OUT;
}
sub
tstuserprefs {
my
$lines
=
shift
;
$set_user_prefs
= 1;
open
(OUT,
">$userrules"
) or
die
;
print
OUT
$lines
;
close
OUT;
}
sub
tstpre {
my
$lines
=
shift
;
open
(OUT,
">$siterules/zz_test.pre"
) or
die
;
print
OUT
$lines
;
close
OUT;
}
sub
disable_compat {
my
$compat
=
shift
;
return
unless
defined
$compat
;
open
(IN,
"$siterules/init.pre"
) or
die
;
open
(OUT,
">$siterules/init.pre.new"
) or
die
;
while
(<IN>) {
next
if
$_
=~ /^\s
*enable_compat
\s+\Q
$compat
\E(?:\s|$)/i;
print
OUT
$_
;
}
close
OUT or
die
;
close
IN or
die
;
rename
(
"$siterules/init.pre.new"
,
"$siterules/init.pre"
);
}
sub
sarun {
my
$args
=
shift
;
my
$read_sub
=
shift
;
my
$post_redir
=
''
;
$args
=~ s/ 2\>\&1$// and
$post_redir
=
' 2>&1'
;
recreate_outputdir_tmp();
clear_pattern_counters();
if
(
defined
$ENV
{
'SA_ARGS'
}) {
$args
=
$ENV
{
'SA_ARGS'
} .
" "
.
$args
;
}
$args
=
"$scr_cf_args $scr_localrules_args $scr_pref_args $scr_test_args $args"
;
my
$scrargs
=
"$scr $args"
;
$scrargs
=~ s!/!\\!g
if
($^O =~ /^MS(DOS|Win)/i);
print
(
"\t$scrargs\n"
);
(-d
"$workdir/d.$testname"
) or
mkdir
(
"$workdir/d.$testname"
, 0755);
my
$test_number
= test_number();
$current_checkfile
=
"$workdir/d.$testname/$test_number"
;
untaint_system(
"$scrargs > $workdir/d.$testname/$test_number $post_redir"
);
$sa_exitcode
= ($?>>8);
if
(
$sa_exitcode
!= 0) {
return
undef
; }
&checkfile
(
"$workdir/d.$testname/$test_number"
,
$read_sub
)
if
(
defined
$read_sub
);
1;
}
sub
salearnrun {
my
$args
=
shift
;
my
$read_sub
=
shift
;
recreate_outputdir_tmp();
%found
= ();
%found_anti
= ();
if
(
defined
$ENV
{
'SA_ARGS'
}) {
$args
=
$ENV
{
'SA_ARGS'
} .
" "
.
$args
;
}
$args
=
"$salearn_cf_args $salearn_localrules_args $salearn_pref_args $salearn_test_args $args"
;
my
$salearnargs
=
"$salearn $args"
;
$salearnargs
=~ s!/!\\!g
if
($^O =~ /^MS(DOS|Win)/i);
print
(
"\t$salearnargs\n"
);
(-d
"$workdir/d.$testname"
) or
mkdir
(
"$workdir/d.$testname"
, 0755);
my
$test_number
= test_number();
$current_checkfile
=
"$workdir/d.$testname/$test_number"
;
untaint_system(
"$salearnargs > $workdir/d.$testname/$test_number"
);
$salearn_exitcode
= ($?>>8);
if
(
$salearn_exitcode
!= 0) {
return
undef
; }
&checkfile
(
"$workdir/d.$testname/$test_number"
,
$read_sub
)
if
(
defined
$read_sub
);
1;
}
sub
saawlrun {
my
$args
=
shift
;
untaint_system(
"$saawl $args"
);
}
sub
sacheckspamdrun {
my
$args
=
shift
;
untaint_system(
"$sacheckspamd $args"
);
}
sub
scrun {
spamcrun (
@_
, 0);
}
sub
scrunwithstderr {
spamcrun (
@_
, 1);
}
sub
scrunwantfail {
spamcrun (
@_
, 1, 1);
}
sub
spamcrun {
my
$args
=
shift
;
my
$read_sub
=
shift
;
my
$capture_stderr
=
shift
;
my
$expect_failure
=
shift
;
if
(
defined
$ENV
{
'SC_ARGS'
}) {
$args
=
$ENV
{
'SC_ARGS'
} .
" "
.
$args
;
}
my
$spamcargs
;
if
(
$args
!~ /\b(?:-p\s*[0-9]+|-F|-U)\b/)
{
$args
=
"-d $spamdhost -p $spamdport $args"
;
}
if
(
$args
!~ /-F/) {
$spamcargs
=
"$spamc -F data/spamc_blank.cf $args"
;
}
else
{
$spamcargs
=
"$spamc $args"
;
}
$spamcargs
=~ s!/!\\!g
if
($^O =~ /^MS(DOS|Win)/i);
print
(
"\t$spamcargs\n"
);
(-d
"$workdir/d.$testname"
) or
mkdir
(
"$workdir/d.$testname"
, 0755);
my
$test_number
= test_number();
if
(
$capture_stderr
) {
untaint_system (
"$spamcargs > $workdir/d.$testname/out.$test_number 2>&1"
);
}
else
{
untaint_system (
"$spamcargs > $workdir/d.$testname/out.$test_number"
);
}
$sa_exitcode
= ($?>>8);
if
(!
$expect_failure
) {
if
(
$sa_exitcode
!= 0) { stop_spamd();
return
undef
; }
}
%found
= ();
%found_anti
= ();
&checkfile
(
"$workdir/d.$testname/out.$test_number"
,
$read_sub
)
if
(
defined
$read_sub
);
if
(
$expect_failure
) {
(
$sa_exitcode
!= 0);
}
else
{
(
$sa_exitcode
== 0);
}
}
sub
spamcrun_background {
my
$args
=
shift
;
my
$read_sub
=
shift
;
if
(
defined
$ENV
{
'SC_ARGS'
}) {
$args
=
$ENV
{
'SC_ARGS'
} .
" "
.
$args
;
}
my
$spamcargs
;
if
(
$args
!~ /\b(?:-p\s*[0-9]+|-o|-U)\b/)
{
$spamcargs
=
"$spamc -p $spamdport $args"
;
}
else
{
$spamcargs
=
"$spamc $args"
;
}
$spamcargs
=~ s!/!\\!g
if
($^O =~ /^MS(DOS|Win)/i);
print
(
"\t$spamcargs &\n"
);
(-d
"$workdir/d.$testname"
) or
mkdir
(
"$workdir/d.$testname"
, 0755);
my
$test_number
= test_number();
untaint_system (
"$spamcargs > $workdir/d.$testname/bg.$test_number &"
) and
return
0;
1;
}
sub
sdrun {
my
$sdargs
=
shift
;
my
$args
=
shift
;
my
$read_sub
=
shift
;
start_spamd (
$sdargs
);
spamcrun (
$args
,
$read_sub
);
stop_spamd ();
1;
}
sub
recreate_outputdir_tmp {
rmtree (
"$workdir/outputdir.tmp"
);
mkdir
(
"$workdir/outputdir.tmp"
,
$tmp_dir_mode
);
chmod
(
$tmp_dir_mode
,
"$workdir/outputdir.tmp"
);
}
sub
start_spamd {
return
if
$SKIP_SPAMD_TESTS
;
die
"TEST_DOES_NOT_RUN_SPAMC_OR_D; in start_spamd! oops"
if
$TEST_DOES_NOT_RUN_SPAMC_OR_D
;
my
$spamd_extra_args
=
shift
;
return
if
(
defined
(
$spamd_pid
) &&
$spamd_pid
> 0);
recreate_outputdir_tmp();
if
(
defined
$ENV
{
'SD_ARGS'
}) {
$spamd_extra_args
=
$ENV
{
'SD_ARGS'
} .
" "
.
$spamd_extra_args
;
}
my
@spamd_args
= (
$spamd
,
qq{-D}
,
qq{-x}
);
if
(!
$spamd_inhibit_log_to_err
) {
push
(
@spamd_args
,
qq{-s}
,
qq{stderr}
,
);
}
if
(
$spamd_extra_args
!~ /(?:-C\s*[^-]\S+)/) {
push
(
@spamd_args
,
$spamd_cf_args
,
$spamd_localrules_args
,
);
}
if
(
$spamd_extra_args
!~ /(?:-p\s*[0-9]+|-o|--socketpath)/) {
push
(
@spamd_args
,
qq{-p}
,
$spamdport
,
);
}
if
(
$spamd_extra_args
!~ /(?:--socketpath)/) {
push
(
@spamd_args
,
qq{-A}
,
$spamdhost
,
qq(-i)
,
$spamdhost
);
}
if
(
$set_test_prefs
) {
warn
"oops! SATest.pm: a test prefs file was created, but spamd isn't reading it\n"
;
}
(-d
"$workdir/d.$testname"
) or
mkdir
(
"$workdir/d.$testname"
, 0755);
my
$test_number
= test_number();
my
$spamd_stdout
=
"$workdir/d.$testname/spamd.out.$test_number"
;
$spamd_stderr
=
"$workdir/d.$testname/spamd.err.$test_number"
;
my
$spamd_stdlog
=
"$workdir/d.$testname/spamd.log.$test_number"
;
my
$spamd_forker
=
$ENV
{
'SPAMD_FORKER'
} ?
$ENV
{
'SPAMD_FORKER'
} :
$RUNNING_ON_WINDOWS
?
"start $perl_path"
:
$perl_path
;
my
$spamd_cmd
=
join
(
' '
,
$spamd_forker
,
qq{SATest.pl}
,
qq{-Mredirect}
,
qq{-O${spamd_stderr}
},
qq{-o${spamd_stdout}
},
qq{--}
,
@spamd_args
,
$spamd_extra_args
,
qq{-s ${spamd_stderr}
.timestamped},
qq{-r ${spamd_pidfile}
},
qq{&}
,
);
unlink
(
$spamd_stdout
,
$spamd_stderr
,
$spamd_stdlog
,
$spamd_pidfile
);
print
(
"\t${spamd_cmd}\n"
);
my
$startat
=
time
;
untaint_system (
$spamd_cmd
);
$spamd_pid
= 0;
my
$retries
= 30;
my
$wait
= 7;
sleep
$wait
;
while
(
$spamd_pid
<= 0) {
my
$spamdlog
=
''
;
my
$pidstr
;
if
(
open
(PID,
$spamd_pidfile
)) {
$pidstr
= <PID>;
close
PID;
}
if
(
$pidstr
) {
chomp
$pidstr
;
$spamd_pid
=
$pidstr
;
dbgprint(
"Found PID $spamd_pid in pidfile\n"
);
last
}
if
(
open
(IN,
"<${spamd_stderr}"
)) {
while
(<IN>) {
/server pid: (\d+)/ and
$spamd_pid
=
"$1"
and dbgprint(
"Found PID $spamd_pid in stderr logfile\n"
);
if
( !(/dbg: config: .
*rulename
/) && (/\bERROR/) ){
warn
"spamd start failed - spamd error! $_\nExiting test with debug output"
;
$retries
= 0;
last
;
}
$spamdlog
.=
$_
;
}
close
IN;
last
if
(
$spamd_pid
);
}
my
$sleep
= (
int
(
$wait
++ / 4) + 1);
warn
"spam_pid not found: Sleeping $sleep - Retry # $retries\n"
if
$retries
&&
$retries
< 20;
sleep
$sleep
if
$retries
> 0;
if
(
$retries
-- <= 0) {
warn
"spamd start failed - Could not find a valid PID.\nEnd Debug log -------------------\n$spamdlog\nEnd Debug log -------------------"
;
warn
"\n\nMaybe you need to kill a running spamd process?\n"
;
warn
"Or the start took too long. Started at $startat, gave up at "
.
time
.
"\n\n"
;
return
0;
}
}
1;
}
sub
stop_spamd {
return
0
if
(
defined
(
$spamd_already_killed
) ||
$SKIP_SPAMD_TESTS
);
die
"TEST_DOES_NOT_RUN_SPAMC_OR_D; in stop_spamd! oops"
if
$TEST_DOES_NOT_RUN_SPAMC_OR_D
;
$spamd_pid
||= 0;
$spamd_pid
= untaint_var(
$spamd_pid
);
if
(
$spamd_pid
<= 1) {
print
(
"Invalid spamd pid: $spamd_pid. Spamd not started/crashed?\n"
);
return
0;
}
else
{
my
$killed
=
kill
(15,
$spamd_pid
);
print
(
"Killed $killed spamd instances\n"
);
for
my
$waitfor
(0 .. 5) {
my
$killstat
;
if
((
$killstat
=
kill
(0,
$spamd_pid
)) == 0) {
last
; }
print
(
"Waiting for spamd at pid $spamd_pid to exit...\n"
);
sleep
1;
}
$spamd_pid
= 0;
$spamd_already_killed
= 1;
return
$killed
;
}
}
sub
create_saobj {
my
(
$args
) =
shift
;
my
%setup_args
= (
rules_filename
=>
$localrules
,
site_rules_filename
=>
$siterules
,
userprefs_filename
=>
$userrules
,
userstate_dir
=>
$userstate
,
local_tests_only
=> 1,
);
foreach
my
$arg
(
keys
%$args
) {
$setup_args
{
$arg
} =
$args
->{
$arg
};
}
my
$sa
= Mail::SpamAssassin->new(\
%setup_args
);
return
$sa
;
}
sub
create_clientobj {
my
$args
=
shift
;
my
$client
= Mail::SpamAssassin::Client->new(
$args
);
return
$client
;
}
sub
checkfile {
my
$filename
=
shift
;
my
$read_sub
=
shift
;
if
(!
open
(IN,
"< $filename"
)) {
warn
"cannot open $filename"
;
return
undef
;
}
else
{
push
@files_checked
,
"$filename"
;
}
&$read_sub
();
close
IN;
}
sub
patterns_run_cb {
my
$string
=
shift
;
if
(!
defined
$string
) {
$string
=
join
(
''
, <IN>);
}
$matched_output
=
$string
;
my
%seen
;
foreach
my
$pat
(
keys
%patterns
) {
if
(
$patterns
{
$pat
} eq
''
) {
$patterns
{
$pat
} =
$pat
;
}
if
(
$seen
{
$patterns
{
$pat
}}++) {
die
"ERROR: duplicate pattern name found: '$patterns{$pat}'\n"
;
}
}
%seen
= ();
foreach
my
$pat
(
keys
%anti_patterns
) {
if
(
$anti_patterns
{
$pat
} eq
''
) {
$anti_patterns
{
$pat
} =
$pat
;
}
if
(
$seen
{
$anti_patterns
{
$pat
}}++) {
die
"ERROR: duplicate anti_pattern name found: '$anti_patterns{$pat}'\n"
;
}
}
foreach
my
$pat
(
sort
keys
%patterns
) {
if
(
index
(
$pat
,
'(?^'
) == 0) {
if
(
$string
=~
$pat
) {
$found
{
$patterns
{
$pat
}}++;
}
}
else
{
my
$re
=
$pat
;
$re
=~ s/([^A-Za-z_0-9\s])/\\$1/gs;
$re
=~ s/\s+/\\s+/gs;
eval
{
$re
=
qr/$re/
; 1; };
if
($@) {
die
"ERROR: failed to compile regex: '$re'\n"
; }
if
(
$string
=~
$re
) {
$found
{
$patterns
{
$pat
}}++;
}
}
}
foreach
my
$pat
(
sort
keys
%anti_patterns
) {
if
(
index
(
$pat
,
'(?^'
) == 0) {
if
(
$string
=~
$pat
) {
$found_anti
{
$anti_patterns
{
$pat
}}++;
}
}
else
{
my
$re
=
$pat
;
$re
=~ s/([^A-Za-z_0-9\s])/\\$1/gs;
$re
=~ s/\s+/\\s+/gs;
eval
{
$re
=
qr/$re/
; 1; };
if
($@) {
die
"ERROR: failed to compile regex: '$re'\n"
; }
if
(
$string
=~
$re
) {
$found_anti
{
$anti_patterns
{
$pat
}}++;
}
}
}
}
sub
ok_all_patterns {
my
$dont_ok
=
shift
;
my
$show_log
=
shift
;
my
(
undef
,
$file
,
$line
) =
caller
();
my
$wasfailure
= 0;
foreach
my
$pat
(
sort
keys
%patterns
) {
my
$type
=
$patterns
{
$pat
};
print
"\tChecking $type\n"
;
if
(
defined
$found
{
$type
}) {
if
(!
$dont_ok
) {
ok (
$found
{
$type
} == 1) or
warn
"Found more than once: $type at $file line $line.\n"
;
}
}
else
{
my
$typestr
=
$type
eq
$pat
?
""
:
"$type = "
;
warn
"\tNot found: $typestr$pat at $file line $line.\n"
;
if
(!
$dont_ok
) {
$keep_workdir
= 1;
ok (0);
}
$wasfailure
++;
}
}
foreach
my
$pat
(
sort
keys
%anti_patterns
) {
my
$type
=
$anti_patterns
{
$pat
};
print
"\tChecking for anti-pattern $type at $file line $line.\n"
;
if
(
defined
$found_anti
{
$type
}) {
my
$typestr
=
$type
eq
$pat
?
""
:
"$type = "
;
warn
"\tFound anti-pattern: $typestr$pat at $file line $line.\n"
;
if
(!
$dont_ok
) { ok (0); }
$wasfailure
++;
}
else
{
if
(!
$dont_ok
) { ok (1); }
}
}
if
(
$wasfailure
) {
if
(
@files_checked
) {
warn
"Output can be examined in: "
.
join
(
' '
,
@files_checked
).
"\n"
;;
$keep_workdir
= 1;
if
(
$show_log
) {
for
my
$logfile
(
@files_checked
) {
warn
"Contents of "
.
$logfile
;
open
(
my
$logfilehandle
,
'<'
,
"$logfile"
) or
die
"Can't open < $logfile: $!"
.
" as "
. Cwd::abs_path(
$logfile
);
while
(<
$logfilehandle
>) {
warn
$_
;
}
}
}
}
return
0;
}
else
{
return
1;
}
}
sub
skip_all_patterns {
my
$skip
=
shift
;
my
(
undef
,
$file
,
$line
) =
caller
();
foreach
my
$pat
(
sort
keys
%patterns
) {
my
$type
=
$patterns
{
$pat
};
print
"\tChecking $type\n"
;
if
(
defined
$found
{
$type
}) {
skip (
$skip
,
$found
{
$type
} == 1) or
warn
"Found more than once: $type at $file line $line.\n"
;
warn
"\tThis test should have been skipped: $skip at $file line $line.\n"
if
$skip
;
}
else
{
if
(
$skip
) {
warn
"\tTest skipped: $skip at $file line $line.\n"
;
}
else
{
my
$typestr
=
$type
eq
$pat
?
""
:
"$type = "
;
warn
"\tNot found: $typestr$pat at $file line $line.\n"
;
}
skip (
$skip
, 0);
}
}
foreach
my
$pat
(
sort
keys
%anti_patterns
) {
my
$type
=
$anti_patterns
{
$pat
};
print
"\tChecking for anti-pattern $type\n"
;
if
(
defined
$found_anti
{
$type
}) {
my
$typestr
=
$type
eq
$pat
?
""
:
"$type = "
;
warn
"\tFound anti-pattern: $typestr$pat at $file line $line.\n"
;
skip (
$skip
, 0);
}
else
{
skip (
$skip
, 1);
}
}
}
sub
clear_pattern_counters {
%found
= ();
%found_anti
= ();
@files_checked
= ();
}
sub
read_config {
return
if
defined
(
$already_read_config
);
$already_read_config
= 1;
my
$prefix
=
''
;
if
(-f
't/test_dir'
) {
$prefix
=
"t/"
; }
if
(!
open
(CF,
"<${prefix}config"
)) {
if
(!
open
(CF,
"<${prefix}config.dist"
)) {
die
"cannot open test suite configuration file 'config.dist': $!"
;
}
}
while
(<CF>) {
s/
/^([^=]+)=(.*)$/ or
next
;
$conf
{$1} = $2;
}
if
(
defined
$ARGV
[0] &&
$ARGV
[0] eq
'--override'
) {
shift
@ARGV
;
my
$k
=
shift
@ARGV
;
my
$v
=
shift
@ARGV
;
@k
=
split
(/:/,
$k
);
@v
=
split
(/:/,
$v
);
if
(
scalar
(
@k
) !=
scalar
(
@v
)) {
print
"Error: The number of override arguments for variables and values did not match\n!"
;
exit
;
}
else
{
print
"\nProcessing Overrides:\n\n"
;
}
for
(
my
$i
= 0;
$i
<
scalar
(
@k
);
$i
++) {
$conf
{
$k
[
$i
]} =
$v
[
$i
];
print
"Overriding $k[$i] with value $v[$i]\n"
;
}
}
close
CF;
}
sub
conf {
read_config();
return
$conf
{
$_
[0]};
}
sub
conf_bool {
my
$val
= conf(
$_
[0]);
return
0
unless
defined
(
$val
);
return
1
if
(
$val
=~ /^y/i);
return
(
$val
+0)
if
(
$val
=~ /^\d/);
return
0;
}
sub
mk_socket_tempdir {
my
$dir
= tempdir(
CLEANUP
=> 1);
die
"FATAL: failed to create socket_tempdir: $!"
unless
-d
$dir
;
return
$dir
;
}
sub
wait_for_file_to_change_or_disappear {
my
(
$f
,
$timeout
,
$action
) =
@_
;
my
$lastmod
= (-M
$f
);
$action
->();
my
$wait
= 0;
my
$newlastmod
;
do
{
sleep
(
int
(
$wait
++ / 4) + 1)
if
$timeout
> 0;
$timeout
--;
$newlastmod
= (-M
$f
);
}
while
((-e
$f
) &&
defined
(
$newlastmod
) &&
$newlastmod
==
$lastmod
&&
$timeout
);
}
sub
wait_for_file_to_appear {
my
(
$f
,
$timeout
) =
@_
;
my
$wait
= 0;
do
{
sleep
(
int
(
$wait
++ / 4) + 1)
if
$timeout
> 0;
$timeout
--;
}
while
((!-e
$f
|| -z
$f
) &&
$timeout
);
}
sub
read_from_pidfile {
my
$f
=
shift
;
my
$npid
= 0;
my
$retries
= 5;
do
{
if
(
$retries
!= 5) {
sleep
1;
warn
"retrying read of pidfile $f, due to short/nonexistent read: "
.
"retry $retries"
;
}
$retries
--;
if
(!
open
(PID,
"<"
.
$f
)) {
warn
"Could not open pid file ${f}: $!\n"
;
next
;
}
$npid
= <PID>;
if
(
defined
$npid
) {
chomp
$npid
; }
close
(PID);
$npid
= untaint_var(
$npid
);
if
(!
$npid
||
$npid
< 1) {
warn
"failed to read anything sensible from $f, retrying read"
;
$npid
= 0;
next
;
}
if
(!
kill
(0,
$npid
)) {
warn
"failed to kill -0 $npid, retrying read"
;
$npid
= 0;
}
}
until
(
$npid
> 1 or
$retries
== 0);
return
$npid
;
}
sub
system_or_die {
my
$cmd
=
$_
[0];
print
(
"\t$cmd\n"
);
untaint_system(
$cmd
);
$? == 0 or
die
"'$cmd' failed: "
.exit_status_str($?,0);
}
sub
exit_status_str($;$) {
my
(
$stat
,
$errno
) =
@_
;
my
$str
;
if
(WIFEXITED(
$stat
)) {
$str
=
sprintf
(
"exit %d"
, WEXITSTATUS(
$stat
));
}
elsif
(WIFSTOPPED(
$stat
)) {
$str
=
sprintf
(
"stopped, signal %d"
, WSTOPSIG(
$stat
));
}
else
{
my
$sig
= WTERMSIG(
$stat
);
$str
=
sprintf
(
"%s, signal %d (%04x)"
,
$sig
== 2 ?
'INTERRUPTED'
:
$sig
== 6 ?
'ABORTED'
:
$sig
== 9 ?
'KILLED'
:
$sig
== 15 ?
'TERMINATED'
:
'DIED'
,
$sig
,
$stat
);
}
if
(
defined
$errno
) {
$str
.=
', '
.
$errno
if
(0+
$errno
) != 0 || (
$errno
ne
''
&&
$errno
ne
'0'
);
}
return
$str
;
}
sub
dbgprint {
print
STDOUT
"["
.
time
().
"] "
.
$_
[0]; }
sub
can_use_net_dns_safely {
return
unless
eval
{
require
Net::DNS; };
return
1
if
($< != 0);
return
1
if
($^O =~ /^(linux|mswin|dos|os2|openbsd)/oi);
my
$has_unsafe_hostname
=
return
1
if
!
$has_unsafe_hostname
;
return
;
}
sub
nameservers_for_safer_use {
my
$nsprefs
=
''
;
if
(
$RUNNING_ON_WINDOWS
&& can_use_net_dns_safely()) {
my
$resolver
= Net::DNS::Resolver->new;
my
@nameservers
=
$resolver
->nameservers;
foreach
my
$ns
(
@nameservers
) {
$nsprefs
.=
"dns_server $ns\n"
;
}
$nsprefs
=
q(
dns_server 1.1.1.1
dns_server 8.8.8.8
)
;
}
return
$nsprefs
;
}
sub
debug_hash {
my
(
$hash
) =
@_
;
my
(
$string
,
$key
,
@keys
,
@sorted
,
$i
);
if
(
uc
(
ref
(
$hash
)) eq
"HASH"
) {
foreach
$key
(
keys
%$hash
) {
push
(
@keys
,
$key
);
}
@sorted
=
sort
@keys
;
for
(
$i
=0;
$i
<
scalar
(
@sorted
);
$i
++) {
if
(
uc
(
ref
(
$hash
->{
$sorted
[
$i
]})) eq
'HASH'
) {
$string
.=
"$sorted[$i] = "
.debug_hash(
$hash
->{
$sorted
[
$i
]}).
"\n"
;
}
else
{
$string
.=
"$sorted[$i] = $hash->{$sorted[$i]}\n"
;
}
}
}
else
{
warn
(
uc
(
ref
(
$hash
)) .
" is not a HASH\n"
);
}
return
$string
;
}
sub
debug_array {
my
(
$array
) =
@_
;
my
(
$string
,
$i
);
if
(
uc
(
ref
(
$array
)) eq
"ARRAY"
) {
for
(
$i
=0;
$i
<
scalar
(
@$array
);
$i
++) {
$string
.=
"Array Element $i = $array->[$i]\n"
;
}
}
return
$string
;
}
sub
test_number {
return
Test::More->builder->current_test;
}
sub
untaint_var {
local
($1);
$_
[0] =~ /^(.*)\z/s;
return
$1;
}
sub
untaint_system {
my
@args
;
push
@args
, untaint_var(
$_
)
foreach
(
@_
);
return
system
(
@args
);
}
sub
untaint_cmd {
if
(
open
(CMD, untaint_var(
$_
[0]).
"|"
)) {
my
$stdout
=
do
{
local
($/); <CMD> };
close
CMD;
return
$stdout
;
}
else
{
return
""
;
}
}
END {
if
(
defined
$workdir
&& !
$keep_workdir
&& $$ ==
$mainpid
) {
rmtree(
$workdir
);
}
}
1;