package TestAPRlib::pool;

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

use Apache::Test;
use Apache::TestUtil;
use Apache::TestTrace;

use APR::Pool ();
use APR::Table ();

sub num_of_tests {
    return 77;
}

sub test {

    my $pool = APR::Pool->new();
    my $table = APR::Table::make($pool, 2);

    ### custom pools ###

    # test: explicit pool object destroy destroys the custom pool
    {
        my $p = APR::Pool->new;

        $p->cleanup_register(\&set_cleanup, [$table, 'new destroy']);

        ok t_cmp(ancestry_count($p), 1,
                 "a new pool has one ancestor: the global pool");

        # explicity destroy the object
        $p->destroy;

        my @notes = $table->get('cleanup');

        ok t_cmp(scalar(@notes), 1, "should be 1 note");

        ok t_cmp($notes[0], 'new destroy');

        $table->clear;
    }


    # test: lexical scoping DESTROYs the custom pool
    {
        {
            my $p = APR::Pool->new;

            ok t_cmp(ancestry_count($p), 1,
                 "a new pool has one ancestor: the global pool");

            $p->cleanup_register(\&set_cleanup, [$table, 'new scoped']);
        }

        my @notes = $table->get('cleanup');

        ok t_cmp(scalar(@notes), 1, "should be 1 note");

        ok t_cmp($notes[0], 'new scoped');

        $table->clear;
    }



    ### custom pools + sub-pools ###

    # test: basic pool and sub-pool tests + implicit destroy of pool objects
    {
        {
            my ($pp, $sp) = both_pools_create_ok($table);
        }

        both_pools_destroy_ok($table);

        $table->clear;
    }


    # test: explicitly destroying a parent pool should destroy its
    # sub-pool
    {
        my ($pp, $sp) = both_pools_create_ok($table);

        # destroying $pp should destroy the subpool $sp too
        $pp->destroy;

        both_pools_destroy_ok($table);

        $table->clear;
    }



    # test: destroying a sub-pool before the parent pool
    {
        my ($pp, $sp) = both_pools_create_ok($table);

        $sp->destroy;
        $pp->destroy;

        both_pools_destroy_ok($table);

        $table->clear;
    }


    # test: destroying a sub-pool explicitly after the parent pool destroy

    # the parent pool should have already destroyed the child pool, so
    # the object is invalid
    {
        my ($pp, $sp) = both_pools_create_ok($table);

        $pp->destroy;
        $sp->destroy;

        both_pools_destroy_ok($table);

        $table->clear;
    }


    # test: destroying a sub-pool before the parent pool and trying to
    # call APR::Pool methods on the a subpool object which points to a
    # destroyed pool
    {
        my ($pp, $sp) = both_pools_create_ok($table);

        # parent pool destroys child pool
        $pp->destroy;

        # this should "gracefully" fail, since $sp's guts were
        # destroyed when the parent pool was destroyed
        eval { $pp = $sp->parent_get };
        ok t_cmp($@,
                 qr/invalid pool object/,
                 "parent pool destroys child pool");

        # since pool $sp now contains 0 pointer, if we try to make a
        # new pool out of it, it's the same as APR->new (i.e. it'll
        # use the global top level pool for it), so the resulting pool
        # should have an ancestry length of exactly 1
        my $ssp = $sp->new;
        ok t_cmp(ancestry_count($ssp), 1,
                 "a new pool has one ancestor: the global pool");


        both_pools_destroy_ok($table);

        $table->clear;
    }

    # test: make sure that one pool won't destroy/affect another pool,
    # which happened to be allocated at the same memory address after
    # the pointer to the first pool was destroyed
    {
        my $pp2;
        {
            my $pp = APR::Pool->new;
            $pp->destroy;
            # $pp2 ideally should take the exact place of apr_pool
            # previously pointed to by $pp
            $pp2 = APR::Pool->new;
            # $pp object didn't go away yet (it'll when exiting this
            # scope). in the previous implementation, $pp will be
            # destroyed second time on the exit of the scope and it
            # could happen to work, because $pp2 pointer has allocated
            # exactly the same address. and if so it would have killed
            # the pool that $pp2 points to

            # this should "gracefully" fail, since $pp's guts were
            # destroyed when the parent pool was destroyed
            # must make sure that it won't try to hijack the new pool
            # $pp2 that (hopefully) took over $pp's place
            eval { $pp->parent_get };
            ok t_cmp($@,
                     qr/invalid pool object/,
                     "a dead pool is a dead pool");
        }

        # next make sure that $pp2's pool is still alive
        $pp2->cleanup_register(\&set_cleanup, [$table, 'overtake']);
        $pp2->destroy;

        my @notes = $table->get('cleanup');

        ok t_cmp(scalar(@notes), 1, "should be 1 note");
        ok t_cmp($notes[0], 'overtake');

        $table->clear;

    }

    # test: similar to the previous test, but this time, the parent
    # pool destroys the child pool. a second allocation of a new pair
    # of the parent and child pools take over exactly the same
    # allocations. so if there are any ghost objects, they must not
    # find the other pools and use them as they own. for example they
    # could destroy the pools, and the perl objects of the pair would
    # have no idea that someone has destroyed the pools without their
    # knowledge. the previous implementation suffered from this
    # problem. the new implementation uses an SV which is stored in
    # the object and in the pool. when the pool is destroyed the SV
    # gets its IVX pointer set to 0, which affects any perl object
    # that is a ref to that SV. so once an apr pool is destroyed all
    # perl objects pointing to it get automatically invalidated and
    # there is no risk of hijacking newly created pools that happen to
    # be at the same memory address.

    {
        my ($pp2, $sp2);
        {
            my $pp = APR::Pool->new;
            my $sp = $pp->new;
            # parent destroys $sp
            $pp->destroy;

            # hopefully these pool will take over the $pp and $sp
            # allocations
            ($pp2, $sp2) = both_pools_create_ok($table);
        }

        # $pp and $sp shouldn't have triggered any cleanups
        my @notes = $table->get('cleanup');
        ok t_cmp(scalar(@notes), 0, "should be 0 notes");
        $table->clear;

        # parent pool destroys child pool
        $pp2->destroy;

        both_pools_destroy_ok($table);

        $table->clear;
    }

    # test: only when the last references to the pool object is gone
    # it should get destroyed
    {

        my $cp;

        {
            my $sp = APR::Pool->new();

            $sp->cleanup_register(\&set_cleanup, [$table, 'several references']);

            $cp = $sp;
            # destroy of $sp shouldn't call apr_pool_destroy, because
            # $cp still references to it
        }

        my @notes = $table->get('cleanup');
        ok t_cmp(scalar(@notes), 0, "should be 0 notes");
        $table->clear;

        # now the last copy is gone and the cleanup hooks will be called
        $cp->destroy;

        @notes = $table->get('cleanup');
        ok t_cmp(scalar(@notes), 1, "should be 1 note");
        ok t_cmp($notes[0], 'several references');

        $table->clear;
    }
    {
        # and another variation
        my $pp = APR::Pool->new();
        my $sp = $pp->new;

        my $gp  = $pp->parent_get;
        my $pp2 = $sp->parent_get;

        # parent destroys children
        $pp->destroy;

        # grand parent ($pool) is undestroyable (core pool)
        $gp->destroy;

        # now all custom pools are destroyed - $sp and $pp2 point nowhere
        $pp2->destroy;
        $sp->destroy;

        ok 1;
    }

    # cleanup_register using a function name as a callback
    {
        {
            my $p = APR::Pool->new;
            $p->cleanup_register('set_cleanup', [$table, 'function name']);
        }

        my @notes = $table->get('cleanup');
        ok t_cmp($notes[0], 'function name', "function name callback");

        $table->clear;
    }

    # cleanup_register using an anon sub callback
    {
        {
            my $p = APR::Pool->new;

            $p->cleanup_register(sub { &set_cleanup }, [$table, 'anon sub']);
        }

        my @notes = $table->get('cleanup');
        ok t_cmp($notes[0], 'anon sub', "anon callback");

        $table->clear;
    }

    # registered callbacks are run in reversed order LIFO
    {
        {
            my $p = APR::Pool->new;

            $p->cleanup_register(\&add_cleanup, [$table, 'first']);
            $p->cleanup_register(\&add_cleanup, [$table, 'second']);
        }

        my @notes = $table->get('cleanup');
        ok t_cmp($notes[0], 'second', "two cleanup functions");
        ok t_cmp($notes[1], 'first',  "two cleanup functions");

        $table->clear;
    }

    # undefined cleanup subs
    {
        my $p = APR::Pool->new;
        $p->cleanup_register('TestAPR::pool::some_non_existing_sub', 1);

        my @warnings;
        local $SIG{__WARN__} = sub {push @warnings, @_};
        $p->destroy;

        ok t_cmp($warnings[0],
                 qr/Undefined subroutine/,
                 "non existing function");
    }
    {
        my $p = APR::Pool->new;
        $p->cleanup_register(\&non_existing1, 1);

        my @warnings;
        local $SIG{__WARN__} = sub {push @warnings, @_};
        $p->destroy;

        ok t_cmp($warnings[0],
                 qr/Undefined subroutine/,
                 "non existing function");
    }

    # cleanups throwing exceptions
    {
        my $p = APR::Pool->new;
        $p->cleanup_register(sub {die "1\n"}, 1);
        $p->cleanup_register(sub {die "2\n"}, 1);

        my @warnings;
        local $SIG{__WARN__} = sub {push @warnings, @_};
        local $@="to be preserved";
        undef $p;

        ok t_cmp(\@warnings,
                 [map "APR::Pool: cleanup died: $_\n", 2, 1],
                 "exceptions thrown by cleanups");
        ok t_cmp($@, "to be preserved", '$@ is preserved');
    }

    ### $p->clear ###
    {
        my ($pp, $sp) = both_pools_create_ok($table);
        $pp->clear;
        # both pools should have run their cleanups
        both_pools_destroy_ok($table);

        # sub-pool $sp should be now bogus, as clear() destroys
        # subpools
        eval { $sp->parent_get };
        ok t_cmp($@,
                 qr/invalid pool object/,
                 "clear destroys sub pools");

        # now we should be able to use the parent pool without
        # allocating it
        $pp->cleanup_register(\&set_cleanup, [$table, 're-using pool']);
        $pp->destroy;

        my @notes = $table->get('cleanup');
        ok t_cmp('re-using pool', $notes[0]);

        $table->clear;
    }


    # a pool can be tagged, so when doing low level apr_pool tracing
    # (when apr is compiled with -DAPR_POOL_DEBUG) it's possible to
    # grep(1) for a certain tag, so it's a useful method
    {
        my $p = APR::Pool->new;
        $p->tag("my pool");

        # though there is no way we can get back the value to test,
        # since there is no apr_pool_tag read accessor
        ok 1;
    }

    # out-of-scope pools
    {
        my $sp = APR::Pool->new->new;
        # the parent temp pool must stick around
        ok t_cmp(2, ancestry_count($sp),
                 "parent pool is still alive + global pool");
    }

    # other stuff
    {
        my $p = APR::Pool->new;

        # find some method that wants a pool object and try to pass it
        # an object that was already destroyed e.g. APR::Table::make($p, 2);

        # only available with -DAPR_POOL_DEBUG
        #my $num_bytes = $p->num_bytes;
        #ok $num_bytes;

    }
}

