The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use strict;
$^W++;
use Class::Prototyped qw(:EZACCESS);
use Data::Dumper;
use Test;

BEGIN {
	$|++;
	plan tests => 12
}

$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Sortkeys = 1;

my $record = '';

package MyClass;
@MyClass::ISA = qw(Class::Prototyped);

sub DESTROY {
	$record .= "You are in MyClass::DESTROY for " . ref($_[0]) . "\n";
}

package MyClass_Alt;
@MyClass_Alt::ISA = qw(Class::Prototyped);

sub DESTROY {
	$record .= "You are in MyClass_Alt::DESTROY for " . ref($_[0]) . "\n";
}

package main;

my $name1;
my $name2;
my $name3;


# This demonstrates normal destruction.
{
	my $foo = MyClass->new(
		'destroy!' => sub {
				$record .= "You are in the objects destroy.\n";
				$_[0]->super('destroy');
				$record .= "Just called super-destroy.\n";
			},
		);
	$name1 = ref($foo);
}
ok( $record, <<END);
You are in the objects destroy.
Just called super-destroy.
You are in MyClass::DESTROY for $name1
END

use Data::Dumper;

# This demonstrates destruction where $p2 has a reference in it to $p1.  Note
# that the destructor for $p1 runs as soon as the C::P::destory destructor
# runs on $p2, thus interrupting the $p2 destruction sequence
$record = '';
{
	my $p2 = MyClass->new(
		'destroy!' => sub {
				$record .= "p2 before super for " . ref($_[0]) . "\n";
				$_[0]->super('destroy');
				$record .= "p2 after super for " . ref($_[0]) . "\n";
			},
		);
	$name2 = ref($p2);

	{
		my $p1 = MyClass->new(
			'destroy!' => sub {
					$record .= "p1 before super for " . ref($_[0]) . "\n";
					$_[0]->super('destroy');
					$record .= "p1 after super for " . ref($_[0]) . "\n";
				},
			);
		$name1 = ref($p1);

		$p2->addSlot('p1' => $p1);
	}
}
ok( $record, <<END);
p2 before super for $name2
p1 before super for $name1
p1 after super for $name1
You are in MyClass::DESTROY for $name1
p2 after super for $name2
You are in MyClass::DESTROY for $name2
END


# This demonstrates destruction where $p2 has a parent slot that points to
# $p1.  In this situation, the $p2 destruction sequence is not interrupted
# because the reference to $p1 is not removed until the $p2 destruction
# sequence has completed.
$record = '';
{
	my $p2 = MyClass->new(
		'destroy!' => sub {
				$record .= "p2 before super for " . ref($_[0]) . "\n";
				$_[0]->super('destroy');
				$record .= "p2 after super for " . ref($_[0]) . "\n";
			},
		);
	$name2 = ref($p2);

	{
		my $p1 = MyClass->new(
			'destroy!' => sub {
					$record .= "p1 before super for " . ref($_[0]) . "\n";
					$_[0]->super('destroy');
					$record .= "p1 after super for " . ref($_[0]) . "\n";
				},
			);
		$name1 = ref($p1);

		$p2->addSlot('parent*' => $p1);
	}
}
ok( $record, <<END);
p2 before super for $name2
p2 after super for $name2
You are in MyClass::DESTROY for $name2
p1 before super for $name1
p1 after super for $name1
You are in MyClass::DESTROY for $name1
END


