# Copyright 2001-2004 The Apache Software Foundation
#
# Licensed 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;

use strict;
use warnings FATAL => 'all';

use constant WIN32   => $^O eq 'MSWin32';
use constant OSX     => $^O eq 'darwin';
use constant CYGWIN  => $^O eq 'cygwin';
use constant NETWARE => $^O eq 'NetWare';
use constant SOLARIS => $^O eq 'solaris';
use constant WINFU   => WIN32 || CYGWIN || NETWARE;
use constant COLOR   => ($ENV{APACHE_TEST_COLOR} && -t STDOUT) ? 1 : 0;

use constant DEFAULT_PORT => 8529;

use constant IS_MOD_PERL_2       =>
    eval { require mod_perl && $mod_perl::VERSION >= 1.99 } || 0;

use constant IS_MOD_PERL_2_BUILD => IS_MOD_PERL_2 &&
    require Apache::Build && Apache::Build::IS_MOD_PERL_BUILD();

use constant IS_APACHE_TEST_BUILD =>
    grep { -e "$_/lib/Apache/TestConfig.pm" } qw(Apache-Test . ..);

use constant CUSTOM_CONFIG_FILE => 'Apache/TestConfigData.pm';

use File::Copy ();
use File::Find qw(finddepth);
use File::Basename qw(dirname);
use File::Path ();
use File::Spec::Functions qw(catfile abs2rel splitdir canonpath
                             catdir file_name_is_absolute devnull);
use Cwd qw(fastcwd);
use Socket ();
use Symbol ();

use Apache::TestConfigPerl ();
use Apache::TestConfigParse ();
use Apache::TestTrace;
use Apache::TestServer ();
use Apache::TestRun ();

use vars qw(%Usage);

# variables stored in $Apache::TestConfigData::vars
my @data_vars_must = qw(httpd apxs);
my @data_vars_opt  = qw(user group port);
# mapping from $Apache::TestConfigData::vars to $ENV settings
my %vars_to_env = (
    httpd => 'APACHE_TEST_HTTPD',
    apxs  => 'APACHE_TEST_APXS',
    user  => 'APACHE_TEST_USER',
    group => 'APACHE_TEST_GROUP',
    port  => 'APACHE_TEST_PORT',
);

%Usage = (
   top_dir         => 'top-level directory (default is $PWD)',
   t_dir           => 'the t/ test directory (default is $top_dir/t)',
   t_conf          => 'the conf/ test directory (default is $t_dir/conf)',
   t_logs          => 'the logs/ test directory (default is $t_dir/logs)',
   t_conf_file     => 'test httpd.conf file (default is $t_conf/httpd.conf)',
   src_dir         => 'source directory to look for mod_foos.so',
   serverroot      => 'ServerRoot (default is $t_dir)',
   documentroot    => 'DocumentRoot (default is $ServerRoot/htdocs',
   port            => 'Port [port_number|select] (default ' . DEFAULT_PORT . ')',
   servername      => 'ServerName (default is localhost)',
   user            => 'User to run test server as (default is $USER)',
   group           => 'Group to run test server as (default is $GROUP)',
   bindir          => 'Apache bin/ dir (default is apxs -q BINDIR)',
   sbindir         => 'Apache sbin/ dir (default is apxs -q SBINDIR)',
   httpd           => 'server to use for testing (default is $bindir/httpd)',
   target          => 'name of server binary (default is apxs -q TARGET)',
   apxs            => 'location of apxs (default is from Apache::BuildConfig)',
   startup_timeout => 'seconds to wait for the server to start (default is 60)',
   httpd_conf      => 'inherit config from this file (default is apxs derived)',
   httpd_conf_extra=> 'inherit additional config from this file',
   minclients      => 'minimum number of concurrent clients (default is 1)',
   maxclients      => 'maximum number of concurrent clients (default is minclients+1)',
   perlpod         => 'location of perl pod documents (for testing downloads)',
   proxyssl_url    => 'url for testing ProxyPass / https (default is localhost)',
   sslca           => 'location of SSL CA (default is $t_conf/ssl/ca)',
   sslcaorg        => 'SSL CA organization to use for tests (default is asf)',
   libmodperl      => 'path to mod_perl\'s .so (full or relative to LIBEXECDIR)',
   defines         => 'values to add as -D defines (for example, "VAR1 VAR2")',
   (map { $_ . '_module_name', "$_ module name"} qw(cgi ssl thread access auth php)),
);

my %filepath_conf_opts = map { $_ => 1 }
    qw(top_dir t_dir t_conf t_logs t_conf_file src_dir serverroot
       documentroot bindir sbindir httpd apxs httpd_conf httpd_conf_extra
       perlpod sslca libmodperl);

sub conf_opt_is_a_filepath {
    my $opt = shift;
    $opt && exists $filepath_conf_opts{$opt};
}

sub usage {
    for my $hash (\%Usage) {
        for (sort keys %$hash){
            printf "  -%-18s %s\n", $_, $hash->{$_};
        }
    }
}

sub filter_args {
    my($args, $wanted_args) = @_;
    my(@pass, %keep);

    my @filter = @$args;

    if (ref($filter[0])) {
        push @pass, shift @filter;
    }

    while (@filter) {
        my $key = shift @filter;
        # optinal - or -- prefix
        if (defined $key && $key =~ /^-?-?(.+)/ && exists $wanted_args->{$1}) {
            if (@filter) {
                $keep{$1} = shift @filter;
            }
            else {
                die "key $1 requires a matching value";
            }
        }
        else {
            push @pass, $key;
        }
    }

    return (\@pass, \%keep);
}

my %passenv = map { $_,1 } qw{
    APACHE_TEST_APXS
    APACHE_TEST_HTTPD
    APACHE_TEST_GROUP
    APACHE_TEST_USER
    APACHE_TEST_PORT
};

sub passenv {
    \%passenv;
}

sub passenv_makestr {
    my @vars;

    for (keys %passenv) {
        push @vars, "$_=\$($_)";
    }

    "@vars";
}

sub server { shift->{server} }

sub modperl_2_inc_fixup {
    (IS_MOD_PERL_2 && !IS_MOD_PERL_2_BUILD) ? "use Apache2;\n" : '';
}

sub modperl_build_config {
    eval {
        require Apache::Build;
    } or return undef;
    return Apache::Build->build_config;
}

sub new_test_server {
    my($self, $args) = @_;
    Apache::TestServer->new($args || $self)
}

# setup httpd-independent components
# for httpd-specific call $self->httpd_config()
sub new {
    my $class = shift;

    my $args;

    $args = shift if $_[0] and ref $_[0];

    $args = $args ? {%$args} : {@_}; #copy

    #see Apache::TestMM::{filter_args,generate_script}
    #we do this so 'perl Makefile.PL' can be passed options such as apxs
    #without forcing regeneration of configuration and recompilation of c-modules
    #as 't/TEST apxs /path/to/apache/bin/apxs' would do
    while (my($key, $val) = each %Apache::TestConfig::Argv) {
        $args->{$key} = $val;
    }

    my $thaw = {};

    #thaw current config
    for (qw(conf t/conf)) {
        last if eval {
            require "$_/apache_test_config.pm";
            $thaw = 'apache_test_config'->new;
            delete $thaw->{save};
            #incase class that generated the config was
            #something else, which we can't be sure how to load
            bless $thaw, 'Apache::TestConfig';
        };
    };

    if ($args->{thaw} and ref($thaw) ne 'HASH') {
        #dont generate any new config
        $thaw->{vars}->{$_} = $args->{$_} for keys %$args;
        $thaw->{server} = $thaw->new_test_server;
        $thaw->add_inc;
        return $thaw;
    }

    #regenerating config, so forget old
    if ($args->{save}) {
        for (qw(vhosts inherit_config modules inc cmodules)) {
            delete $thaw->{$_} if exists $thaw->{$_};
        }
    }

    # custom config options from Apache::TestConfigData
    # again, this should force reconfiguration
    custom_config_add_conf_opts($args);

    my $self = bless {
        clean => {},
        vhosts => {},
        inherit_config => {},
        modules => {},
        inc => [],
        %$thaw,
        mpm => "",
        httpd_defines => {},
        vars => $args,
        postamble => [],
        preamble => [],
        postamble_hooks => [],
        preamble_hooks => [],
    }, ref($class) || $class;

    my $vars = $self->{vars}; #things that can be overridden

    for (qw(save verbose)) {
        next unless exists $args->{$_};
        $self->{$_} = delete $args->{$_};
    }

    $vars->{top_dir} ||= fastcwd;
    $vars->{top_dir} = pop_dir($vars->{top_dir}, 't');

    $self->add_inc;

    #help to find libmodperl.so
    my $src_dir = catfile $vars->{top_dir}, qw(src modules perl);
    $vars->{src_dir}      ||= $src_dir if -d $src_dir;

    $vars->{t_dir}        ||= catfile $vars->{top_dir}, 't';
    $vars->{serverroot}   ||= $vars->{t_dir};
    $vars->{documentroot} ||= catfile $vars->{serverroot}, 'htdocs';
    $vars->{perlpod}      ||= $self->find_in_inc('pods') ||
                              $self->find_in_inc('pod');
    $vars->{perl}         ||= $^X;
    $vars->{t_conf}       ||= catfile $vars->{serverroot}, 'conf';
    $vars->{sslca}        ||= catfile $vars->{t_conf}, 'ssl', 'ca';
    $vars->{sslcaorg}     ||= 'asf';
    $vars->{t_logs}       ||= catfile $vars->{serverroot}, 'logs';
    $vars->{t_conf_file}  ||= catfile $vars->{t_conf},   'httpd.conf';

    if (WINFU) {
        for (keys %$vars) {
            $vars->{$_} =~ s|\\|\/|g if defined $vars->{$_};
        }
    }

    $vars->{scheme}       ||= 'http';
    $vars->{servername}   ||= $self->default_servername;
    $vars->{port}           = $self->select_first_port;
    $vars->{remote_addr}  ||= $self->our_remote_addr;

    $vars->{user}         ||= $self->default_user;
    $vars->{group}        ||= $self->default_group;
    $vars->{serveradmin}  ||= $self->default_serveradmin;

    $vars->{minclients}   ||= 1;
    $vars->{maxclients_preset} = $vars->{maxclients} || 0;
    # if maxclients wasn't explicitly passed try to
    # prevent 'server reached MaxClients setting' errors
    $vars->{maxclients}   ||= $vars->{minclients} + 1;

    # if a preset maxclients valus is smaller than minclients,
    # maxclients overrides minclients
    if ($vars->{maxclients_preset} &&
        $vars->{maxclients_preset} < $vars->{minclients}) {
        $vars->{minclients} = $vars->{maxclients_preset};
    }

    # for threaded mpms MaxClients must be a multiple of
    # ThreadsPerChild (i.e. maxclients % minclients == 0)
    # so unless -maxclients was explicitly specified use a double of
    # minclients
    $vars->{maxclientsthreadedmpm} =
        $vars->{maxclients_preset} || $vars->{minclients} * 2;

    $vars->{proxy}        ||= 'off';
    $vars->{proxyssl_url} ||= '';
    $vars->{defines}      ||= '';

    $self->{hostport} = $self->hostport;
    $self->{server} = $self->new_test_server;

    return $self;

}