# returns how many ancestor generations the pool has (parent,
# grandparent, etc.)
sub ancestry_count {
    my $child = shift;
    my $gen = 0;
    while (my $parent = $child->parent_get) {
        # prevent possible endless loops
        die "child pool reports to be its own parent, corruption!"
            if $parent == $child;
        $gen++;
        die "child knows its parent, but the parent denies having that child"
            unless $parent->is_ancestor($child);
        $child = $parent;
    }
    return $gen;
}

sub add_cleanup {
    my $arg = shift;
    debug "adding cleanup note: $arg->[1]";
    $arg->[0]->add(cleanup => $arg->[1]);
    1;
}

sub set_cleanup {
    my $arg = shift;
    debug "setting cleanup note: $arg->[1]";
    $arg->[0]->set(cleanup => $arg->[1]);
    1;
}

# +4 tests
sub both_pools_create_ok {
    my $table = shift;

    my $pp = APR::Pool->new;

    ok t_cmp(1, $pp->isa('APR::Pool'), "isa('APR::Pool')");

    ok t_cmp(1, ancestry_count($pp),
             "a new pool has one ancestor: the global pool");

    my $sp = $pp->new;

    ok t_cmp($sp->isa('APR::Pool'), 1, "isa('APR::Pool')");

    ok t_cmp(ancestry_count($sp), 2,
             "a subpool has 2 ancestors: the parent and global pools");

    $pp->cleanup_register(\&add_cleanup, [$table, 'parent']);
    $sp->cleanup_register(\&set_cleanup, [$table, 'child']);

    return ($pp, $sp);

}

# +3 tests
sub both_pools_destroy_ok {
    my $table = shift;
    my @notes = $table->get('cleanup');

    ok t_cmp(scalar(@notes), 2, "should be 2 notes");
    ok t_cmp($notes[0], 'child');
    ok t_cmp($notes[1], 'parent');
}

1;
