Hi,

Andy, I know you subscribe to perl-qa so I didn't CC.
These two patches add support for detecting cycles in code
references, using PadWalker, to Devel::Cycle and Test::Memory::Cycle.

Currently this will silently stop working if PadWalker is not
installed to keep things tidy - I personally feel that this be a bit
louder, but whatever Lincoln feels is best is his own choice.

Anyway, have fun.

-- 
  Yuval Kogman <[EMAIL PROTECTED]>
http://nothingmuch.woobling.org  0xEBD27418

diff -Nur Devel-Cycle-1.04/lib/Devel/Cycle.pm 
Devel-Cycle-with_code/lib/Devel/Cycle.pm
--- Devel-Cycle-1.04/lib/Devel/Cycle.pm 2005-04-27 17:40:16.000000000 +0300
+++ Devel-Cycle-with_code/lib/Devel/Cycle.pm    2006-04-24 00:57:11.000000000 
+0300
@@ -19,6 +19,11 @@
 our $VERSION = '1.04';
 our $FORMATTING = 'roasted';
 
+BEGIN {
+       require constant;
+       constant->import( HAVE_PADWALKER => eval { require PadWalker; 1 } );
+}
+
 sub find_weakened_cycle {
   my $ref      = shift;
   my $callback = shift;
@@ -88,6 +93,15 @@
                  (@report,['HASH',$key,$current => 
$current->{$key},$inc_weak_refs?isweak($current->{$key}):()]));
     }
   }
+  elsif (HAVE_PADWALKER && $type eq 'CODE') {
+    my $closed_vars = PadWalker::closed_over( $current );
+    foreach my $varname ( sort keys %$closed_vars ) {
+        my $value = $closed_vars->{$varname};
+        next if !$inc_weak_refs && isweak($$value);
+        _find_cycle( $$value,{%$seenit},$callback,$inc_weak_refs,
+          (@report,['CODE',$varname,$current => 
$$value,$inc_weak_refs?isweak($$value):()]));
+    }
+  }
 }
 
 sub _do_report {
@@ -118,21 +132,25 @@
     return $sygil . ($sygil ? '$' : '$$'). $prefix . $shortname . $suffix if 
$type eq 'SCALAR';
     return $sygil . ($sygil ? '@' : '$') . $prefix . $shortname . $suffix  if 
$type eq 'ARRAY';
     return $sygil . ($sygil ? '%' : '$') . $prefix . $shortname . $suffix  if 
$type eq 'HASH';
+    return $sygil . ($sygil ? '&' : '$') . $prefix . $shortname . $suffix if 
$type eq 'CODE';
   }
 }
 
+# why not Scalar::Util::reftype?
 sub _get_type {
   my $thingy = shift;
   return unless ref $thingy;
   return 'SCALAR' if UNIVERSAL::isa($thingy,'SCALAR') || 
UNIVERSAL::isa($thingy,'REF');
   return 'ARRAY'  if UNIVERSAL::isa($thingy,'ARRAY');
   return 'HASH'   if UNIVERSAL::isa($thingy,'HASH');
+  return 'CODE'   if UNIVERSAL::isa($thingy,'CODE');
 }
 
 sub _format_index {
   my ($type,$index) = @_;
   return "->[$index]" if $type eq 'ARRAY';
   return "->{'$index'}" if $type eq 'HASH';
+  return " variable $index" if $type eq 'CODE';
   return;
 }
 
diff -Nur Test-Memory-Cycle-1.02/Cycle.pm Test-Memory-Cycle-with_code/Cycle.pm
--- Test-Memory-Cycle-1.02/Cycle.pm     2005-05-17 19:02:39.000000000 +0300
+++ Test-Memory-Cycle-with_code/Cycle.pm        2006-04-24 00:54:16.000000000 
+0300
@@ -98,6 +98,7 @@
             $str = sprintf("    %s => %s",$refdisp,$valuedisp)               
if $type eq 'SCALAR';
             $str = sprintf("    %s => %s","${refdisp}->[$index]",$valuedisp) 
if $type eq 'ARRAY';
             $str = sprintf("    %s => %s","${refdisp}->{$index}",$valuedisp) 
if $type eq 'HASH';
+            $str = sprintf("    closure %s => %s","${refdisp}, 
$index",$valuedisp) if $type eq 'CODE';
 
             push( @diags, $str );
         }
@@ -213,6 +214,7 @@
         $sigil = '%' if $sigil eq "HASH ";
         $sigil = '@' if $sigil eq "ARRAY ";
         $sigil = '$' if $sigil eq "REF ";
+        $sigil = '&' if $sigil eq "CODE ";
         $refdisp = $shortnames{ $refstr } = $sigil . $new_shortname++;
     }
 
