This patch lets Test::More compare circular data structures. Get the latest version (0.47) of Test::More, apply fixes.patch to fix some issues, then apply circular.patch. Test with circular.t,
F -- Do you need someone with lots of Unix sysadmin and/or lots of OO software development experience? Go on, giz a job. My CV - http://www.fergaldaly.com/cv.html
--- More.pm.orig 2002-08-26 17:20:41.000000000 +0100 +++ More.pm 2003-03-04 02:03:29.000000000 +0000 @@ -25,7 +25,7 @@ cmp_ok skip todo todo_skip pass fail - eq_array eq_hash eq_set + eq_array eq_hash eq_set eq_deeply $TODO plan can_ok isa_ok @@ -937,7 +937,7 @@ my($this, $that, $name) = @_; my $ok; - if( !ref $this || !ref $that ) { + if( !ref $this && !ref $that ) { $ok = $Test->is_eq($this, $that, $name); } else { @@ -984,8 +984,9 @@ foreach my $idx (0..$#vals) { my $val = $vals[$idx]; $vals[$idx] = !defined $val ? 'undef' : - $val eq $DNE ? "Does not exist" - : "'$val'"; + ref $val ? $val eq $DNE ? "Does not exist" + : $val + : "'$val'" } $out .= "$vars[0] = $vals[0]\n"; @@ -995,6 +996,12 @@ return $out; } +sub eq_deeply { + my ($a1, $a2) = @_; + + local @Data_Stack = (); + return _deep_check($a1, $a2); +} =item B<eq_array> @@ -1006,7 +1013,14 @@ =cut #'# -sub eq_array { + +sub eq_array { + my ($a1, $a2) = @_; + + return UNIVERSAL::isa($a2, "ARRAY") ? eq_deeply($a1, $a2) : 0; +} + +sub _eq_array { my($a1, $a2) = @_; return 1 if $a1 eq $a2; @@ -1034,19 +1048,24 @@ # Quiet uninitialized value warnings when comparing undefs. local $^W = 0; - if( $e1 eq $e2 ) { + if( ! (ref $e1 xor ref $e2) and $e1 eq $e2 ) { $ok = 1; } else { - if( UNIVERSAL::isa($e1, 'ARRAY') and + if ( (ref $e1 and $e1 eq $DNE) or + (ref $e2 and $e2 eq $DNE) ) + { + $ok = 0; + } + elsif( UNIVERSAL::isa($e1, 'ARRAY') and UNIVERSAL::isa($e2, 'ARRAY') ) { - $ok = eq_array($e1, $e2); + $ok = _eq_array($e1, $e2); } elsif( UNIVERSAL::isa($e1, 'HASH') and UNIVERSAL::isa($e2, 'HASH') ) { - $ok = eq_hash($e1, $e2); + $ok = _eq_hash($e1, $e2); } elsif( UNIVERSAL::isa($e1, 'REF') and UNIVERSAL::isa($e2, 'REF') ) @@ -1060,6 +1079,7 @@ { push @Data_Stack, { type => 'REF', vals => [$e1, $e2] }; $ok = _deep_check($$e1, $$e2); + pop @Data_Stack if $ok; } else { push @Data_Stack, { vals => [$e1, $e2] }; @@ -1082,6 +1102,12 @@ =cut sub eq_hash { + my ($a1, $a2) = @_; + + return UNIVERSAL::isa($a2, "HASH") ? eq_deeply($a1, $a2) : 0; +} + +sub _eq_hash { my($a1, $a2) = @_; return 1 if $a1 eq $a2; @@ -1156,6 +1182,10 @@ return Test::Builder->new; } +sub set_builder { + $Test = shift; +} + =back
--- More.pm.dne 2003-03-04 02:03:29.000000000 +0000 +++ More.pm 2003-03-04 02:14:01.000000000 +0000 @@ -931,7 +931,7 @@ =cut -use vars qw(@Data_Stack); +use vars qw(@Data_Stack %Compared ); my $DNE = bless [], 'Does::Not::Exist'; sub is_deeply { my($this, $that, $name) = @_; @@ -942,6 +942,7 @@ } else { local @Data_Stack = (); + local %Compared = (); if( _deep_check($this, $that) ) { $ok = $Test->ok(1, $name); } @@ -1000,6 +1001,7 @@ my ($a1, $a2) = @_; local @Data_Stack = (); + local %Compared = (); return _deep_check($a1, $a2); } @@ -1040,6 +1042,16 @@ } sub _deep_check { + + # This handles circular references by remembering which structures we + # are already in the process of comapring so that if we need to compare + # them again after descending further into the structures, we know there + # is no point. We just assume they are equal and look for differences + # elsewhere. + + # We also gain a speed boost from this by remembering the results of + # earlier deep comparions + my($e1, $e2) = @_; my $ok = 0; @@ -1052,38 +1064,58 @@ $ok = 1; } else { - if ( (ref $e1 and $e1 eq $DNE) or - (ref $e2 and $e2 eq $DNE) ) - { - $ok = 0; - } - elsif( UNIVERSAL::isa($e1, 'ARRAY') and - UNIVERSAL::isa($e2, 'ARRAY') ) - { - $ok = _eq_array($e1, $e2); - } - elsif( UNIVERSAL::isa($e1, 'HASH') and - UNIVERSAL::isa($e2, 'HASH') ) - { - $ok = _eq_hash($e1, $e2); - } - elsif( UNIVERSAL::isa($e1, 'REF') and - UNIVERSAL::isa($e2, 'REF') ) - { - push @Data_Stack, { type => 'REF', vals => [$e1, $e2] }; - $ok = _deep_check($$e1, $$e2); - pop @Data_Stack if $ok; - } - elsif( UNIVERSAL::isa($e1, 'SCALAR') and - UNIVERSAL::isa($e2, 'SCALAR') ) - { - push @Data_Stack, { type => 'REF', vals => [$e1, $e2] }; - $ok = _deep_check($$e1, $$e2); - pop @Data_Stack if $ok; + if ( ref $e1 and ref $e2 and $Compared{$e1}->{$e2} ) { + # we've either: + + # - compared these two before, in which case we know they're + # equal already or + + # - we're in the process of comparing them and we've bumped + # into them again because one of them is a circular data + # structure, in which case we assume they're equal and go + # look for differences elsewhere + + $ok = 1; } else { - push @Data_Stack, { vals => [$e1, $e2] }; - $ok = 0; + if ( ref $e1 and ref $e2 ) { + $Compared{$e1}->{$e2} = 1; + $Compared{$e2}->{$e1} = 1; + } + + if ( (ref $e1 and $e1 eq $DNE) or + (ref $e2 and $e2 eq $DNE) ) + { + $ok = 0; + } + elsif( UNIVERSAL::isa($e1, 'ARRAY') and + UNIVERSAL::isa($e2, 'ARRAY') ) + { + $ok = _eq_array($e1, $e2); + } + elsif( UNIVERSAL::isa($e1, 'HASH') and + UNIVERSAL::isa($e2, 'HASH') ) + { + $ok = _eq_hash($e1, $e2); + } + elsif( UNIVERSAL::isa($e1, 'REF') and + UNIVERSAL::isa($e2, 'REF') ) + { + push @Data_Stack, { type => 'REF', vals => [$e1, $e2] }; + $ok = _deep_check($$e1, $$e2); + pop @Data_Stack if $ok; + } + elsif( UNIVERSAL::isa($e1, 'SCALAR') and + UNIVERSAL::isa($e2, 'SCALAR') ) + { + push @Data_Stack, { type => 'REF', vals => [$e1, $e2] }; + $ok = _deep_check($$e1, $$e2); + pop @Data_Stack if $ok; + } + else { + push @Data_Stack, { vals => [$e1, $e2] }; + $ok = 0; + } } } }
# -*-perl-*- use strict; use Test::More (tests => 10); if(1) { my $a1 = gen_layers(2); my $a2 = gen_layers(2); is_deeply($a1, $a2, "2 layers"); push(@$a1, "break"); ok(! eq_deeply($a1, $a2), "2 layers broken"); push(@$a2, "break"); is_deeply($a1, $a2, "2 layers fixed"); } if(1) { my $a1 = gen_layers(2); my $a2 = gen_layers(3); is_deeply($a1, $a2, "2 and 3 layers"); push(@$a1, "break"); ok(! eq_deeply($a1, $a2), "2 and 3 layers broken"); push(@$a2, "break"); ok(! eq_deeply($a1, $a2), "2 and 3 layers not fixed"); } if(1) { my $a1 = gen_interleave(); my $a2 = gen_interleave(); is_deeply($a1, $a2, "interleave"); } sub gen_layers { my $num = shift; my $first = ['text', gen_circle()]; $num--; my $last = $first; while ($num--) { my $next = ['text', gen_circle()]; push(@$last, $next); $last = $next; } push(@$last, $first); return $first } sub gen_circle { my $a = ['circle']; push(@$a, $a); return $a } sub gen_interleave { my $a = []; my $b = []; push(@$a, $b, $a); push(@$b, $a, $b); return $a; }