# In this test, $p3 and $p2 reference $p1.  The destructor for $p2 completes
# removing one of the references to $p1.  Then the destructor for $p3 runs,
# at which point the last reference to $p1 is removed and so the destructor
# for $p1 interrupts the $p3 destruction sequence.
$record = '';
{
	my $p3 = MyClass->new(
		'destroy!' => sub {
				$record .= "p3 before super for " . ref($_[0]) . "\n";
				$_[0]->super('destroy');
				$record .= "p3 after super for " . ref($_[0]) . "\n";
			},
		);
	$name3 = ref($p3);

	{
		my $p2 = MyClass->new(
			'destroy!' => sub {
					$record .= "p2 before super for " . ref($_[0]) . "\n";
					$_[0]->super('destroy');
					$record .= "p2 after super for " . ref($_[0]) . "\n";
				},
			);
		$name2 = ref($p2);

		{
			my $p1 = MyClass->new(
				'destroy!' => sub {
						$record .= "p1 before super for " . ref($_[0]) . "\n";
						$_[0]->super('destroy');
						$record .= "p1 after super for " . ref($_[0]) . "\n";
					},
				);
			$name1 = ref($p1);

			$p2->addSlot('p1' => $p1);
			$p3->addSlot('p1' => $p1);
		}
	}
}
ok( $record, <<END);
p2 before super for $name2
p2 after super for $name2
You are in MyClass::DESTROY for $name2
p3 before super for $name3
p1 before super for $name1
p1 after super for $name1
You are in MyClass::DESTROY for $name1
p3 after super for $name3
You are in MyClass::DESTROY for $name3
END


# Same test, but using parent slots instead.  Note that as a result, the
# destruction sequence for $p3 completes before the destructor for $p1
# is triggered
$record = '';
{
	my $p3 = MyClass->new(
		'destroy!' => sub {
				$record .= "p3 before super for " . ref($_[0]) . "\n";
				$_[0]->super('destroy');
				$record .= "p3 after super for " . ref($_[0]) . "\n";
			},
		);
	$name3 = ref($p3);

	{
		my $p2 = MyClass->new(
			'destroy!' => sub {
					$record .= "p2 before super for " . ref($_[0]) . "\n";
					$_[0]->super('destroy');
					$record .= "p2 after super for " . ref($_[0]) . "\n";
				},
			);
		$name2 = ref($p2);

		{
			my $p1 = MyClass->new(
				'destroy!' => sub {
						$record .= "p1 before super for " . ref($_[0]) . "\n";
						$_[0]->super('destroy');
						$record .= "p1 after super for " . ref($_[0]) . "\n";
					},
				);
			$name1 = ref($p1);

			$p2->addSlot('parent*' => $p1);
			$p3->addSlot('parent*' => $p1);
		}
	}
}
ok( $record, <<END);
p2 before super for $name2
p2 after super for $name2
You are in MyClass::DESTROY for $name2
p3 before super for $name3
p3 after super for $name3
You are in MyClass::DESTROY for $name3
p1 before super for $name1
p1 after super for $name1
You are in MyClass::DESTROY for $name1
END


# Here we use qw([parent* promote]) instead of parent* to move the parent slot
# up in precedence over class*.  In the previous tests, the destroy method
# wasn't inherited from $p1 because the destroy method in C::P by way of
# MyClass took precedence.
$record = '';
{
	my $p2 = MyClass->new(
		'destroy!' => sub {
				$record .= "p2 before super for " . ref($_[0]) . "\n";
				$_[0]->super('destroy');
				$record .= "p2 after super for " . ref($_[0]) . "\n";
			},
		);
	$name2 = ref($p2);

	{
		my $p1 = MyClass->new(
			'destroy!' => sub {
					$record .= "p1 before super for " . ref($_[0]) . "\n";
					$_[0]->super('destroy');
					$record .= "p1 after super for " . ref($_[0]) . "\n";
				},
			);
		$name1 = ref($p1);

		$p2->addSlot([qw(parent* promote)] => $p1);
	}
}
ok( $record, <<END);
p2 before super for $name2
p1 before super for $name2
p1 after super for $name2
p2 after super for $name2
You are in MyClass::DESTROY for $name2
p1 before super for $name1
p1 after super for $name1
You are in MyClass::DESTROY for $name1
END


