blob: 519cba7341e74fc07a6ce5d4065563f96e0ebed5 [file] [log] [blame]
# please insert nothing before this line: -*- mode: cperl; cperl-indent-level: 4; cperl-continued-statement-offset: 4; indent-tabs-mode: nil -*-
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;