# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # package Apache::TestConfig; #not TestConfigPerl on purpose #things specific to mod_perl use strict; use warnings FATAL => 'all'; use File::Spec::Functions qw(catfile splitdir abs2rel file_name_is_absolute); use File::Find qw(finddepth); use Apache::TestTrace; use Apache::TestRequest; use Config; my %libmodperl = (1 => 'libperl.so', 2 => 'mod_perl.so'); sub configure_libmodperl { my $self = shift; my $server = $self->{server}; my $libname = $server->version_of(\%libmodperl); my $vars = $self->{vars}; if ($vars->{libmodperl}) { # if set, libmodperl was specified from the command line and # should be used instead of the one that is looked up # resolve a non-absolute path $vars->{libmodperl} = $self->find_apache_module($vars->{libmodperl}) unless file_name_is_absolute($vars->{libmodperl}); } # $server->{rev} could be set to 2 as a fallback, even when # the wanted version is 1. So check that we use mod_perl 2 elsif ($server->{rev} >= 2 && IS_MOD_PERL_2) { if (my $build_config = $self->modperl_build_config()) { if ($build_config->{MODPERL_LIB_SHARED}) { $libname = $build_config->{MODPERL_LIB_SHARED}; $vars->{libmodperl} ||= $self->find_apache_module($libname); } else { $vars->{libmodperl} ||= $self->find_apache_module('mod_perl.so'); } # XXX: we have a problem with several perl trees pointing # to the same httpd tree. So it's possible that we # configure the test suite to run with mod_perl.so built # against perl which it wasn't built with. Should we use # something like ldd to check the match? # # For now, we'll default to the first mod_perl.so found. } else { # XXX: can we test whether mod_perl was linked statically # so we don't need to preload it # if (!linked statically) { # die "can't find mod_perl built for perl version $]" # } error "can't find mod_perl.so built for perl version $]"; } # don't use find_apache_module or we may end up with the wrong # shared object, built against different perl } else { # mod_perl 1.0 $vars->{libmodperl} ||= $self->find_apache_module($libname); # XXX: how do we find out whether we have a static or dynamic # mod_perl build? die if its dynamic and can't find the module } my $cfg = ''; if ($vars->{libmodperl} && -e $vars->{libmodperl}) { if (Apache::TestConfig::WIN32) { my $lib = "$Config{installbin}\\$Config{libperl}"; $lib =~ s/lib$/dll/; $cfg = 'LoadFile ' . qq("$lib"\n) if -e $lib; } # add the module we found to the cached modules list # otherwise have_module('mod_perl') doesn't work unless # we have a LoadModule in our base config $self->{modules}->{'mod_perl.c'} = $vars->{libmodperl}; $cfg .= 'LoadModule ' . qq(perl_module "$vars->{libmodperl}"\n); } else { my $msg = "unable to locate $libname (could be a static build)\n"; $cfg = "#$msg"; debug $msg; } $self->preamble(IfModule => '!mod_perl.c', $cfg); } sub configure_inc { my $self = shift; my $top = $self->{vars}->{top_dir}; my $inc = $self->{inc}; for (catdir($top, qw(blib lib)), catdir($top, qw(blib arch))) { if (-d $_) { push @$inc, $_; } } # try ../blib as well for Apache::Reload & Co for (catdir($top, qw(.. blib lib)), catdir($top, qw(.. blib arch))) { push @$inc, $_ if -d $_; } # spec: If PERL5LIB is defined, PERLLIB is not used. for (qw(PERL5LIB PERLLIB)) { next unless exists $ENV{$_}; push @$inc, split /$Config{path_sep}/, $ENV{$_}; last; } # enable live testing of the Apache-Test dev modules if they are # located at the project's root dir my $apache_test_dev_dir = catfile($top, 'Apache-Test', 'lib'); unshift @$inc, $apache_test_dev_dir if -d $apache_test_dev_dir; } sub write_pm_test { my($self, $module, $sub, @base) = @_; my $dir = catfile $self->{vars}->{t_dir}, @base; my $t = catfile $dir, "$sub.t"; return if -e $t; $self->gendir($dir); my $fh = $self->genfile($t); my $path = Apache::TestRequest::module2path($module); print $fh <preamble(IfModule => 'mod_perl.c', [ qw(PerlPassEnv APACHE_TEST_TRACE_LEVEL PerlPassEnv HARNESS_PERL_SWITCHES PerlPassEnv APACHE_TEST_NO_STICKY_PREFERENCES) ]); } sub startup_pl_code { my $self = shift; my $serverroot = $self->{vars}->{serverroot}; my $cover = <<'EOF'; if (($ENV{HARNESS_PERL_SWITCHES}||'') =~ m/Devel::Cover/) { eval { # 0.48 is the first version of Devel::Cover that can # really generate mod_perl coverage statistics require Devel::Cover; Devel::Cover->VERSION(0.48); # this ignores coverage data for some generated files Devel::Cover->import('+inc' => 't/response/',); 1; } or die "Devel::Cover error: $@"; } EOF return <<"EOF"; BEGIN { use lib '$serverroot'; for my \$file (qw(modperl_inc.pl modperl_extra.pl)) { eval { require "conf/\$file" } or die if grep { -e "\$_/conf/\$file" } \@INC; } $cover } 1; EOF } sub configure_startup_pl { my $self = shift; #for 2.0 we could just use PerlSwitches -Mlib=... #but this will work for both 2.0 and 1.xx if (my $inc = $self->{inc}) { my $include_pl = catfile $self->{vars}->{t_conf}, 'modperl_inc.pl'; my $fh = $self->genfile($include_pl); for (reverse @$inc) { next unless $_; print $fh "use lib '$_';\n"; } my $tlib = catdir $self->{vars}->{t_dir}, 'lib'; if (-d $tlib) { print $fh "use lib '$tlib';\n"; } # directory for temp packages which can change during testing # we use require here since a circular dependency exists # between Apache::TestUtil and Apache::TestConfigPerl, so # use does not work here eval { require Apache::TestUtil; }; if ($@) { die "could not require Apache::TestUtil: $@"; } else { print $fh "use lib '" . Apache::TestUtil::_temp_package_dir() . "';\n"; } # if Apache::Test is used to develop a project, we want the # project/lib directory to be first in @INC (loaded last) if ($ENV{APACHE_TEST_LIVE_DEV}) { my $dev_lib = catdir $self->{vars}->{top_dir}, "lib"; print $fh "use lib '$dev_lib';\n" if -d $dev_lib; } print $fh "1;\n"; } if ($self->server->{rev} >= 2) { $self->postamble(IfModule => 'mod_perl.c', "PerlSwitches -Mlib=$self->{vars}->{serverroot}\n"); } my $startup_pl = catfile $self->{vars}->{t_conf}, 'modperl_startup.pl'; unless (-e $startup_pl) { my $fh = $self->genfile($startup_pl); print $fh $self->startup_pl_code; close $fh; } $self->postamble(IfModule => 'mod_perl.c', "PerlRequire $startup_pl\n"); } my %sethandler_modperl = (1 => 'perl-script', 2 => 'modperl'); sub set_handler { my($self, $module, $args) = @_; return if grep { $_ eq 'SetHandler' } @$args; push @$args, SetHandler => $self->server->version_of(\%sethandler_modperl); } sub set_connection_handler { my($self, $module, $args) = @_; my $port = $self->new_vhost($module); my $vars = $self->{vars}; $self->postamble(Listen => '0.0.0.0:' . $port); } my %add_hook_config = ( Response => \&set_handler, ProcessConnection => \&set_connection_handler, PreConnection => \&set_connection_handler, ); my %container_config = ( ProcessConnection => \&vhost_container, PreConnection => \&vhost_container, ); sub location_container { my($self, $module) = @_; my $path = Apache::TestRequest::module2path($module); Location => "/$path"; } sub vhost_container { my($self, $module) = @_; my $port = $self->{vhosts}->{$module}->{port}; my $namebased = $self->{vhosts}->{$module}->{namebased}; VirtualHost => ($namebased ? '*' : '_default_') . ":$port"; } sub new_vhost { my($self, $module, $namebased) = @_; my($port, $servername, $vhost); unless ($namebased and exists $self->{vhosts}->{$module}) { $port = $self->server->select_next_port; $vhost = $self->{vhosts}->{$module} = {}; $vhost->{port} = $port; $vhost->{namebased} = $namebased ? 1 : 0; } else { $vhost = $self->{vhosts}->{$module}; $port = $vhost->{port}; # remember the already configured Listen/NameVirtualHost $vhost->{namebased}++; } $servername = $self->{vars}->{servername}; $vhost->{servername} = $servername; $vhost->{name} = join ':', $servername, $port; $vhost->{hostport} = $self->hostport($vhost, $module); $port; } my %outside_container = map { $_, 1 } qw{ Alias AliasMatch AddType PerlChildInitHandler PerlTransHandler PerlPostReadRequestHandler PerlSwitches PerlRequire PerlModule }; my %strip_tags = map { $_ => 1} qw(base noautoconfig); #test .pm's can have configuration after the __DATA__ token sub add_module_config { my($self, $module, $args) = @_; my $fh = Symbol::gensym(); open($fh, $module) or return; while (<$fh>) { last if /^(__(DATA|END)__|\#if CONFIG_FOR_HTTPD_TEST)/; } my %directives; while (<$fh>) { last if /^\#endif/; #for .c modules next unless /\S+/; chomp; s/^\s+//; $self->replace; if (/^#/) { # preserve comments $self->postamble($_); next; } my($directive, $rest) = split /\s+/, $_, 2; $directives{$directive}++ unless $directive =~ /^postamble($directive => $rest); } elsif ($directive =~ /IfModule/) { $self->postamble($_); } elsif ($directive =~ m/^<(\w+)/) { # strip special container directives like and my $strip_container = exists $strip_tags{lc $1} ? 1 : 0; $directives{noautoconfig}++ if lc($1) eq 'noautoconfig'; my $indent = ''; $self->process_container($_, $fh, lc($1), $strip_container, $indent); } else { push @$args, $directive, $rest; } } \%directives; } # recursively process the directives including nested containers, # re-indent 4 and ucfirst the closing tags letter sub process_container { my($self, $first_line, $fh, $directive, $strip_container, $indent) = @_; my $new_indent = $indent; unless ($strip_container) { $new_indent .= " "; local $_ = $first_line; s/^\s*//; $self->replace; if (/process_vhost_open_tag($_, $indent); } else { $self->postamble($indent . $_); } } $self->process_container_remainder($fh, $directive, $new_indent); unless ($strip_container) { $self->postamble($indent . ""); } } # processes the body of the container without the last line, including # the end tag sub process_container_remainder { my($self, $fh, $directive, $indent) = @_; my $end_tag = ""; while (<$fh>) { chomp; last if m|^\s*\Q$end_tag|i; s/^\s*//; $self->replace; if (m/^\s*<(\w+)/) { $self->process_container($_, $fh, $1, 0, $indent); } else { $self->postamble($indent . $_); } } } # does the necessary processing to create a vhost container header sub process_vhost_open_tag { my($self, $line, $indent) = @_; my $cfg = $self->parse_vhost($line); if ($cfg) { my $port = $cfg->{port}; $cfg->{out_postamble}->(); $self->postamble($cfg->{line}); $cfg->{in_postamble}->(); } else { $self->postamble("$indent$line"); } } #the idea for each group: # Response: there will be many of these, mostly modules to test the API # that plan tests => ... and output with ok() # the naming allows grouping, making it easier to run an # individual set of tests, e.g. t/TEST t/apr # the PerlResponseHandler and SetHandler modperl is auto-configured # Hooks: for testing the simpler Perl*Handlers # auto-generates the Perl*Handler config # Protocol: protocol modules need their own port/vhost to listen on #@INC is auto-modified so each test .pm can be found #modules can add their own configuration using __DATA__ my %hooks = map { $_, ucfirst $_ } qw(init trans headerparser access authen authz type fixup log); $hooks{Protocol} = 'ProcessConnection'; $hooks{Filter} = 'OutputFilter'; my @extra_subdirs = qw(Response Protocol PreConnection Hooks Filter); # add the subdirs to @INC early, in case mod_perl is started earlier sub configure_pm_tests_inc { my $self = shift; for my $subdir (@extra_subdirs) { my $dir = catfile $self->{vars}->{t_dir}, lc $subdir; next unless -d $dir; push @{ $self->{inc} }, $dir; } } # @status fields use constant APACHE_TEST_CONFIGURE => 0; use constant APACHE_TEST_CONFIG_ORDER => 1; sub configure_pm_tests_pick { my($self, $entries) = @_; for my $subdir (@extra_subdirs) { my $dir = catfile $self->{vars}->{t_dir}, lc $subdir; next unless -d $dir; finddepth(sub { return unless /\.pm$/; my $file = catfile $File::Find::dir, $_; my $module = abs2rel $file, $dir; my $status = $self->run_apache_test_config_scan($file); push @$entries, [$file, $module, $subdir, $status]; }, $dir); } } # a simple numerical order is performed and configuration sections are # inserted using that order. If the test package specifies no special # token that matches /APACHE_TEST_CONFIG_ORDER\s+([+-]?\d+)/ anywhere # in the file, 0 is assigned as its order. If the token is specified, # config section with negative values will be inserted first, with # positive last. By using different values you can arrange for the # test configuration sections to be inserted in any desired order sub configure_pm_tests_sort { my($self, $entries) = @_; @$entries = sort { $a->[3]->[APACHE_TEST_CONFIG_ORDER] <=> $b->[3]->[APACHE_TEST_CONFIG_ORDER] } @$entries; } sub configure_pm_tests { my $self = shift; my @entries = (); $self->configure_pm_tests_pick(\@entries); $self->configure_pm_tests_sort(\@entries); for my $entry (@entries) { my ($file, $module, $subdir, $status) = @$entry; my @args = (); my $file_display; { $file_display=$file; my $topdir=$self->{vars}->{top_dir}; $file_display=~s!^\Q$topdir\E(.)(?:\1)*!!; } $self->postamble("\n# included from $file_display"); my $directives = $self->add_module_config($file, \@args); $module =~ s,\.pm$,,; $module =~ s/^[a-z]://i; #strip drive if any $module = join '::', splitdir $module; $self->run_apache_test_configure($file, $module, $status); my @base = map { s/^test//i; $_ } split '::', $module; my $sub = pop @base; my $hook = ($subdir eq 'Hooks' ? $hooks{$sub} : '') || $hooks{$subdir} || $subdir; if ($hook eq 'OutputFilter' and $module =~ /::i\w+$/) { #XXX: tmp hack $hook = 'InputFilter'; } my $handler = join $hook, qw(Perl Handler); if ($self->server->{rev} < 2 and lc($hook) eq 'response') { $handler =~ s/response//i; #s/PerlResponseHandler/PerlHandler/ } debug "configuring $module"; unless ($directives->{noautoconfig}) { if (my $cv = $add_hook_config{$hook}) { $self->$cv($module, \@args); } my $container = $container_config{$hook} || \&location_container; #unless the .pm test already configured the Perl*Handler unless ($directives->{$handler}) { my @handler_cfg = ($handler => $module); if ($outside_container{$handler}) { my $cfg = $self->massage_config_args(@handler_cfg); $self->postamble(IfModule => 'mod_perl.c', $cfg); } else { push @args, @handler_cfg; } } if (@args) { my $cfg = $self->massage_config_args($self->$container($module), \@args); $self->postamble(IfModule => 'mod_perl.c', $cfg); } } $self->postamble("# end of $file_display\n"); $self->write_pm_test($module, lc $sub, map { lc } @base); } } # scan tests for interesting information sub run_apache_test_config_scan { my ($self, $file) = @_; my @status = (); $status[APACHE_TEST_CONFIGURE] = 0; $status[APACHE_TEST_CONFIG_ORDER] = 0; my $fh = Symbol::gensym(); if (open $fh, $file) { local $/; my $content = <$fh>; close $fh; # XXX: optimize to match once? if ($content =~ /APACHE_TEST_CONFIGURE/m) { $status[APACHE_TEST_CONFIGURE] = 1; } if ($content =~ /APACHE_TEST_CONFIG_ORDER\s+([+-]?\d+)/m) { $status[APACHE_TEST_CONFIG_ORDER] = int $1; } } else { error "cannot open $file: $!"; } return \@status; } # We have to test whether tests have APACHE_TEST_CONFIGURE() in them # and run it if found at this stage, so when the server starts # everything is ready. # XXX: however we cannot use a simple require() because some tests # won't require() outside of mod_perl environment. Therefore we scan # the slurped file in. and if APACHE_TEST_CONFIGURE has been found we # require the file and run this function. sub run_apache_test_configure { my ($self, $file, $module, $status) = @_; return unless $status->[APACHE_TEST_CONFIGURE]; eval { require $file }; warn $@ if $@; # double check that it's a real sub if ($module->can('APACHE_TEST_CONFIGURE')) { eval { $module->APACHE_TEST_CONFIGURE($self); }; warn $@ if $@; } } 1;