# Note that we get the same behavior (including access to MyClass::DESTROY
# via the $p1 inheritance path) when we have a classless object.
$record = '';
{
	my $p2 = Class::Prototyped->new(
		'destroy!' => sub {
				$record .= "p2 before super for " . ref($_[0]) . "\n";
				$_[0]->super('destroy');
				$record .= "p2 after super for " . ref($_[0]) . "\n";
			},
		);
	$name2 = ref($p2);

	{
		my $p1 = MyClass->new(
			'destroy!' => sub {
					$record .= "p1 before super for " . ref($_[0]) . "\n";
					$_[0]->super('destroy');
					$record .= "p1 after super for " . ref($_[0]) . "\n";
				},
			);
		$name1 = ref($p1);

		$p2->addSlot('parent*' => $p1);
	}
}
ok( $record, <<END);
p2 before super for $name2
p1 before super for $name2
p1 after super for $name2
p2 after super for $name2
You are in MyClass::DESTROY for $name2
p1 before super for $name1
p1 after super for $name1
You are in MyClass::DESTROY for $name1
END


# Demonstration of two objects referencing the same chained destructor
$record = '';
{
	my $p3 = MyClass->new(
		'destroy!' => sub {
				$record .= "p3 before super for " . ref($_[0]) . "\n";
				$_[0]->super('destroy');
				$record .= "p3 after super for " . ref($_[0]) . "\n";
			},
		);
	$name3 = ref($p3);

	{
		my $p2 = MyClass->new(
			'destroy!' => sub {
					$record .= "p2 before super for " . ref($_[0]) . "\n";
					$_[0]->super('destroy');
					$record .= "p2 after super for " . ref($_[0]) . "\n";
				},
			);
		$name2 = ref($p2);

		{
			my $p1 = MyClass->new(
				'destroy!' => sub {
						$record .= "p1 before super for " . ref($_[0]) . "\n";
						$_[0]->super('destroy');
						$record .= "p1 after super for " . ref($_[0]) . "\n";
					},
				);
			$name1 = ref($p1);

			$p2->addSlot([qw(parent* promote)] => $p1);
			$p3->addSlot([qw(parent* promote)] => $p1);
		}
	}
}
ok( $record, <<END);
p2 before super for $name2
p1 before super for $name2
p1 after super for $name2
p2 after super for $name2
You are in MyClass::DESTROY for $name2
p3 before super for $name3
p1 before super for $name3
p1 after super for $name3
p3 after super for $name3
You are in MyClass::DESTROY for $name3
p1 before super for $name1
p1 after super for $name1
You are in MyClass::DESTROY for $name1
END


# Demonstration of chained destructors
$record = '';
{
	my $p3 = MyClass->new(
		'destroy!' => sub {
				$record .= "p3 before super for " . ref($_[0]) . "\n";
				$_[0]->super('destroy');
				$record .= "p3 after super for " . ref($_[0]) . "\n";
			},
		);
	$name3 = ref($p3);

	{
		my $p2 = MyClass->new(
			'destroy!' => sub {
					$record .= "p2 before super for " . ref($_[0]) . "\n";
					$_[0]->super('destroy');
					$record .= "p2 after super for " . ref($_[0]) . "\n";
				},
			);
		$name2 = ref($p2);

		$p3->addSlot([qw(parent* promote)] => $p2);

		{
			my $p1 = MyClass->new(
				'destroy!' => sub {
						$record .= "p1 before super for " . ref($_[0]) . "\n";
						$_[0]->super('destroy');
						$record .= "p1 after super for " . ref($_[0]) . "\n";
					},
				);
			$name1 = ref($p1);

			$p2->addSlot([qw(parent* promote)] => $p1);
		}
	}
}
ok( $record, <<END);
p3 before super for $name3
p2 before super for $name3
p1 before super for $name3
p1 after super for $name3
p2 after super for $name3
p3 after super for $name3
You are in MyClass::DESTROY for $name3
p2 before super for $name2
p1 before super for $name2
p1 after super for $name2
p2 after super for $name2
You are in MyClass::DESTROY for $name2
p1 before super for $name1
p1 after super for $name1
You are in MyClass::DESTROY for $name1
END