# figure out where httpd is and run extra config hooks which require
# knowledge of where httpd is
sub httpd_config {
    my $self = shift;

    $self->configure_apxs;
    $self->configure_httpd;

    my $vars = $self->{vars};
    unless ($vars->{httpd} or $vars->{apxs}) {

        # mod_perl 2.0 build always knows the right httpd location
        # (and optionally apxs)
        if (IS_MOD_PERL_2_BUILD) {
            # XXX: at the moment not sure what could go wrong, but it
            # shouldn't enter interactive config, which doesn't work
            # with mod_perl 2.0 build (by design)
            die "something is wrong, mod_perl 2.0 build should have " .
                "supplied all the needed information to run the tests. " .
                "Please post lib/Apache/BuildConfig.pm along with the " .
                "bug report";
        }

        if ($ENV{APACHE_TEST_NO_STICKY_PREFERENCES}) {
            error "You specified APACHE_TEST_NO_STICKY_PREFERENCES=1 " .
                "in which case you must explicitly specify -httpd " .
                "and/or -apxs options";
            Apache::TestRun::exit_perl(0);
        }

        $self->clean(1);
        # this method restarts the whole program via exec
        # so it never returns
        $self->custom_config_first_time($self->{vars});
    }

    # if we have gotten that far we know at least about the location
    # of httpd and or apxs, so let's save it if we haven't saved any
    # custom configs yet
    unless (custom_config_exists()) {
        $self->custom_config_save($self->{vars});
    }

    $self->inherit_config; #see TestConfigParse.pm
    $self->configure_httpd_eapi; #must come after inherit_config

    $self->default_module(cgi    => [qw(mod_cgi mod_cgid)]);
    $self->default_module(thread => [qw(worker threaded)]);
    $self->default_module(ssl    => [qw(mod_ssl)]);
    $self->default_module(access => [qw(mod_access mod_authz_host)]);
    $self->default_module(auth   => [qw(mod_auth mod_auth_basic)]);
    $self->default_module(php    => [qw(sapi_apache2 mod_php4 mod_php5)]);

    $self->{server}->post_config;

    $self;
}

sub default_module {
    my($self, $name, $choices) = @_;

    my $mname = $name . '_module_name';

    unless ($self->{vars}->{$mname}) {
        ($self->{vars}->{$mname}) = grep {
            $self->{modules}->{"$_.c"};
        } @$choices;

        $self->{vars}->{$mname} ||= $choices->[0];
    }

    $self->{vars}->{$name . '_module'} =
      $self->{vars}->{$mname} . '.c'
}

sub configure_apxs {
    my $self = shift;

    $self->{APXS} = $self->default_apxs;

    return unless $self->{APXS};

    $self->{APXS} =~ s{/}{\\}g if WIN32;

    my $vars = $self->{vars};

    $vars->{bindir}   ||= $self->apxs('BINDIR', 1);
    $vars->{sbindir}  ||= $self->apxs('SBINDIR');
    $vars->{target}   ||= $self->apxs('TARGET');
    $vars->{conf_dir} ||= $self->apxs('SYSCONFDIR');

    if ($vars->{conf_dir}) {
        $vars->{httpd_conf} ||= catfile $vars->{conf_dir}, 'httpd.conf';
    }
}

sub configure_httpd {
    my $self = shift;
    my $vars = $self->{vars};

    debug "configuring httpd";

    $vars->{target} ||= (WIN32 ? 'Apache.EXE' : 'httpd');

    unless ($vars->{httpd}) {
        #sbindir should be bin/ with the default layout
        #but its eaiser to workaround apxs than fix apxs
        for my $dir (map { $vars->{$_} } qw(sbindir bindir)) {
            next unless defined $dir;
            my $httpd = catfile $dir, $vars->{target};
            next unless -x $httpd;
            $vars->{httpd} = $httpd;
            last;
        }

        $vars->{httpd} ||= $self->default_httpd;
    }

    if ($vars->{httpd}) {
        my @chunks = splitdir $vars->{httpd};
        #handle both $prefix/bin/httpd and $prefix/Apache.exe
        for (1,2) {
            pop @chunks;
            last unless @chunks;
            $self->{httpd_basedir} = catfile @chunks;
            last if -d "$self->{httpd_basedir}/bin";
        }
    }

    #cleanup httpd droppings
    my $sem = catfile $vars->{t_logs}, 'apache_runtime_status.sem';
    unless (-e $sem) {
        $self->clean_add_file($sem);
    }
}

sub configure_httpd_eapi {
    my $self = shift;
    my $vars = $self->{vars};

    #deal with EAPI_MM_CORE_PATH if defined.
    if (defined($self->{httpd_defines}->{EAPI_MM_CORE_PATH})) {
        my $path = $self->{httpd_defines}->{EAPI_MM_CORE_PATH};

        #ensure the directory exists
        my @chunks = splitdir $path;
        pop @chunks; #the file component of the path
        $path = catdir @chunks;
        unless (file_name_is_absolute $path) {
            $path = catdir $vars->{serverroot}, $path;
        }
        $self->gendir($path);
    }
}

sub configure_proxy {
    my $self = shift;
    my $vars = $self->{vars};

    #if we proxy to ourselves, must bump the maxclients
    if ($vars->{proxy} =~ /^on$/i) {
        unless ($vars->{maxclients_preset}) {
            $vars->{minclients}++;
            $vars->{maxclients}++;
        }
        $vars->{proxy} = $self->{vhosts}->{'mod_proxy'}->{hostport};
        return $vars->{proxy};
    }

    return undef;
}

# adds the config to the head of the group instead of the tail
# XXX: would be even better to add to a different sub-group
# (e.g. preamble_first) of only those that want to be first and then,
# make sure that they are dumped to the config file first in the same
# group (e.g. preamble)
sub add_config_first {
    my $self = shift;
    my $where = shift;
    unshift @{ $self->{$where} }, $self->massage_config_args(@_);
}

sub add_config_last {
    my $self = shift;
    my $where = shift;
    push @{ $self->{$where} }, $self->massage_config_args(@_);
}

sub massage_config_args {
    my $self = shift;
    my($directive, $arg, $data) = @_;
    my $args = "";

    if ($data) {
        $args = "<$directive $arg>\n";
        if (ref($data) eq 'HASH') {
            while (my($k,$v) = each %$data) {
                $args .= "    $k $v\n";
            }
        }
        elsif (ref($data) eq 'ARRAY') {
            # balanced (key=>val) list
            my $pairs = @$data / 2;
            for my $i (0..($pairs-1)) {
                $args .= sprintf "    %s %s\n", $data->[$i*2], $data->[$i*2+1];
            }
        }
        else {
            $args .= "    $data";
        }
        $args .= "</$directive>\n";
    }
    elsif (ref($directive) eq 'ARRAY') {
        $args = join "\n", @$directive;
    }
    else {
        $args = join " ", grep length($_), $directive,
          (ref($arg) && (ref($arg) eq 'ARRAY') ? "@$arg" : $arg || "");
    }

    return $args;
}

