5 ##################################################
7 ##################################################
16 use HTTP
::Cookies
::Netscape
;
17 use File
::Temp qw
/ tempfile /;
18 use File
::Copy qw
/ cp /;
23 plan
( skip_all
=> "creating testsites requires root permission" );
25 our $omd_symlink_created = 0;
27 ##################################################
28 # HTML::Lint installed?
29 my $use_html_lint = 0;
35 ##################################################
36 # dont test over a proxy
37 delete $ENV{'HTTP_PROXY'};
38 delete $ENV{'HTTPS_PROXY'};
39 delete $ENV{'FTP_PROXY'};
40 delete $ENV{'http_proxy'};
41 delete $ENV{'https_proxy'};
42 delete $ENV{'ftp_proxy'};
45 ##################################################
49 returns path to omd binary
55 return $omd_bin if defined $omd_bin;
57 $omd_bin = $ENV{'OMD_BIN'} || '/usr/bin/omd';
61 if($omd_bin eq '/usr/bin/omd') {
62 BAIL_OUT
('Broken installation, got /usr/bin/omd but no /omd')
63 } elsif($omd_bin eq 'destdir/opt/omd/versions/default/bin/omd') {
64 symlink(getcwd
()."/destdir/omd", '/omd');
65 $omd_symlink_created = 1;
67 BAIL_OUT
('did not find a valid /omd, please make sure it exists')
72 my $target = readlink('/omd');
73 if($omd_bin eq '/usr/bin/omd') {
74 if($target ne "/opt/omd" && $target ne "opt/omd") {
75 BAIL_OUT
('symlink for /omd already exists but is wrong: should be: /opt/omd but got: '.$target);
78 elsif($omd_bin eq 'destdir/opt/omd/versions/default/bin/omd') {
79 if($target ne getcwd
()."/destdir/omd") {
80 BAIL_OUT
('symlink for /omd already exists but is wrong: should be: '.getcwd
().'/destdir/omd but got: '.$target);
84 BAIL_OUT
('cannot run tests, /omd has to be a symlink to '.getcwd
().'/destdir/omd (or /opt/omd for testing packages) in order to run tests for the source version');
88 -x
$omd_bin or BAIL_OUT
($omd_bin." is required for further tests: $!");
93 ##################################################
97 execute a test command
101 cmd => command line to execute
102 exit => expected exit code (set to undef to ignore exit code verification)
103 like => (list of) regular expressions which have to match stdout
104 errlike => (list of) regular expressions which have to match stderr, default: empty
105 sleep => time to wait after executing the command
111 my($rc, $stderr) = ( -1, '') ;
115 isnt
($test->{'cmd'}, undef, "running cmd: ".$test->{'cmd'}) or $return = 0;
117 my($prg,$arg) = split(/\s+/, $test->{'cmd'}, 2);
118 my $t = Test
::Cmd
->new(prog
=> $prg, workdir
=> '') or die($!);
121 local $SIG{ALRM
} = sub { die "timeout on cmd: ".$test->{'cmd'}."\n" };
122 $t->run(args
=> $arg, stdin
=> $test->{'stdin'});
128 $stderr = $t->stderr;
129 $stderr = TestUtils
::_clean_stderr
($stderr);
134 $test->{'exit'} = 0 unless exists $test->{'exit'};
135 if(defined $test->{'exit'} and $test->{'exit'} != -1) {
136 ok
($rc == $test->{'exit'}, "exit code: ".$rc." == ".$test->{'exit'}) or do { _diag_cmd
($test, $t); $return = 0 };
140 if(defined $test->{'like'}) {
141 for my $expr (ref $test->{'like'} eq 'ARRAY' ? @
{$test->{'like'}} : $test->{'like'} ) {
142 like
($t->stdout, $expr, "stdout like ".$expr) or do { diag
("\ncmd: '".$test->{'cmd'}."' failed\n"); $return = 0 };
147 $test->{'errlike'} = '/^\s*$/' unless exists $test->{'errlike'};
148 if(defined $test->{'errlike'}) {
149 for my $expr (ref $test->{'errlike'} eq 'ARRAY' ? @
{$test->{'errlike'}} : $test->{'errlike'} ) {
150 like
($stderr, $expr, "stderr like ".$expr) or do { diag
("\ncmd: '".$test->{'cmd'}."' failed"); $return = 0 };
154 # sleep after the command?
155 if(defined $test->{'sleep'}) {
156 ok
(sleep($test->{'sleep'}), "slept $test->{'sleep'} seconds") or do { $return = 0 };
160 $test->{'stdout'} = $t->stdout;
161 $test->{'stderr'} = $t->stderr;
162 $test->{'exit'} = $rc;
168 ##################################################
172 verify contents of a file
176 file => file to check
177 like => (list of) regular expressions which have to match
178 unlike => (list of) regular expressions which must not match stderr
188 if(defined $test->{'like'}) {
189 @like = ref $test->{'like'} eq 'ARRAY' ? @
{$test->{'like'}} : $test->{'like'};
192 if(defined $test->{'unlike'}) {
193 @unlike = ref $test->{'unlike'} eq 'ARRAY' ? @
{$test->{'unlike'}} : $test->{'unlike'};
196 ok
(-r
$test->{'file'}, $test->{'file'}." does exist");
199 skip
'file missing', (scalar @like + scalar @unlike) unless -r
$test->{'file'};
202 open my $fh, $test->{'file'} or die "Couldn't open file ".$test->{'file'}.": $!";
207 if(defined $test->{'like'}) {
208 for my $expr (@like) {
209 like
($content, $expr, "content like ".$expr) or $failed++;
214 if(defined $test->{'unlike'}) {
215 for my $expr (@unlike) {
216 unlike
($content, $expr, "output unlike ".$expr) or $failed++;
221 return 1 unless $failed;
226 ##################################################
228 =head2 create_test_site
230 creates a test site and returns the name
233 sub create_test_site
{
234 my $site = $_[0] || "testsite";
235 if(test_command
({ cmd
=> TestUtils
::get_omd_bin
()." create $site" })) {
242 ##################################################
244 =head2 remove_test_site
249 sub remove_test_site
{
251 # kill all processes, sometimes checks are still running and prevent us from removing the site
252 test_command
({ cmd
=> "/usr/bin/pkill -2 -U $site; sleep 1;".TestUtils
::get_omd_bin
()." rm $site", stdin
=> "yes\n" });
257 ##################################################
265 url => url to request
266 auth => authentication (realm:user:pass)
267 code => expected response code
268 like => (list of) regular expressions which have to match content
269 unlike => (list of) regular expressions which must not match content
270 skip_html_lint => flag to disable the html lint checking
271 skip_link_check => (list of) regular expressions to skip the link checks for
272 waitfor => wait till regex occurs (max 120sec)
280 my $page = _request
($test);
282 # wait for something?
283 if(defined $test->{'waitfor'}) {
285 my $waitfor = $test->{'waitfor'};
287 while($now < $start + 120) {
288 if($page->{'content'} =~ m/$waitfor/mx) {
289 ok
(1, "content ".$waitfor." found after ".($now - $start)."seconds");
295 $page = _request
($test);
297 fail
("content did not occur within 120 seconds") unless $found;
302 $test->{'code'} = 200 unless exists $test->{'code'};
303 if(defined $test->{'code'}) {
304 is
($page->{'code'}, $test->{'code'}, "response code for ".$test->{'url'}." is: ".$test->{'code'}) or _diag_request
($test, $page);
308 if(defined $test->{'content_type'}) {
309 is
($page->{'content_type'}, $test->{'content_type'}, 'Content-Type is: '.$test->{'content_type'});
313 if(defined $test->{'like'}) {
314 defined $page->{'content'} or _diag_request
($test, $page);
315 for my $expr (ref $test->{'like'} eq 'ARRAY' ? @
{$test->{'like'}} : $test->{'like'} ) {
316 like
($page->{'content'}, $expr, "content like ".$expr);
320 # not matching output
321 if(defined $test->{'unlike'}) {
322 for my $expr (ref $test->{'unlike'} eq 'ARRAY' ? @
{$test->{'unlike'}} : $test->{'unlike'} ) {
323 unlike
($page->{'content'}, $expr, "content unlike ".$expr) or _diag_request
($test, $page);
329 if($page->{'content_type'} =~ 'text\/html') {
330 unless(defined $test->{'skip_html_lint'} && $test->{'skip_html_lint'} == 1) {
331 if($use_html_lint == 0) {
332 skip
"no HTML::Lint installed", 2;
334 if($page->{'content'} =~ /^\[.*\]$/mx || $page->{'content'} =~ /^\{.*\}$/mx) {
335 skip
"no lint check for json data", 2;
337 if($ENV{LINTSKIPPATTERN
} && $test->{'url'} =~ m
|/$ENV{LINTSKIPPATTERN}/|mx
) {
338 skip
"lint check skipped by LINTSKIPPATTERN: ".$ENV{LINTSKIPPATTERN
}, 2;
340 my $lint = new HTML
::Lint
;
341 isa_ok
( $lint, "HTML::Lint" );
343 $lint->parse($page->{'content'});
344 my @errors = $lint->errors;
345 @errors = _diag_lint_errors_and_remove_some_exceptions
($lint);
346 is
( scalar @errors, 0, "No errors found in HTML (".$test->{'url'}.")" );
347 $lint->clear_errors();
352 # check for missing images / css or js
353 if($page->{'content_type'} =~ 'text\/html'
354 and (!defined $test->{'skip_html_links'} or $test->{'skip_html_links'} == 0)
356 my $content = $page->{'content'};
357 $content =~ s/<\!\-\-.*?\-\->//gsmx;
358 my @matches = $content =~ m/(src|href)=['|"](.+?)['|"]/gi;
361 for my $match (@matches) {
364 next if $match =~ m/^http/;
365 next if $match =~ m/^mailto:/;
366 next if $match =~ m/^#/;
367 next if $match =~ m/^javascript:/;
368 next if $match =~ m/internal&srv=runtime/;
369 next if $match =~ m/this\./;
370 if(defined $test->{'skip_link_check'}) {
372 for my $expr (ref $test->{'skip_link_check'} eq 'ARRAY' ? @
{$test->{'skip_link_check'}} : $test->{'skip_link_check'} ) {
373 if($skip == 0 and $match =~ m/$expr/) {
379 $links_to_check->{$match} = 1;
382 for my $test_url (keys %{$links_to_check}) {
383 $test_url = _get_url
($test->{'url'}, $test_url);
384 our $already_checked;
385 $already_checked = {} unless defined $already_checked;
386 next if defined $already_checked->{$test_url};
387 #diag("checking link: ".$test_url);
388 my $req = _request
({url
=> $test_url, auth
=> $test->{'auth'}});
389 if($req->{'response'}->is_redirect) {
391 while(my $location = $req->{'response'}->{'_headers'}->{'location'}) {
392 if($location !~ m/^(http|\/)/gmx
) { $location = _relative_url
($location, $req->{'response'}->base()->as_string()); }
393 $req= _request
($location);
395 last if $redirects > 10;
398 if($req->{'code'} == 200) {
399 $already_checked->{$test_url} = 1;
402 diag
("got status ".$req->{'code'}." for url: '$test_url'");
404 my $tmp_test = { 'url' => $test_url };
405 _diag_request
($tmp_test, $req);
406 TestUtils
::bail_out_clean
("error in url '$test_url' linked from '".$test->{'url'}."'");
409 is
( $errors, 0, 'All stylesheets, images and javascript exist' );
414 ##################################################
424 return $config->{$key} if defined $config;
426 my $conf_file = "/omd/versions/default/share/omd/distro.info";
427 $config = read_config
($conf_file);
429 return $config->{$key};
433 ##################################################
437 return config from file
441 my $conf_file = shift;
444 open(my $fh, '<', $conf_file) or carp
("cannot open $conf_file: $!");
448 next if $line =~ m/^\s*(#|$)/;
449 $line =~ s/\s*#.*$//;
451 my($key,$value) = split/\s+\+=\s*/,$line,2;
455 ($key,$value) = split/\s+=\s*/,$line,2;
458 $value =~ s/\s+$// if defined $value;
460 $config->{$key} .= " ".$value;
462 $config->{$key} = $value;
471 ##################################################
480 my $timeout = shift || 120;
482 my $testfile = glob($file);
483 $file = $testfile if defined $testfile;
487 pass
("file: $file does already exist");
490 while($x < $timeout) {
492 pass
("file: $file appeared after $x seconds");
498 fail
("file: $file did not appear within $x seconds");
503 ##################################################
505 =head2 wait_for_content
507 waits for web page content until timeout
511 url => url to request
512 auth => authentication (realm:user:pass)
513 code => expected response code
514 like => (list of) regular expressions which have to match content
518 sub wait_for_content
{
520 my $timeout = shift || 120;
524 while ($x < $timeout) {
525 $req = _request
($test);
526 if($req->{'code'} == 200) {
527 #diag("code:$req->{code} url:$test->{url} auth:$test->{auth}");
529 for my $pattern (@
{$test->{'like'}}) {
530 if ($req->{'content'}!~/$pattern/) {
531 #diag("errors:$errors pattern:$pattern");
536 pass
(sprintf "content: [ %s ] appeared after $x seconds", join(',',@
{$test->{'like'}}));
540 diag
("Error searching for web content:\ncode:$req->{code}\nurl:$test->{url}\nauth:$test->{auth}\ncontent:$req->{content}");
545 fail
(sprintf "content: [ %s ] did not appear within $x seconds", join(',',@
{$test->{'like'}}));
550 ##################################################
552 =head2 prepare_obj_config
554 prepare test object config
557 sub prepare_obj_config
{
562 my $files = join(" ", (ref $src eq 'ARRAY' ? @
{$src} : $src));
563 for my $file (`find $files -type f`) {
566 if(-d
$dst) { $dstfile = $dst.'/'.basename
($file); }
567 cp
($file, $dstfile) or die("copy $file $dstfile failed: $!");
568 test_command
({ cmd
=> "/usr/bin/env sed -i $dstfile -e 's/###SITE###/".$site."/' -e 's|###ROOT###|/omd/sites/".$site."|'" }) if defined $site;
575 ##################################################
577 =head2 bail_out_clean
579 bail out from testing but some minor cleanup before
585 carp
("cleaning up before bailout");
587 my $omd_bin = get_omd_bin
();
588 for my $site (qw
/testsite testsite2 testsite3/) {
589 test_command
({ cmd
=> $omd_bin." rm $site", stdin
=> "yes\n", 'exit' => undef, errlike
=> undef });
596 ##################################################
598 =head2 _diag_lint_errors_and_remove_some_exceptions
600 removes some lint errors we want to ignore
603 sub _diag_lint_errors_and_remove_some_exceptions
{
606 LINT_ERROR
: for my $error ( $lint->errors ) {
607 my $err_str = $error->as_string;
608 for my $exclude_pattern (
609 "<IMG SRC=[^>]*>\ tag\ has\ no\ HEIGHT\ and\ WIDTH\ attributes",
610 "<IMG SRC=[^>]*>\ does\ not\ have\ ALT\ text\ defined",
611 "<input>\ is\ not\ a\ container\ \-\-\ <\/input>\ is\ not\ allowed",
612 "Unknown attribute \"start\" for tag <div>",
613 "Unknown attribute \"end\" for tag <div>",
615 "Unknown\ attribute\ \"placeholder\"\ for\ tag\ <input>",
616 "Unknown\ attribute\ \"autocomplete\"\ for\ tag\ <form>",
617 "Unknown\ attribute\ \"autocomplete\"\ for\ tag\ <input>",
619 next LINT_ERROR
if($err_str =~ m/$exclude_pattern/i);
621 diag
($error->as_string."\n");
622 push @return, $error;
628 ##################################################
636 url => url to request
637 auth => authentication (realm:user:pass)
645 our($fh, $cookie_jar, $cookie_file);
647 if(!defined $cookie_jar) {
648 ($fh, $cookie_file) = tempfile
();
649 unlink ($cookie_file);
650 $cookie_jar = HTTP
::Cookies
::Netscape
->new(
651 file
=> $cookie_file,
656 my $ua = LWP
::UserAgent
->new(
660 requests_redirectable
=> ['GET', 'POST'],
664 $ua->cookie_jar($cookie_jar);
666 if(defined $data->{'auth'}) {
667 $data->{'url'} =~ m/(http|https):\/\
/(.*?)(\/|:\d
+)/;
670 if(defined $port and $port =~ m/^:(\d+)/) { $port = $1; } else { $port = 80; }
671 my($realm,$user,$pass) = split(/:/, $data->{'auth'}, 3);
672 $ua->credentials($netloc.":".$port, $realm, $user, $pass);
676 if(defined $data->{'post'}) {
677 $response = $ua->post($data->{'url'}, $data->{'post'});
679 $response = $ua->get($data->{'url'});
682 $return->{'response'} = $response;
683 $return->{'code'} = $response->code;
684 $return->{'content'} = $response->decoded_content || $response->content;
685 $return->{'content_type'} = $response->header('Content-Type');
691 ##################################################
695 returns a absolute url
707 # split original url in host, path and file
708 if($url =~ m/^(http|https):\/\
/([^\/]*)(|\
/|:\d+)(.*?)$/) {
709 my $host = $1."://".$2.$3;
710 $host =~ s/\/$//; # remove last /
711 my $fullpath = $4 || '';
712 $fullpath =~ s/\?.*$//;
713 $fullpath =~ s/^\///;
714 my($path,$file) = ('', '');
715 if($fullpath =~ m/^(.+)\/(.*)$/) {
722 $path =~ s/^\///; # remove first /
724 if($link =~ m/^(http|https):\/\
//) {
727 elsif($link =~ m/^\//) { # absolute link
731 return $host."/".$link;
733 return $host."/".$path."/".$link;
737 TestUtils
::bail_out_clean
("unknown url scheme in _get_url: '".$url."'");
744 ##################################################
748 remove some know errors from stderr
752 my $text = shift || '';
753 $text =~ s/[\w\-]+: Could not reliably determine the server's fully qualified domain name, using .*? for ServerName//g;
754 $text =~ s/\[warn\] module \w+ is already loaded, skipping//g;
755 $text =~ s/Syntax OK//g;
756 $text =~ s/no crontab for \w+//g;
760 ##################################################
764 print diagnostic output for failed commands
770 my $stdout = $cmd->stdout || '';
771 my $stderr = $cmd->stderr || '';
772 diag
("\ncmd: '".$test->{'cmd'}."' failed\n");
773 diag
("stdout: ".$stdout."\n");
774 diag
("stderr: ".$stderr."\n");
776 # check logfiles on apache errors
777 if( $stdout =~ m/Starting dedicated Apache for site (\w+)[\.\ ]*ERROR/
778 or $stdout =~ m/500 Internal Server Error/) {
780 _tail
("apache logs:", "/omd/sites/$site/var/log/apache/error_log") if defined $site;
783 if( $stderr =~ m/User '(\w+)' still logged in or running processes/ ) {
785 diag
("ps: ".`ps -fu $site`) if $site;
790 ##################################################
794 print diagnostic output for failed requests
801 diag
(Dumper
($page->{'response'}));
803 $test->{'url'} =~ m/localhost\/(\w
+)\
//;
805 return unless defined $site;
807 # check logfiles on apache errors
808 _tail
("apache logs:", "/omd/sites/$site/var/log/apache/error_log");
810 _tail
("thruk logs:", "/omd/sites/$site/var/log/thruk.log") if $test->{'url'} =~ m
/\
/thruk\//;
815 ##################################################
825 return unless defined $file;
828 diag
(`tail -n100 $file`);
830 diag
("cannot read $file: $!");
836 ##################################################
838 =head2 _tail_apache_logs
840 print tail of all apache logs
843 sub _tail_apache_logs
{
844 _tail
("global apache logs:", glob('/var/log/apache*/error*log'));
845 _tail
("global apache logs:", glob('/var/log/httpd*/error*log'));
849 ##################################################
851 =head2 restart_system_apache
853 restart system apache
856 sub restart_system_apache
{
857 my $name = TestUtils
::config
('APACHE_INIT_NAME');
858 my $init = TestUtils
::config
('INIT_CMD');
860 $cmd =~ s/\Q%(name)s\E/$name/mx;
862 $stop =~ s/\Q%(action)s\E/stop/mx;
864 $start =~ s/\Q%(action)s\E/start/mx;
865 $cmd = $stop.'; sleep 3; '.$start;
866 TestUtils
::test_command
({ cmd
=> $cmd });
869 ##################################################
873 unlink($cookie_file) if $cookie_file;
874 if(defined $omd_symlink_created and $omd_symlink_created == 1) {