# Demonstration of the search for DESTROY taking the same path as the search
# for destroy
$record = '';
{
	my $p3 = MyClass->new(
		'destroy!' => sub {
				$record .= "p3 before super for " . ref($_[0]) . "\n";
				$_[0]->super('destroy');
				$record .= "p3 after super for " . ref($_[0]) . "\n";
			},
		);
	$name3 = ref($p3);

	{
		my $p2 = MyClass->new(
			'destroy!' => sub {
					$record .= "p2 before super for " . ref($_[0]) . "\n";
					$_[0]->super('destroy');
					$record .= "p2 after super for " . ref($_[0]) . "\n";
				},
			);
		$name2 = ref($p2);

		$p3->addSlot('parent2**' => $p2);

		{
			my $p1 = MyClass_Alt->new(
				'destroy!' => sub {
						$record .= "p1 before super for " . ref($_[0]) . "\n";
						$_[0]->super('destroy');
						$record .= "p1 after super for " . ref($_[0]) . "\n";
					},
				);
			$name1 = ref($p1);

			$p3->addSlot('parent1**' => $p1);
		}
	}
}
ok( $record, <<END);
p3 before super for $name3
p1 before super for $name3
p1 after super for $name3
p3 after super for $name3
You are in MyClass_Alt::DESTROY for $name3
p1 before super for $name1
p1 after super for $name1
You are in MyClass_Alt::DESTROY for $name1
p2 before super for $name2
p2 after super for $name2
You are in MyClass::DESTROY for $name2
END

# Demonstration of the search for DESTROY taking a different path from the
# search for destroy
$record = '';
{
	my $p3 = MyClass->new(
		'destroy!' => sub {
				$record .= "p3 before super for " . ref($_[0]) . "\n";
				$_[0]->super('destroy');
				$record .= "p3 after super for " . ref($_[0]) . "\n";
			},
		);
	$name3 = ref($p3);

	{
		my $p2 = MyClass->new(
			'destroy!' => sub {
					$record .= "p2 before super for " . ref($_[0]) . "\n";
					$_[0]->super('destroy');
					$record .= "p2 after super for " . ref($_[0]) . "\n";
				},
			);
		$name2 = ref($p2);

		$p3->addSlot('parent2**' => $p2);

		{
			my $p1 = Class::Prototyped->new(
				'destroy!' => sub {
						$record .= "p1 before super for " . ref($_[0]) . "\n";
						$_[0]->super('destroy');
						$record .= "p1 after super for " . ref($_[0]) . "\n";
					},
				);
			$name1 = ref($p1);

			$p3->addSlot('parent1**' => $p1);
		}
	}
}
ok( $record, <<END);
p3 before super for $name3
p1 before super for $name3
p1 after super for $name3
p3 after super for $name3
You are in MyClass::DESTROY for $name3
p1 before super for $name1
p1 after super for $name1
p2 before super for $name2
p2 after super for $name2
You are in MyClass::DESTROY for $name2
END


# Demonstration of a real-world dependency on the parent slot remaining viable
$record = '';
{
	my $p3;
	{
		my $p2;
		{
			my $p1;
			{
				$p1 = Class::Prototyped->new(
					name => 'p1',
					count => 0,
				);
				my $ref = ref($p1);
				$p1->addSlots(
					'new!' => sub {
						my $self = $_[0]->super('new', 'parent*' => @_);
						$self->count($self->count()+1);
						$record .= "Incremented count to " . $self->count . " using " .
								ref($self) . " from new called on " . ref($_[0]) . "\n";
						return $self;
					},
					'destroy!' => sub {
						if (ref($_[0]) eq $ref) {
							$record .= "p1::destroy called on self with a count of " .
									$_[0]->count . "\n";
						} else {
							$record .= "p1::destroy called on " . $_[0]->name() . " (" .
									ref($_[0]) . ")\n";
							$_[0]->count($_[0]->count()-1);
							$record .= "Decremented count to " . $_[0]->count . "\n";
						}
					},
				);
			}
			$name1 = ref($p1);

			$p2 = $p1->new(name => 'p2');
			$name2 = ref($p2);

			$p3 = $p2->new(name => 'p3');
			$name3 = ref($p3);

		}
	}
}
ok( $record, <<END);
Incremented count to 1 using $name2 from new called on $name1
Incremented count to 2 using $name3 from new called on $name2
p1::destroy called on p3 ($name3)
Decremented count to 1
p1::destroy called on p2 ($name2)
Decremented count to 0
p1::destroy called on self with a count of 0
END