diff -Nur Test-Memory-Cycle-1.02/lib/Test/Memory/Cycle.pm 
Test-Memory-Cycle-with_code/lib/Test/Memory/Cycle.pm
--- Test-Memory-Cycle-1.02/lib/Test/Memory/Cycle.pm     1970-01-01 
02:00:00.000000000 +0200
+++ Test-Memory-Cycle-with_code/lib/Test/Memory/Cycle.pm        2006-04-24 
00:54:16.000000000 +0300
@@ -0,0 +1,241 @@
+package Test::Memory::Cycle;
+
+=head1 NAME
+
+Test::Memory::Cycle - Check for memory leaks and circular memory references
+
+=head1 VERSION
+
+Version 1.02
+
+=cut
+
+our $VERSION = "1.02";
+
+=head1 SYNOPSIS
+
+Perl's garbage collection has one big problem: Circular references
+can't get cleaned up.  A circular reference can be as simple as two
+objects that refer to each other:
+
+    my $mom = {
+        name => "Marilyn Lester",
+    };
+
+    my $me = {
+        name => "Andy Lester",
+        mother => $mom,
+    };
+    $mom->{son} = $me;
+
+C<Test::Memory::Cycle> is built on top of C<Devel::Cycle> to give
+you an easy way to check for these circular references.
+
+    use Test::Memory::Cycle;
+
+    my $object = new MyObject;
+    # Do stuff with the object.
+    memory_cycle_ok( $object );
+
+You can also use C<memory_cycle_exists()> to make sure that you have a
+cycle where you expect to have one.
+
+=cut
+
+use strict;
+use warnings;
+
+use Devel::Cycle qw( find_cycle find_weakened_cycle );
+use Test::Builder;
+
+my $Test = Test::Builder->new;
+
+sub import {
+    my $self = shift;
+    my $caller = caller;
+    no strict 'refs';
+    *{$caller.'::memory_cycle_ok'}              = \&memory_cycle_ok;
+    *{$caller.'::memory_cycle_exists'}          = \&memory_cycle_exists;
+
+    *{$caller.'::weakened_memory_cycle_ok'}     = \&weakened_memory_cycle_ok;
+    *{$caller.'::weakened_memory_cycle_exists'} = 
\&weakened_memory_cycle_exists;
+    *{$caller.'::memory_cycle_exists'}          = \&memory_cycle_exists;
+
+    *{$caller.'::weakened_memory_cycle_ok'}     = \&weakened_memory_cycle_ok;
+    *{$caller.'::weakened_memory_cycle_exists'} = 
\&weakened_memory_cycle_exists;
+
+    $Test->exported_to($caller);
+    $Test->plan(@_);
+}
+
+=head1 FUNCTIONS
+
+=head2 C<memory_cycle_ok( I<$object>, I<$msg> )>
+
+Checks that I<$object> doesn't have any circular memory references.
+
+=cut
+
+sub memory_cycle_ok {
+    my $ref = shift;
+    my $msg = shift;
+
+    my $cycle_no = 0;
+    my @diags;
+
+    # Callback function that is called once for each memory cycle found.
+    my $callback = sub {
+        my $path = shift;
+        $cycle_no++;
+        push( @diags, "Cycle #$cycle_no" );
+        foreach (@$path) {
+            my ($type,$index,$ref,$value) = @$_;
+
+            my $str = "Unknown! This should never happen!";
+            my $refdisp = _ref_shortname( $ref );
+            my $valuedisp = _ref_shortname( $value );
+
+            $str = sprintf("    %s => %s",$refdisp,$valuedisp)               
if $type eq 'SCALAR';
+            $str = sprintf("    %s => %s","${refdisp}->[$index]",$valuedisp) 
if $type eq 'ARRAY';
+            $str = sprintf("    %s => %s","${refdisp}->{$index}",$valuedisp) 
if $type eq 'HASH';
+            $str = sprintf("    closure %s => %s","${refdisp}, 
$index",$valuedisp) if $type eq 'CODE';
+
+            push( @diags, $str );
+        }
+    };
+
+    find_cycle( $ref, $callback );
+    my $ok = !$cycle_no;
+    $Test->ok( $ok, $msg );
+    $Test->diag( join( "\n", @diags, "" ) ) unless $ok;
+
+    return $ok;
+} # memory_cycle_ok
+
+=head2 C<memory_cycle_exists( I<$object>, I<$msg> )>
+
+Checks that I<$object> B<does> have any circular memory references.
+
+=cut
+
+sub memory_cycle_exists {
+    my $ref = shift;
+    my $msg = shift;
+
+    my $cycle_no = 0;
+
+    # Callback function that is called once for each memory cycle found.
+    my $callback = sub { $cycle_no++ };
+
+    find_cycle( $ref, $callback );
+    my $ok = $cycle_no;
+    $Test->ok( $ok, $msg );
+
+    return $ok;
+} # memory_cycle_exists
+
+=head2 C<weakened_memory_cycle_ok( I<$object>, I<$msg> )>
+
+Checks that I<$object> doesn't have any circular memory references, but unlike 
+C<memory_cycle_ok> this will also check for weakened cycles produced with 
+Scalar::Util's C<weaken>.
+
+=cut
+
+sub weakened_memory_cycle_ok {
+    my $ref = shift;
+    my $msg = shift;
+
+    my $cycle_no = 0;
+    my @diags;
+
+    # Callback function that is called once for each memory cycle found.
+    my $callback = sub {
+        my $path = shift;
+        $cycle_no++;
+        push( @diags, "Cycle #$cycle_no" );
+        foreach (@$path) {
+            my ($type,$index,$ref,$value,$is_weakened) = @$_;
+
+            my $str = "Unknown! This should never happen!";
+            my $refdisp = _ref_shortname( $ref );
+            my $valuedisp = _ref_shortname( $value );
+
+            $str = sprintf("    %s => %s",($is_weakened ? 
'w->':'').$refdisp,$valuedisp)               if $type eq 'SCALAR';
+            $str = sprintf("    %s => %s",($is_weakened ? 
'w->':'')."${refdisp}->[$index]",$valuedisp) if $type eq 'ARRAY';
+            $str = sprintf("    %s => %s",($is_weakened ? 
'w->':'')."${refdisp}->{$index}",$valuedisp) if $type eq 'HASH';
+
+            push( @diags, $str );
+        }
+    };
+
+    find_weakened_cycle( $ref, $callback );
+    my $ok = !$cycle_no;
+    $Test->ok( $ok, $msg );
+    $Test->diag( join( "\n", @diags, "" ) ) unless $ok;
+
+    return $ok;
+} # weakened_memory_cycle_ok
+
+=head2 C<weakened_memory_cycle_exists( I<$object>, I<$msg> )>
+
+Checks that I<$object> B<does> have any circular memory references, but unlike 
+C<memory_cycle_exists> this will also check for weakened cycles produced with 
+Scalar::Util's C<weaken>.
+
+=cut
+
+sub weakened_memory_cycle_exists {
+    my $ref = shift;
+    my $msg = shift;
+
+    my $cycle_no = 0;
+
+    # Callback function that is called once for each memory cycle found.
+    my $callback = sub { $cycle_no++ };
+
+    find_weakened_cycle( $ref, $callback );
+    my $ok = $cycle_no;
+    $Test->ok( $ok, $msg );
+
+    return $ok;
+} # weakened_memory_cycle_exists
+
+
+my %shortnames;
+my $new_shortname = "A";
+
+sub _ref_shortname {
+    my $ref = shift;
+    my $refstr = "$ref";
+    my $refdisp = $shortnames{ $refstr };
+    if ( !$refdisp ) {
+        my $sigil = ref($ref) . " ";
+        $sigil = '%' if $sigil eq "HASH ";
+        $sigil = '@' if $sigil eq "ARRAY ";
+        $sigil = '$' if $sigil eq "REF ";
+        $sigil = '&' if $sigil eq "CODE ";
+        $refdisp = $shortnames{ $refstr } = $sigil . $new_shortname++;
+    }
+
+    return $refdisp;
+}
+
+=head1 AUTHOR
+
+Written by Andy Lester, C<< <andy @ petdance.com> >>.
+
+=head1 ACKNOWLEDGEMENTS
+
+Thanks to the contributions of Stevan Little, and to Lincoln Stein for writing 
Devel::Cycle.
+
+=head1 COPYRIGHT
+
+Copyright 2005, Andy Lester, All Rights Reserved.
+
+You may use, modify, and distribute this package under the
+same terms as Perl itself.
+
+=cut
+
+1;
diff -Nur Test-Memory-Cycle-1.02/t/family-code.t 
Test-Memory-Cycle-with_code/t/family-code.t
--- Test-Memory-Cycle-1.02/t/family-code.t      1970-01-01 02:00:00.000000000 
+0200
+++ Test-Memory-Cycle-with_code/t/family-code.t 2006-04-24 00:55:14.000000000 
+0300
@@ -0,0 +1,34 @@
+#!perl -T
+
+use strict;
+use warnings FATAL => 'all';
+
+use Scalar::Util qw( weaken );
+
+use Test::More tests => 4;
+#use Test::Builder::Tester; # not used yet
+
+# use ok "Test::Memory::Cycle";
+BEGIN {
+    use_ok( 'Test::Memory::Cycle' );
+}
+
+my $code_refs_parent = { };
+$code_refs_parent->{child} = sub { $code_refs_parent };
+
+my $code_refs_self;
+$code_refs_self = sub { $code_refs_self };
+
+my $code_refs_parent_weak = { };
+$code_refs_parent_weak->{child} = do {
+    my $weak_parent = $code_refs_parent_weak;
+    weaken( $weak_parent );
+    sub { $weak_parent };
+};
+
+memory_cycle_exists( $code_refs_parent, "code refs referencing containers" );
+memory_cycle_exists( $code_refs_self, "code refs referencing themselves" );
+memory_cycle_ok( $code_refs_parent_weak, "code refs with weak refs to 
containers" );
+
+# the output can be tested later if someone cares
+

Attachment: pgpwVvnKjcZCw.pgp
Description: PGP signature

Reply via email to