sub postamble_first {
    shift->add_config_first(postamble => @_);
}

sub postamble {
    shift->add_config_last(postamble => @_);
}

sub preamble_first {
    shift->add_config_first(preamble => @_);
}

sub preamble {
    shift->add_config_last(preamble => @_);
}

sub postamble_register {
    push @{ shift->{postamble_hooks} }, @_;
}

sub preamble_register {
    push @{ shift->{preamble_hooks} }, @_;
}

sub add_config_hooks_run {
    my($self, $where, $out) = @_;

    for (@{ $self->{"${where}_hooks"} }) {
        if ((ref($_) and ref($_) eq 'CODE') or $self->can($_)) {
            $self->$_();
        }
        else {
            error "cannot run configure hook: `$_'";
        }
    }

    for (@{ $self->{$where} }) {
        $self->replace;
        print $out "$_\n";
    }
}

sub postamble_run {
    shift->add_config_hooks_run(postamble => @_);
}

sub preamble_run {
    shift->add_config_hooks_run(preamble => @_);
}

sub default_group {
    return if WINFU;

    my $gid = $);

    #use only first value if $) contains more than one
    $gid =~ s/^(\d+).*$/$1/;

    my $group = $ENV{APACHE_TEST_GROUP} || (getgrgid($gid) || "#$gid");

    if ($group eq 'root') {
        # similar to default_user, we want to avoid perms problems,
        # when the server is started with group 'root'. When running
        # under group root it may fail to create dirs and files,
        # writable only by user
        my $user = default_user();
        my $gid = $user ? (getpwnam($user))[3] : '';
        $group = (getgrgid($gid) || "#$gid") if $gid;
    }

    $group;
}

sub default_user {
    return if WINFU;

    my $uid = $>;

    my $user = $ENV{APACHE_TEST_USER} || (getpwuid($uid) || "#$uid");

    if ($user eq 'root') {
        my $other = (getpwnam('nobody'))[0];
        if ($other) {
            $user = $other;
        }
        else {
            die "cannot run tests as User root";
            #XXX: prompt for another username
        }
    }

    $user;
}

sub default_serveradmin {
    my $vars = shift->{vars};
    join '@', ($vars->{user} || 'unknown'), $vars->{servername};
}

sub default_apxs {
    my $self = shift;

    return $self->{vars}->{apxs} if $self->{vars}->{apxs};

    if (my $build_config = modperl_build_config()) {
        return $build_config->{MP_APXS};
    }

    $ENV{APACHE_TEST_APXS};
}

sub default_httpd {
    my $vars = shift->{vars};

    if (my $build_config = modperl_build_config()) {
        if (my $p = $build_config->{MP_AP_PREFIX}) {
            for my $bindir (qw(bin sbin)) {
                my $httpd = catfile $p, $bindir, $vars->{target};
                return $httpd if -e $httpd;
            }
        }
    }

    $ENV{APACHE_TEST_HTTPD};
}

my $localhost;

sub default_localhost {
    my $localhost_addr = pack('C4', 127, 0, 0, 1);
    gethostbyaddr($localhost_addr, Socket::AF_INET()) || 'localhost';
}

sub default_servername {
    my $self = shift;
    $localhost ||= $self->default_localhost;
    die "Can't figure out the default localhost's server name"
        unless $localhost;
}

# memoize the selected value (so we make sure that the same port is used
# via select). The problem is that select_first_port() is called 3 times after
# -clean, and it's possible that a lower port will get released
# between calls, leading to various places in the test suite getting a
# different base port selection.
#
# XXX: There is still a problem if two t/TEST's configure at the same
# time, so they both see the same port free, but only the first one to
# bind() will actually get the port. So there is a need in another
# check and reconfiguration just before the server starts.
#
my $port_memoized;
sub select_first_port {
    my $self = shift;

    my $port ||= $port_memoized || $ENV{APACHE_TEST_PORT} 
        || $self->{vars}{port} || DEFAULT_PORT;

    # memoize
    $port_memoized = $port;

    return $port unless $port eq 'select';

    # port select mode: try to find another available port, take into
    # account that each instance of the test suite may use more than
    # one port for virtual hosts, therefore try to check ports in big
    # steps (20?).
    my $step  = 20;
    my $tries = 20;
    $port = DEFAULT_PORT;
    until (Apache::TestServer->port_available($port)) {
        unless (--$tries) {
            error "no ports available";
            error "tried ports @{[DEFAULT_PORT]} - $port in $step increments";
            return 0;
        }
        $port += $step;
    }

    info "the default base port is used, using base port $port instead"
        unless $port == DEFAULT_PORT;

    # memoize
    $port_memoized = $port;

    return $port;
}

my $remote_addr;

sub our_remote_addr {
    my $self = shift;
    my $name = $self->default_servername;
    my $iaddr = (gethostbyname($name))[-1];
    unless (defined $iaddr) {
        error "Can't resolve host: '$name' (check /etc/hosts)";
        exit 1;
    }
    $remote_addr ||= Socket::inet_ntoa($iaddr);
}

sub default_loopback {
    '127.0.0.1';
}

sub port {
    my($self, $module) = @_;

    unless ($module) {
        my $vars = $self->{vars};
        return $self->select_first_port() unless $vars->{scheme} eq 'https';
        $module = $vars->{ssl_module_name};
    }
    return $self->{vhosts}->{$module}->{port};
}

sub hostport {
    my $self = shift;
    my $vars = shift || $self->{vars};
    my $module = shift || '';

    my $name = $vars->{servername};

    join ':', $name , $self->port($module || '');
}

#look for mod_foo.so
sub find_apache_module {
    my($self, $module) = @_;

    die "find_apache_module: module name argument is required"
        unless $module;

    my $vars = $self->{vars};
    my $sroot = $vars->{serverroot};

    my @trys = grep { $_ }
      ($vars->{src_dir},
       $self->apxs('LIBEXECDIR'),
       catfile($sroot, 'modules'),
       catfile($sroot, 'libexec'));

    for (@trys) {
        my $file = catfile $_, $module;
        if (-e $file) {
            debug "found $module => $file";
            return $file;
        }
    }

    # if the module wasn't found try to lookup in the list of modules
    # inherited from the system-wide httpd.conf
    my $name = $module;
    $name =~ s/\.s[ol]$/.c/;  #mod_info.so => mod_info.c
    $name =~ s/^lib/mod_/; #libphp4.so => mod_php4.c
    return $self->{modules}->{$name} if $self->{modules}->{$name};

}

#generate files and directories

my %warn_style = (
    html    => sub { "<!-- @_ -->" },
    c       => sub { "/* @_ */" },
    default => sub { join '', grep {s/^/\# /gm} @_ },
);

my %file_ext = (
    map({$_ => 'html'} qw(htm html)),
    map({$_ => 'c'   } qw(c h)),
);

# return the passed file's extension or '' if there is no one
# note: that '/foo/bar.conf.in' returns an extension: 'conf.in';
# note: a hidden file .foo will be recognized as an extension 'foo'
sub filename_ext {
    my ($self, $filename) = @_;
    my $ext = (File::Basename::fileparse($filename, '\..*'))[2] || '';
    $ext =~ s/^\.(.*)/lc $1/e;
    $ext;
}

sub warn_style_sub_ref {
    my ($self, $filename) = @_;
    my $ext = $self->filename_ext($filename);
    return $warn_style{ $file_ext{$ext} || 'default' };
}

sub genwarning {
    my($self, $filename, $from_filename) = @_;
    return unless $filename;
    my $warning = "WARNING: this file is generated";
    $warning .= " (from $from_filename)" if defined $from_filename;
    $warning .= ", do not edit\n";
    $warning .= calls_trace();
    return $self->warn_style_sub_ref($filename)->($warning);
}

sub calls_trace {
    my $frame = 1;
    my $trace = '';

    while (1) {
        my($package, $filename, $line) = caller($frame);
        last unless $filename;
        $trace .= sprintf "%02d: %s:%d\n", $frame, $filename, $line;
        $frame++;
    }

    return $trace;
}

sub clean_add_file {
    my($self, $file) = @_;

    $self->{clean}->{files}->{ rel2abs($file) } = 1;
}

sub clean_add_path {
    my($self, $path) = @_;

    $path = rel2abs($path);

    # remember which dirs were created and should be cleaned up
    while (1) {
        $self->{clean}->{dirs}->{$path} = 1;
        $path = dirname $path;
        last if -e $path;
    }
}

sub genfile_trace {
    my($self, $file, $from_file) = @_;
    my $name = abs2rel $file, $self->{vars}->{t_dir};
    my $msg = "generating $name";
    $msg .= " from $from_file" if defined $from_file;
    debug $msg;
}

sub genfile_warning {
    my($self, $file, $from_file, $fh) = @_;

    if (my $msg = $self->genwarning($file, $from_file)) {
        print $fh $msg, "\n";
    }
}

# $from_file == undef if there was no templates used
sub genfile {
    my($self, $file, $from_file, $nowarning) = @_;

    # create the parent dir if it doesn't exist yet
    my $dir = dirname $file;
    $self->makepath($dir);

    $self->genfile_trace($file, $from_file);

    my $fh = Symbol::gensym();
    open $fh, ">$file" or die "open $file: $!";

    $self->genfile_warning($file, $from_file, $fh) unless $nowarning;

    $self->clean_add_file($file);

    return $fh;
}

# gen + write file
sub writefile {
    my($self, $file, $content, $nowarning) = @_;

    my $fh = $self->genfile($file, undef, $nowarning);

    print $fh $content if $content;

    close $fh;
}

sub perlscript_header {

    require FindBin;

    my @dirs = ();

    # mp2 needs its modper-2.0/lib before blib was created
    if (IS_MOD_PERL_2_BUILD || $ENV{APACHE_TEST_LIVE_DEV}) {
        # the live 'lib/' dir of the distro
        # (e.g. modperl-2.0/ModPerl-Registry/lib)
        my $dir = canonpath catdir $FindBin::Bin, "lib";
        push @dirs, $dir if -d $dir;

        # the live dir of the top dir if any  (e.g. modperl-2.0/lib)
        if (-e catfile($FindBin::Bin, "..", "Makefile.PL")) {
            my $dir = canonpath catdir $FindBin::Bin, "..", "lib";
            push @dirs, $dir if -d $dir;
        }
    }

    for (qw(. ..)) {
        my $dir = canonpath catdir $FindBin::Bin, $_ , "Apache-Test", "lib";
        if (-d $dir) {
            push @dirs, $dir;
            last;
        }
    }

    {
        my $dir = canonpath catdir $FindBin::Bin, "t", "lib";
        push @dirs, $dir if -d $dir;
    }

    my $dirs = join("\n    ", '', @dirs) . "\n";;

    return <<"EOF";

use strict;
use warnings FATAL => 'all';

use lib qw($dirs);

EOF
}

# gen + write executable perl script file
sub write_perlscript {
    my($self, $file, $content) = @_;

    my $fh = $self->genfile($file, undef, 1);

    # shebang
    print $fh "#!$Config{perlpath}\n";

    $self->genfile_warning($file, undef, $fh);

    print $fh $content if $content;

    close $fh;
    chmod 0755, $file;
}

sub cpfile {
    my($self, $from, $to) = @_;
    File::Copy::copy($from, $to);
    $self->clean_add_file($to);
}

sub symlink {
    my($self, $from, $to) = @_;
    CORE::symlink($from, $to);
    $self->clean_add_file($to);
}

sub gendir {
    my($self, $dir) = @_;
    $self->makepath($dir);
}

# returns a list of dirs successfully created
sub makepath {
    my($self, $path) = @_;

    return if !defined($path) || -e $path;

    $self->clean_add_path($path);

    return File::Path::mkpath($path, 0, 0755);
}

sub open_cmd {
    my($self, $cmd) = @_;
    # untaint some %ENV fields
    local @ENV{ qw(IFS CDPATH ENV BASH_ENV) };

    # Temporarly untaint PATH
    (local $ENV{PATH}) = ( $ENV{PATH} =~ /(.*)/ );
    # -T disallows relative directories in the PATH
    $ENV{PATH} = join ':', grep !/^\./, split /:/, $ENV{PATH};

    my $handle = Symbol::gensym();
    open $handle, "$cmd|" or die "$cmd failed: $!";

    return $handle;
}

sub clean {
    my $self = shift;
    $self->{clean_level} = shift || 2; #2 == really clean, 1 == reconfigure

    $self->new_test_server->clean;
    $self->cmodules_clean;
    $self->sslca_clean;

    for (keys %{ $self->{clean}->{files} }) {
        if (-e $_) {
            debug "unlink $_";
            unlink $_;
        }
        else {
            debug "unlink $_: $!";
        }
    }

    # if /foo comes before /foo/bar, /foo will never be removed
    # hence ensure that sub-dirs are always treated before a parent dir
    for (reverse sort keys %{ $self->{clean}->{dirs} }) {
        if (-d $_) {
            my $dh = Symbol::gensym();
            opendir($dh, $_);
            my $notempty = grep { ! /^\.{1,2}$/ } readdir $dh;
            closedir $dh;
            next if $notempty;
            debug "rmdir $_";
            rmdir $_;
        }
    }
}

sub replace {
    my $self = shift;
    my $file = $Apache::TestConfig::File
        ? "in file $Apache::TestConfig::File" : '';

    s[@(\w+)@]
     [ my $key = lc $1;
      exists $self->{vars}->{$key}
      ? $self->{vars}->{$key}
      : die "invalid token: \@$1\@ $file\n";
     ]ge;
}

#need to configure the vhost port for redirects and $ENV{SERVER_PORT}
#to have the correct values
my %servername_config = (
    0 => sub {
        my($name, $port) = @_;
        [ServerName => ''], [Port => 0];
    },
    1 => sub {
        my($name, $port) = @_;
        [ServerName => $name], [Port => $port];
    },
    2 => sub {
        my($name, $port) = @_;
        [ServerName => "$name:$port"];
    },
);

sub servername_config {
    my $self = shift;
    $self->server->version_of(\%servername_config)->(@_);
}

sub parse_vhost {
    my($self, $line) = @_;

    my($indent, $module, $namebased);
    if ($line =~ /^(\s*)<VirtualHost\s+(?:_default_:|([^:]+):(?!:))?(.*?)\s*>\s*$/) {
        $indent    = $1 || "";
        $namebased = $2 || "";
        $module    = $3;
    }
    else {
        return undef;
    }

    my $vars = $self->{vars};
    my $mods = $self->{modules};
    my $have_module = "$module.c";
    my $ssl_module = $vars->{ssl_module};

    #if module ends with _ssl and it is not the module that implements ssl,
    #then assume this module is a vhost with SSLEngine On (or similar)
    #see mod_echo in extra.conf.in for example
    if ($module =~ /^(mod_\w+)_ssl$/ and $have_module ne $ssl_module) {
        $have_module = "$1.c"; #e.g. s/mod_echo_ssl.c/mod_echo.c/
        return undef unless $mods->{$ssl_module};
    }

    #don't allocate a port if this module is not configured
    #assumes the configuration is inside an <IfModule $have_module>
    if ($module =~ /^mod_/ and not $mods->{$have_module}) {
        return undef;
    }

    #allocate a port and configure this module into $self->{vhosts}
    my $port = $self->new_vhost($module, $namebased);

    #extra config that should go *inside* the <VirtualHost ...>
    my @in_config = $self->servername_config($namebased
                                                 ? $namebased
                                                 : $vars->{servername},
                                             $port);

    my @out_config = ();
    if ($self->{vhosts}->{$module}->{namebased} < 2) {
        #extra config that should go *outside* the <VirtualHost ...>
        @out_config = ([Listen => '0.0.0.0:' . $port]);

        if ($self->{vhosts}->{$module}->{namebased}) {
            push @out_config => [NameVirtualHost => "*:$port"];
        }
    }

    #there are two ways of building a vhost
    #first is when we parse test .pm and .c files
    #second is when we scan *.conf.in
    my $form_postamble = sub {
        my $indent = shift;
        for my $pair (@_) {
            $self->postamble("$indent@$pair");
        }
    };

    my $form_string = sub {
        my $indent = shift;
        join "\n", map { "$indent@$_\n" } @_;
    };

    my $double_indent = $indent ? $indent x 2 : ' ' x 4;
    return {
        port          => $port,
        #used when parsing .pm and .c test modules
        in_postamble  => sub { $form_postamble->($double_indent, @in_config) },
        out_postamble => sub { $form_postamble->($indent, @out_config) },
        #used when parsing *.conf.in files
        in_string     => $form_string->($double_indent, @in_config),
        out_string    => $form_string->($indent, @out_config),
        line          => "$indent<VirtualHost " . ($namebased ? '*' : '_default_') .
                         ":$port>",
    };
}

sub replace_vhost_modules {
    my $self = shift;

    if (my $cfg = $self->parse_vhost($_)) {
        $_ = '';
        for my $key (qw(out_string line in_string)) {
            next unless $cfg->{$key};
            $_ .= "$cfg->{$key}\n";
        }
    }
}

sub replace_vars {
    my($self, $in, $out) = @_;

    local $_;
    while (<$in>) {
        $self->replace;
        $self->replace_vhost_modules;
        print $out $_;
    }
}

sub index_html_template {
    my $self = shift;
    return "welcome to $self->{server}->{name}\n";
}

sub generate_index_html {
    my $self = shift;
    my $dir = $self->{vars}->{documentroot};
    $self->gendir($dir);
    my $file = catfile $dir, 'index.html';
    return if -e $file;
    my $fh = $self->genfile($file);
    print $fh $self->index_html_template;
}

sub types_config_template {
    return <<EOF;
text/html  html htm
image/gif  gif
image/jpeg jpeg jpg jpe
image/png  png
text/plain asc txt
EOF
}

sub generate_types_config {
    my $self = shift;

    # handle the case when mod_mime is built as a shared object
    # but wasn't included in the system-wide httpd.conf
    my $mod_mime = $self->find_apache_module('mod_mime.so');
    if ($mod_mime && -e $mod_mime) {
        $self->preamble(IfModule => '!mod_mime.c',
                        qq{LoadModule mime_module "$mod_mime"\n});
    }

    unless ($self->{inherit_config}->{TypesConfig}) {
        my $types = catfile $self->{vars}->{t_conf}, 'mime.types';
        unless (-e $types) {
            my $fh = $self->genfile($types);
            print $fh $self->types_config_template;
            close $fh;
        }
        $self->postamble(TypesConfig => qq("$types"));
    }
}

# various dup bugs in older perl and perlio in perl < 5.8.4 need a
# workaround to explicitly rewind the dupped DATA fh before using it
my $DATA_pos = tell DATA;
sub httpd_conf_template {
    my($self, $try) = @_;

    my $in = Symbol::gensym();
    if (open $in, $try) {
        return $in;
    }
    else {
        my $dup = Symbol::gensym();
        open $dup, "<&DATA" or die "Can't dup DATA: $!";
        seek $dup, $DATA_pos, 0; # rewind to the beginning
        return $dup; # so we don't close DATA
    }
}

#certain variables may not be available until certain config files
#are generated.  for example, we don't know the ssl port until ssl.conf.in
#is parsed.  ssl port is needed for proxyssl testing

sub check_vars {
    my $self = shift;
    my $vars = $self->{vars};

    unless ($vars->{proxyssl_url}) {
        my $ssl = $self->{vhosts}->{ $vars->{ssl_module_name} };
        if ($ssl) {
            $vars->{proxyssl_url} ||= $ssl->{hostport};
        }

        if ($vars->{proxyssl_url}) {
            unless ($vars->{maxclients_preset}) {
                $vars->{minclients}++;
                $vars->{maxclients}++;
            }
        }
    }
}

sub extra_conf_files_needing_update {
    my $self = shift;

    my @need_update = ();
    finddepth(sub {
        return unless /\.in$/;
        (my $generated = $File::Find::name) =~ s/\.in$//;
        push @need_update, $generated 
            unless -e $generated && -M $generated < -M $File::Find::name;
    }, $self->{vars}->{t_conf});

    return @need_update;
}

sub generate_extra_conf {
    my $self = shift;

    my(@extra_conf, @conf_in, @conf_files);

    finddepth(sub {
        return unless /\.in$/;
        push @conf_in, catdir $File::Find::dir, $_;
    }, $self->{vars}->{t_conf});

    #make ssl port always be 8530 when available
    for my $file (@conf_in) {
        if (basename($file) =~ /^ssl/) {
            unshift @conf_files, $file;
        }
        else {
            push @conf_files, $file;
        }
    }

    for my $file (@conf_files) {
        (my $generated = $file) =~ s/\.in$//;
        debug "Will 'Include' $generated config file";
        push @extra_conf, $generated;
    }

    # if at least one .in file was modified or the derivative is
    # missing, regenerate them all (so information like assigned port
    # numbers will be correct)
    if ($self->extra_conf_files_needing_update) {
        for my $file (@conf_files) {
            local $Apache::TestConfig::File = $file;

            my $in = Symbol::gensym();
            open($in, $file) or next;

            (my $generated = $file) =~ s/\.in$//;
            my $out = $self->genfile($generated, $file);
            $self->replace_vars($in, $out);

            close $in;
            close $out;

            $self->check_vars;
        }
    }

    #we changed order to give ssl the first port after DEFAULT_PORT
    #but we want extra.conf Included first so vhosts inherit base config
    #such as LimitRequest*
    return [ sort @extra_conf ];
}

sub sslca_can {
    my($self, $check) = @_;

    my $vars = $self->{vars};
    return 0 unless $self->{modules}->{ $vars->{ssl_module} };
    return 0 unless -d "$vars->{t_conf}/ssl";

    require Apache::TestSSLCA;

    if ($check) {
        my $openssl = Apache::TestSSLCA::openssl();
        if (which($openssl)) {
            return 1;
        }

        error "cannot locate '$openssl' program required to generate SSL CA";
        exit(1);
    }

    return 1;
}

sub sslca_generate {
    my $self = shift;

    my $ca = $self->{vars}->{sslca};
    return if $ca and -d $ca; #t/conf/ssl/ca

    return unless $self->sslca_can(1);

    Apache::TestSSLCA::generate($self);
}

sub sslca_clean {
    my $self = shift;

    # XXX: httpd config is required, for now just skip ssl clean if
    # there is none. should probably add some flag which will tell us
    # when httpd_config was already run
    return unless $self->{vars}->{httpd} && $self->{vars}->{ssl_module};

    return unless $self->sslca_can;

    Apache::TestSSLCA::clean($self);
}

#XXX: just a quick hack to support t/TEST -ssl
#outside of httpd-test/perl-framework
sub generate_ssl_conf {
    my $self = shift;
    my $vars = $self->{vars};
    my $conf = "$vars->{t_conf}/ssl";
    my $httpd_test_ssl = "../httpd-test/perl-framework/t/conf/ssl";
    my $ssl_conf = "$vars->{top_dir}/$httpd_test_ssl";

    if (-d $ssl_conf and not -d $conf) {
        $self->gendir($conf);
        for (qw(ssl.conf.in)) {
            $self->cpfile("$ssl_conf/$_", "$conf/$_");
        }
        for (qw(certs keys crl)) {
            $self->symlink("$ssl_conf/$_", "$conf/$_");
        }
    }
}

sub find_in_inc {
    my($self, $dir) = @_;
    for my $path (@INC) {
        my $location = "$path/$dir";
        return $location if -d $location;
    }
    return "";
}

sub prepare_t_conf {
    my $self = shift;
    $self->gendir($self->{vars}->{t_conf});
}

my %aliases = (
    "perl-pod"     => "perlpod",
    "binary-httpd" => "httpd",
    "binary-perl"  => "perl",
);
sub generate_httpd_conf {
    my $self = shift;
    my $vars = $self->{vars};

    #generated httpd.conf depends on these things to exist
    $self->generate_types_config;
    $self->generate_index_html;

    $self->gendir($vars->{t_logs});
    $self->gendir($vars->{t_conf});

    my @very_last_postamble = ();
    if (my $extra_conf = $self->generate_extra_conf) {
        for my $file (@$extra_conf) {
            my $entry;
            if ($file =~ /\.conf$/) {
                next if $file =~ m|/httpd\.conf$|;
                $entry = qq(Include "$file");
            }
            elsif ($file =~ /\.pl$/) {
                $entry = qq(<IfModule mod_perl.c>\n    PerlRequire "$file"\n</IfModule>\n);
            }
            else {
                next;
            }

            # put the .last includes very last
            if ($file =~ /\.last\.(conf|pl)$/) {
                 push @very_last_postamble, $entry;
            }
            else {
                $self->postamble($entry);
            }

        }
    }

    $self->configure_proxy;

    my $conf_file = $vars->{t_conf_file};
    my $conf_file_in = join '.', $conf_file, 'in';

    my $in = $self->httpd_conf_template($conf_file_in);

    my $out = $self->genfile($conf_file);

    $self->preamble_run($out);

    for my $name (qw(user group)) { #win32/cygwin do not support
        if ($vars->{$name}) {
            print $out "\u$name    $vars->{$name}\n";
        }
    }

    #2.0: ServerName $ServerName:$Port
    #1.3: ServerName $ServerName
    #     Port       $Port
    my @name_cfg = $self->servername_config($vars->{servername},
                                            $vars->{port});
    for my $pair (@name_cfg) {
        print $out "@$pair\n";
    }

    $self->replace_vars($in, $out);

    # handle the case when mod_alias is built as a shared object
    # but wasn't included in the system-wide httpd.conf
    my $mod_alias = $self->find_apache_module('mod_alias.so');
    if ($mod_alias && -e $mod_alias) {
        print $out <<EOF;
<IfModule !mod_alias.c>
    LoadModule alias_module "$mod_alias"
</IfModule>
EOF
    }

    print $out "<IfModule mod_alias.c>\n";
    for (keys %aliases) {
        next unless $vars->{$aliases{$_}};
        print $out "    Alias /getfiles-$_ $vars->{$aliases{$_}}\n";
    }
    print $out "</IfModule>\n";

    print $out "\n";

    $self->postamble_run($out);

    print $out join "\n", @very_last_postamble;

    close $in;
    close $out or die "close $conf_file: $!";
}

sub need_reconfiguration {
    my($self, $conf_opts) = @_;
    my @reasons = ();
    my $vars = $self->{vars};

    # if '-port select' we need to check from scratch which ports are
    # available
    if (my $port = $conf_opts->{port} || $Apache::TestConfig::Argv{port}) {
        if ($port eq 'select') {
            push @reasons, "'-port $port' requires reconfiguration";
        }
    }

    my $exe = $vars->{apxs} || $vars->{httpd} || '';
    # if httpd.conf is older than executable
    push @reasons,
        "$exe is newer than $vars->{t_conf_file}"
            if -e $exe && 
               -e $vars->{t_conf_file} &&
               -M $exe < -M $vars->{t_conf_file};

    # any .in files are newer than their derived versions?
    if (my @files = $self->extra_conf_files_needing_update) {
        # invalidate the vhosts cache, since a different port could be
        # assigned on reparse
        $self->{vhosts} = {};
        for my $file (@files) {
            push @reasons, "$file.in is newer than $file";
        }
    }

    # if special env variables are used (since they can change any time)
    # XXX: may be we could check whether they have changed since the
    # last run and thus avoid the reconfiguration?
    {
        my $passenv = passenv();
        if (my @env_vars = grep { $ENV{$_} } keys %$passenv) {
            push @reasons, "environment variables (@env_vars) are set";
        }
    }

    return @reasons;
}

sub error_log {
    my($self, $rel) = @_;
    my $file = catfile $self->{vars}->{t_logs}, 'error_log';
    my $rfile = abs2rel $file, $self->{vars}->{top_dir};
    return wantarray ? ($file, $rfile) :
      $rel ? $rfile : $file;
}

#utils

#For Win32 systems, stores the extensions used for executable files
#They may be . prefixed, so we will strip the leading periods.

my @path_ext = ();

if (WIN32) {
    if ($ENV{PATHEXT}) {
        push @path_ext, split ';', $ENV{PATHEXT};
        for my $ext (@path_ext) {
            $ext =~ s/^\.*(.+)$/$1/;
        }
    }
    else {
        #Win9X: doesn't have PATHEXT
        push @path_ext, qw(com exe bat);
    }
}

sub which {
    my $program = shift;

    return undef unless $program;

    for my $base (map { catfile($_, $program) } File::Spec->path()) {
        if ($ENV{HOME} and not WIN32) {
            # only works on Unix, but that's normal:
            # on Win32 the shell doesn't have special treatment of '~'
            $base =~ s/~/$ENV{HOME}/o;
        }

        return $base if -x $base && -f _;

        if (WIN32) {
            for my $ext (@path_ext) {
                return "$base.$ext" if -x "$base.$ext" && -f _;
            }
        }
    }
}

sub apxs {
    my($self, $q, $ok_fail) = @_;
    return unless $self->{APXS};
    my $val;
    unless (exists $self->{_apxs}{$q}) {
        local @ENV{ qw(PATH IFS CDPATH ENV BASH_ENV) };
        my $devnull = devnull();
        my $apxs = shell_ready($self->{APXS});
        $val = qx($apxs -q $q 2>$devnull);
        chomp $val if defined $val; # apxs post-2.0.40 adds a new line
        if ($val) {
            $self->{_apxs}{$q} = $val;
        }
        unless ($val) {
            if ($ok_fail) {
                return "";
            }
            else {
                warn "APXS ($self->{APXS}) query for $q failed\n";
                return $val;
            }
        }
    }
    $self->{_apxs}{$q};
}

sub pop_dir {
    my $dir = shift;

    my @chunks = splitdir $dir;
    while (my $remove = shift) {
        pop @chunks if $chunks[-1] eq $remove;
    }

    catfile @chunks;
}

sub add_inc {
    my $self = shift;
    return if $ENV{MOD_PERL}; #already setup by mod_perl
    require lib;
    # make sure that Apache-Test/lib will be first in @INC,
    # followed by modperl-2.0/lib (or some other project's lib/),
    # followed by blib/ and finally system-wide libs.
    my $top_dir = $self->{vars}->{top_dir};
    my @dirs = map { catdir $top_dir, "blib", $_ } qw(lib arch);

    my $apache_test_dir = catdir $top_dir, "Apache-Test";
    unshift @dirs, $apache_test_dir if -d $apache_test_dir;

    if ($ENV{APACHE_TEST_LIVE_DEV}) {
        my $lib_dir = catdir $top_dir, "lib";
        push @dirs, $lib_dir if -d $lib_dir;
    }

    lib::->import(@dirs);
    #print join "\n", "add_inc", @INC, "";
}

#freeze/thaw so other processes can access config

sub thaw {
    my $class = shift;
    $class->new({thaw => 1, @_});
}

sub freeze {
    require Data::Dumper;
    local $Data::Dumper::Terse = 1;
    my $data = Data::Dumper::Dumper(shift);
    chomp $data;
    $data;
}

sub sync_vars {
    my $self = shift;

    return if $self->{save}; #this is not a cached config

    my $changed = 0;
    my $thaw = $self->thaw;
    my $tvars = $thaw->{vars};
    my $svars = $self->{vars};

    for my $key (@_) {
        for my $v ($tvars, $svars) {
            if (exists $v->{$key} and not defined $v->{$key}) {
                $v->{$key} = ''; #rid undef
            }
        }
        next if exists $tvars->{$key} and exists $svars->{$key} and
                       $tvars->{$key} eq $svars->{$key};
        $tvars->{$key} = $svars->{$key};
        $changed = 1;
    }

    return unless $changed;

    $thaw->{save} = 1;
    $thaw->save;
}

sub save {
    my($self) = @_;

    return unless $self->{save};

    my $name = 'apache_test_config';
    my $file = catfile $self->{vars}->{t_conf}, "$name.pm";
    my $fh = $self->genfile($file);

    debug "saving config data to $name.pm";

    (my $obj = $self->freeze) =~ s/^/    /;

    print $fh <<EOF;
package $name;

sub new {
$obj;
}

1;
EOF

    close $fh or die "failed to write $file: $!";
}

sub as_string {
    my $cfg = '';
    my $command = '';

    # httpd opts
    my $test_config = Apache::TestConfig->new({thaw=>1});
    # XXX: need to run httpd config to get the value of httpd
    if (my $httpd = $test_config->{vars}->{httpd}) {
        $httpd = shell_ready($httpd);
        $command = "$httpd -V";
        $cfg .= "\n*** $command\n";
        $cfg .= qx{$command};
    } else {
        $cfg .= "\n\n*** The httpd binary was not found\n";
    }

    # perl opts
    my $perl = shell_ready($^X);
    $command = "$perl -V";
    $cfg .= "\n\n*** $command\n";
    $cfg .= qx{$command};

    return $cfg;
}

# make a string suitable for feed to shell calls (wrap in quotes and
# escape quotes)
sub shell_ready {
    my $arg = shift;
    $arg =~ s!\\?"!\\"!g;
    return qq["$arg"];
}

### Permanent custom configuration functions ###

# determine which configuration file Apache/TestConfigData.pm to use
# (as there could be several). The order searched is:
# 1) $ENV{HOME}/.apache-test/
# 2) in @INC
my $custom_config_path;
sub custom_config_path {

    return $custom_config_path if $custom_config_path;

    my @inc  = ();

    # XXX $ENV{HOME} isn't propagated in mod_perl
    push @inc, catdir $ENV{HOME}, '.apache-test' if $ENV{HOME};

    push @inc, @INC;

    for (@inc) {
        my $candidate = File::Spec->rel2abs(catfile $_, CUSTOM_CONFIG_FILE);
        next unless -e $candidate;
        return $custom_config_path = $candidate;
    }

    return '';
}

sub custom_config_exists {
    # try to load custom config if it wasn't loaded yet (there are
    # many entry points to this API)
    custom_config_load();

    # it's enough to check whether we have a custom
    # config for 'httpd' or 'apxs'.
    my $httpd = $Apache::TestConfigData::vars->{httpd} || '';
    return 1 if $httpd && -e $httpd && -x _;

    my $apxs = $Apache::TestConfigData::vars->{apxs} || '';
    return 1 if $apxs && -e $apxs && -x _;

    return 0;
}

# to be used only from Apache-Test/Makefile.PL to write the custom
# configuration module so it'll be copied to blib during 'make' and
# updated to use custom config data during 'make test' and then
# installed system-wide via 'make install'
#
# it gets written only if the custom configuration didn't exist
# already
sub custom_config_file_stub_write {

    return if custom_config_exists();

    # It doesn't matter whether it gets written under modperl-2.0/lib
    # or Apache-Test/lib root, since Apache::TestRun uses the same
    # logic and will update that file with real config data, which
    # 'make install' will then pick and install system-wide. but
    # remember that $FindBin::Bin is the location of top-level
    # 'Makefile.PL'
    require FindBin; # load it only for this particular use
    my $path = catfile $FindBin::Bin, "lib",
        Apache::TestConfig::CUSTOM_CONFIG_FILE;

    # write an empty stub
    Apache::TestConfig::custom_config_write($path, '');
}

sub custom_config_save {
    my $self = shift;
    my $conf_opts = shift;

    if ($ENV{APACHE_TEST_NO_STICKY_PREFERENCES}) {
        debug "APACHE_TEST_NO_STICKY_PREFERENCES=1 => " .
            "skipping save of custom config data";
        return;
    }

    my $vars = $self->{vars};
    my $config_dump = '';

    # minimum httpd and/or apxs needs to be set
    return 0 unless $vars->{httpd} or $Apache::TestConfigData::vars->{httpd}
        or          $vars->{apxs}  or $Apache::TestConfigData::vars->{apxs};

    # it doesn't matter how these vars were set (httpd may or may not
    # get set using the path to apxs, w/o an explicit -httpd value)
    for (@data_vars_must) {
        next unless my $var = $vars->{$_} || $conf_opts->{$_};
        $config_dump .= qq{    '$_' => '$var',\n};
    }

    # save these vars only if they were explicitly set via command line
    # options. For example if someone builds A-T as user 'foo', then
    # installs it as root and we save it, all users will now try to
    # configure under that user 'foo' which won't quite work.
    for (@data_vars_opt) {
        next unless my $var = $conf_opts->{$_};
        $config_dump .= qq{    '$_' => '$var',\n};
    }

    if (IS_APACHE_TEST_BUILD) {
        my $path = catfile $vars->{top_dir}, 'lib', CUSTOM_CONFIG_FILE;
        # if it doesn't exist, then we already have a global config file
        # if it does, then we have need to update it and its blib/ copy
        if (-e $path and custom_config_path_is_writable($path)) {
            custom_config_write($path, $config_dump);
            # also update blib/lib, since usually that's the one that
            # appears in @INC when t/TEST is run. and it won't be
            # synced with blib/ unless 'make' was run
            my $blib_path = catfile $vars->{top_dir},
                'blib', 'lib', CUSTOM_CONFIG_FILE;
            if (-e $blib_path and custom_config_path_is_writable($blib_path)) {
                custom_config_write($blib_path, $config_dump);
            }
            return 1;
        }
    }

    my $path;
    if ($path = custom_config_path() ) {
        # do nothing, the config file already exists (global)
        debug "Found custom config '$path'";
    }
    elsif (File::Spec->file_name_is_absolute(__FILE__)) {
        # next try a global location, as if it was configured before
        # Apache::Test's 'make install' (install in the same dir as
        # Apache/TestRun.pm)
        # if the filename is not absolute that means that we are still
        # in Apache-Test build (could just test for IS_APACHE_TEST_BUILD)
        my $base = dirname dirname __FILE__;
        $path = catdir $base, CUSTOM_CONFIG_FILE;
    }

    # check whether we can write to the directory of the chosen path
    # (e.g. root-owned directory)
    if ($path and custom_config_path_is_writable($path)) {
        custom_config_write($path, $config_dump);
        return 1;
    }
    # if we have no writable path yet, try to use ~
    elsif ($ENV{HOME}) {
        $path = catfile $ENV{HOME}, '.apache-test', CUSTOM_CONFIG_FILE;
        if ($path and custom_config_path_is_writable($path)) {
            custom_config_write($path, $config_dump);
            return 1;
        }
    }

    # XXX: should we croak since we failed to write config
    error "Failed to find a config file to save the custom " .
        "configuration in";
    return 0;
}

sub custom_config_path_is_writable {
    my $path = shift;

    return 0 unless $path;

    my $file_created    = '';
    my $top_dir_created = '';
    # first make sure that the file is writable if it exists
    # already (it might be non-writable if installed via EU::MM or in
    # blib/)
    if (-e $path) {
        my $mode = (stat _)[2];
        $mode |= 0200;
        chmod $mode, $path; # it's ok if we fail
        # keep it writable if we have changed it from not being one
        # so that custom_config_save will be able to just overwrite it
    }
    else {
        my $dir = dirname $path;
        if ($dir and !-e $dir) {
            my @dirs = File::Path::mkpath($dir, 0, 0755);
            # the top level dir to nuke on cleanup if it was created
            $top_dir_created = shift @dirs if @dirs;
        }
        # not really create yet, but will be in the moment
        $file_created = 1;
    }

    # try to open for append (even though it may not exist
    my $fh = Symbol::gensym;
    if (open $fh, ">>$path") {
        close $fh;
        # cleanup if we just created the file
        unlink $path if $file_created;
        File::Path::rmtree([$top_dir_created], 0, 0) if $top_dir_created;
        return 1;
    }

    return 0;
}

sub custom_config_write {
    my($path, $config_dump) = @_;

    my $pkg = << "EOC";
package Apache::TestConfigData;

use strict;
use warnings;

\$Apache::TestConfigData::vars = {
$config_dump
};

1;

=head1 NAME

Apache::TestConfigData - Configuration file for Apache::Test

=cut
EOC

    debug "Writing custom config $path";
    my $dir = dirname $path;
    File::Path::mkpath($dir, 0, 0755) unless -e $dir;
    my $fh = Symbol::gensym;
    open $fh, ">$path" or die "Cannot open $path: $!";
    print $fh $pkg;
    close $fh;
}

sub custom_config_add_conf_opts {
    my $args = shift;

    return unless $Apache::TestConfigData::vars and 
        keys %$Apache::TestConfigData::vars;

    debug "overlaying custom config data";

    # the logic is quite complicated with 'httpd' and 'apxs', since
    # one is enough to run the test suite, and we need to avoid the
    # situation where both are saved in custom config but only one
    # (let's say httpd) is overriden by the command line /env var and
    # a hell may break loose if we take that overriden httpd value and
    # also use apxs from custom config which could point to a different
    # server. So if there is an override of apxs or httpd, do not use
    # the custom config for apxs or httpd.
    my $vars_must_overriden = grep {
        $ENV{ $vars_to_env{$_} } || $args->{$_}
    } @data_vars_must;

    # mod_perl 2.0 build always knows the right httpd location (and
    # optionally apxs)
    $vars_must_overriden++ if IS_MOD_PERL_2_BUILD();

    unless ($vars_must_overriden) {
        for (@data_vars_must) {
            next unless $Apache::TestConfigData::vars->{$_};
            $args->{$_} = $Apache::TestConfigData::vars->{$_};
        }
    }

    for (@data_vars_opt) {
        next unless $Apache::TestConfigData::vars->{$_};
        # env vars override custom config
        my $env_value = $ENV{ $vars_to_env{$_} };
        next unless defined $env_value and length $env_value;
        $args->{$_} ||= $Apache::TestConfigData::vars->{$_};
    }
}

my $custom_config_loaded = 0;
sub custom_config_load {

    if ($ENV{APACHE_TEST_NO_STICKY_PREFERENCES}) {
        debug "APACHE_TEST_NO_STICKY_PREFERENCES=1 => " .
            "skipping load of custom config data";
        return;
    }

    return if $custom_config_loaded;

    if (my $custom_config_path = custom_config_path()) {
        debug "loading custom config data from: '$custom_config_path'";
        $custom_config_loaded++;
        require $custom_config_path;
    }
    else {
        debug "no custom config data was loaded";
    }
}

sub custom_config_first_time {
    my $self = shift;
    my $conf_opts = shift;

    my $vars = $self->{vars};

    print qq[

We are now going to configure the Apache-Test framework.
This configuration process needs to be done only once.

];

    print qq[

First we need to know where the 'httpd' executable is located.
If you have more than one Apache server is installed, make sure
you supply the path to the one you are going to use for testing.
You can always override this setting at run time via the '-httpd'
option. For example:

  % t/TEST -httpd /path/to/alternative/httpd

or via the environment variable APACHE_TEST_HTTPD. For example:

  % APACHE_TEST_HTTPD=/path/to/alternative/httpd t/TEST

If for some reason you want to skip the test suite, type: skip
];

    {
        my %choices = ();
        my @tries = qw(httpd httpd2);
        # Win32 uses Apache or perhaps Apache2, not apache/apache2
        push @tries, WIN32 ? qw(Apache Apache2) : qw(apache apache2);
        for (grep defined $_,
             map({ catfile $vars->{$_}, $vars->{target} } qw(sbindir bindir)),
             $self->default_httpd, which($vars->{target}),
             $ENV{APACHE}, $ENV{APACHE2},
             $ENV{APACHE_TEST_HTTPD}, $ENV{APACHE_TEST_HTTPD2},
             map {which($_)} @tries) {
            $choices{$_}++ if -e $_ && -x _;
        }
        my $optional = 0;
        my $wanted = 'httpd';
        $vars->{$wanted} = 
            _custom_config_prompt_path($wanted, \%choices, $optional);
    }

    print qq[

Next we need to know where the 'apxs' script is located. This script
provides a lot of information about the apache installation, and makes
it easier to find things. However it's not available on all platforms,
therefore it's optional.

If you don't have it installed it's not a problem. Just press Enter.

Notice that if you have Apache 2.x installed that script could be
called as 'apxs2'.

If you have more than one Apache server is installed, make sure you
supply the path to the apxs script you are going to use for testing.
You can always override this setting at run time via the '-apxs'
option. For example:

  % t/TEST -apxs /path/to/alternative/apxs

or via the environment variable APACHE_TEST_APXS. For example:

  % APACHE_TEST_APXS=/path/to/alternative/apxs t/TEST

];
    {
        my %choices = ();
        for (grep defined $_,
             map({ catfile $vars->{$_}, 'apxs' } qw(sbindir bindir)),
             $self->default_apxs,
             $ENV{APXS},  $ENV{APACHE_TEST_APXS},  which('apxs'),
             $ENV{APXS2}, $ENV{APACHE_TEST_APXS2}, which('apxs2')) {
            $choices{$_}++ if -e $_ && -x _;
        }
        my $optional = 1;
        my $wanted = 'apxs';
        $vars->{$wanted} = 
            _custom_config_prompt_path($wanted, \%choices, $optional);
    }

    $self->custom_config_save($conf_opts);

    # we probably could reconfigure on the fly ($self->configure), but
    # the problem is various cached data which won't be refreshed. so
    # the simplest is just to restart the run from scratch
    Apache::TestRun::rerun();
}

sub _custom_config_prompt_path {
    my($wanted, $rh_choices, $optional) = @_;

    my $ans;
    my $default = '';
    my $optional_str = $optional ? " (optional)" : '';
    my $prompt =
        "\nPlease provide a full path to$optional_str '$wanted' executable";

    my @choices = ();
    if (%$rh_choices) {
        $prompt .= " or choose from the following options:\n\n";
        my $c = 0;
        for (sort keys %$rh_choices) {
            $c++;
            $prompt .= "    [$c] $_\n";
            push @choices, $_;
        }
        $prompt .= " \n";
        $default = 1; # a wild guess
    }
    else {
        $prompt .= ":\n\n";
    }

    while (1) {
        $ans = ExtUtils::MakeMaker::prompt($prompt, $default);

        # strip leading/closing spaces
        $ans =~ s/^\s*|\s*$//g;

        # convert the item number to the path
        if ($ans =~ /^(\d+)$/) {
            if ($1 > 0 and $choices[$1-1]) {
                $ans = $choices[$1-1];
            }
            else {
                warn "The choice '$ans' doesn't exist\n";
                next;
            }
        }

        if ($optional) {
            return '' unless $ans;
        }

        # stop the test suite without an error (so automatic tools
        # like CPAN.pm will be able to continue)
        if (lc($ans) eq 'skip' && !$optional) {
            Apache::TestRun::skip_test_suite();
            next; # in case they change their mind
        }

        unless (File::Spec->file_name_is_absolute($ans)) {
            my $cwd = Cwd::cwd();
            warn "The path '$ans' is not an absolute path. " .
                "Please specify an absolute path\n";
            next;
        }

        warn("'$ans' doesn't exist\n"),     next unless -e $ans;
        warn("'$ans' is not executable\n"), next unless -x $ans;

        return $ans;
    }
}

1;

=head1 NAME

Apache::TestConfig -- Test Configuration setup module

=head1 SYNOPSIS

  use Apache::TestConfig;

  my $cfg = Apache::TestConfig->new(%args)
  my $fh = $cfg->genfile($file);
  $cfg->writefile($file, $content);
  $cfg->gendir($dir);
  ...

=head1 DESCRIPTION

C<Apache::TestConfig> is used in creating the C<Apache::Test>
configuration files.

=head1 FUNCTIONS

=over

=item genwarning()

  my $warn = $cfg->genwarning($filename)

genwarning() returns a warning string as a comment, saying that the
file was autogenerated and that it's not a good idea to modify this
file. After the warning a perl trace of calls to this this function is
appended. This trace is useful for finding what code has created the
file.

  my $warn = $cfg->genwarning($filename, $from_filename)

If C<$from_filename> is specified it'll be used in the warning to tell
which file it was generated from.

genwarning() automatically recognizes the comment type based on the
file extension. If the extension is not recognized, the default C<#>
style is used.

Currently it support C<E<lt>!-- --E<gt>>, C</* ... */> and C<#>
styles.

=item genfile()

  my $fh = $cfg->genfile($file);

genfile() creates a new file C<$file> for writing and returns a file
handle.

If parent directories of C<$file> don't exist they will be
automagically created.

The file C<$file> and any created parent directories (if found empty)
will be automatically removed on cleanup.

A comment with a warning and calls trace is added to the top of this
file. See genwarning() for more info about this comment.

  my $fh = $cfg->genfile($file, $from_file);

If C<$from_filename> is specified it'll be used in the warning to tell
which file it was generated from.

  my $fh = $cfg->genfile($file, $from_file, $nowarning);

If C<$nowarning> is true, the warning won't be added. If using this
optional argument and there is no C<$from_file> you must pass undef as
in:

  my $fh = $cfg->genfile($file, undef, $nowarning);


=item writefile()

  $cfg->writefile($file, $content, [$nowarning]);

writefile() creates a new file C<$file> with the content of
C<$content>.

A comment with a warning and calls trace is added to the top of this
file unless C<$nowarnings> is passed and set to a true value. See
genwarning() for more info about this comment.

If parent directories of C<$file> don't exist they will be
automagically created.

The file C<$file> and any created parent directories (if found empty)
will be automatically removed on cleanup.

=item write_perlscript()

  $cfg->write_perlscript($filename, @lines);

Similar to writefile() but creates an executable Perl script with
correctly set shebang line.

=item gendir()

  $cfg->gendir($dir);

gendir() creates a new directory C<$dir>.

If parent directories of C<$dir> don't exist they will be
automagically created.

The directory C<$dir> and any created parent directories will be
automatically removed on cleanup if found empty.

=back

=head1 Environment Variables

The following environment variables affect the configuration and the
run-time of the C<Apache::Test> framework:

=head2 APACHE_TEST_COLOR

To aid visual control over the configuration process and the run-time
phase, C<Apache::Test> uses coloured fonts when the environment
variable C<APACHE_TEST_COLOR> is set to a true value.

=head2 APACHE_TEST_LIVE_DEV

When using C<Apache::Test> during the project development phase, it's
often convenient to have the I<project/lib> (live) directory appearing
first in C<@INC> so any changes to the Perl modules, residing in it,
immediately affect the server, without a need to rerun C<make> to
update I<blib/lib>. When the environment variable
C<APACHE_TEST_LIVE_DEV> is set to a true value during the
configuration phase (C<t/TEST -config>, C<Apache::Test> will
automatically unshift the I<project/lib> directory into C<@INC>, via
the autogenerated I<t/conf/modperl_inc.pl> file.

=head1 AUTHOR

=head1 SEE ALSO

perl(1), Apache::Test(3)

=cut


__DATA__
Listen     0.0.0.0:@Port@

ServerRoot   "@ServerRoot@"
DocumentRoot "@DocumentRoot@"

PidFile     @t_logs@/httpd.pid
ErrorLog    @t_logs@/error_log
LogLevel    debug

<IfModule mod_log_config.c>
    TransferLog @t_logs@/access_log
</IfModule>

ServerAdmin @ServerAdmin@

#needed for http/1.1 testing
KeepAlive       On

HostnameLookups Off

<Directory />
    Options FollowSymLinks
    AllowOverride None
</Directory>

<IfModule @THREAD_MODULE@>
    StartServers         1
    MinSpareThreads      @MinClients@
    MaxSpareThreads      @MinClients@
    ThreadsPerChild      @MinClients@
    MaxClients           @MaxClientsThreadedMPM@
    MaxRequestsPerChild  0
</IfModule>

<IfModule perchild.c>
    NumServers           1
    StartThreads         @MinClients@
    MinSpareThreads      @MinClients@
    MaxSpareThreads      @MinClients@
    MaxThreadsPerChild   @MaxClients@
    MaxRequestsPerChild  0
</IfModule>

<IfModule prefork.c>
    StartServers         @MinClients@
    MinSpareServers      @MinClients@
    MaxSpareServers      @MinClients@
    MaxClients           @MaxClients@
    MaxRequestsPerChild  0
</IfModule>

<IfDefine APACHE1>
    StartServers         @MinClients@
    MinSpareServers      @MinClients@
    MaxSpareServers      @MinClients@
    MaxClients           @MaxClients@
    MaxRequestsPerChild  0
</IfDefine>

<IfModule mpm_winnt.c>
    ThreadsPerChild      50
    MaxRequestsPerChild  0
</IfModule>

<Location /server-info>
    SetHandler server-info
</Location>

<Location /server-status>
    SetHandler server-status
</Location>

