File Coverage

lib/Data/Compare.pm
Criterion Covered Total %
statement 145 165 87.8
branch 72 88 81.8
condition 48 69 69.5
subroutine 16 17 94.1
pod 2 6 33.3
total 283 345 82.0


line stmt bran cond sub pod time code
1             # Data::Compare - compare perl data structures
2             # Author: Fabien Tassin
3             # updated by David Cantrell
4             # Copyright 1999-2001 Fabien Tassin
5             # portions Copyright 2003 - 2013 David Cantrell
6              
7             package Data::Compare;
8              
9 14     14   203184 use strict;
  14         137  
  14         339  
10 14     14   58 use warnings;
  14         21  
  14         484  
11              
12 14     14   67 use vars qw(@ISA @EXPORT $VERSION $DEBUG %been_there);
  14         19  
  14         1047  
13 14     14   77 use Exporter;
  14         33  
  14         460  
14 14     14   65 use Carp;
  14         43  
  14         3027  
15 14     14   13872 use Clone qw(clone);
  14         32797  
  14         833  
16 14     14   159 use Scalar::Util qw(tainted);
  14         25  
  14         1431  
17 14     14   8192 use File::Find::Rule;
  14         118043  
  14         92  
18              
19             @ISA = qw(Exporter);
20             @EXPORT = qw(Compare);
21             $VERSION = 1.27;
22             $DEBUG = $ENV{PERL_DATA_COMPARE_DEBUG} || 0;
23              
24             my %handler;
25              
26 14     14   1100 use Cwd;
  14         32  
  14         23700  
27              
28             sub import {
29 25     25   373 my $cwd = getcwd();
30 25 50 33     741 register_plugins() unless(tainted getcwd() || !chdir $cwd);
31 25         19640 __PACKAGE__->export_to_level(1, @EXPORT);
32             }
33              
34             # finds and registers plugins
35             sub register_plugins {
36 26     26 0 759 foreach my $file (
37             File::Find::Rule->file()->name('*.pm')->in(
38 82         281 map { "$_/Data/Compare/Plugins" }
39 296         6809 grep { -d "$_/Data/Compare/Plugins" }
40             @INC
41             )
42             ) {
43             # all of this just to avoid loading the same plugin twice and
44             # generating a pile of warnings. Grargh!
45 82         41779 $file =~ s!.*(Data/Compare/Plugins/.*)\.pm$!$1!;
46 82         276 $file =~ s!/!::!g;
47             # ignore badly named example from earlier version, oops
48 82 50       209 next if($file eq 'Data::Compare::Plugins::Scalar-Properties');
49 82         15155 my $requires = eval "require $file";
50 82 100       299 next if($requires eq '1'); # already loaded this plugin?
51              
52             # not an arrayref? bail
53 13 50       194 if(ref($requires) ne 'ARRAY') {
54 0         0 warn("$file isn't a valid Data::Compare plugin (didn't return arrayref)\n");
55 0         0 return;
56             }
57             # coerce into arrayref of arrayrefs if necessary
58 13 50       38 if(ref((@{$requires})[0]) ne 'ARRAY') { $requires = [$requires] }
  13         75  
  0         0  
59              
60             # register all the handlers
61 13         24 foreach my $require (@{$requires}) {
  13         27  
62 26         34 my($handler, $type1, $type2, $cruft) = reverse @{$require};
  26         53  
63 26 100       58 $type2 = $type1 unless(defined($type2));
64 26         87 ($type1, $type2) = sort($type1, $type2);
65 26 50 33     219 if(!defined($type1) || ref($type1) ne '' || !defined($type2) || ref($type2) ne '') {
    50 33        
    50 33        
66 0         0 warn("$file isn't a valid Data::Compare plugin (invalid type)\n");
67             } elsif(defined($cruft)) {
68 0         0 warn("$file isn't a valid Data::Compare plugin (extra data)\n");
69             } elsif(ref($handler) ne 'CODE') {
70 0         0 warn("$file isn't a valid Data::Compare plugin (no coderef)\n");
71             } else {
72 26         96 $handler{$type1}{$type2} = $handler;
73             }
74             }
75             }
76             }
77              
78             sub new {
79 3     3 0 32 my $this = shift;
80 3   33     12 my $class = ref($this) || $this;
81 3         4 my $self = {};
82 3         3 bless $self, $class;
83 3         7 $self->{'x'} = shift;
84 3         4 $self->{'y'} = shift;
85 3         6 return $self;
86             }
87              
88             sub Cmp {
89 7     7 0 33 my $self = shift;
90              
91 7 50 66     18 croak "Usage: DataCompareObj->Cmp(x, y)" unless $#_ == 1 || $#_ == -1;
92 7   100     13 my $x = shift || $self->{'x'};
93 7   100     12 my $y = shift || $self->{'y'};
94              
95 7         9 return Compare($x, $y);
96             }
97              
98             sub Compare {
99 1072 50 66 1072 0 8146 croak "Usage: Data::Compare::Compare(x, y, [opts])\n" unless $#_ == 1 || $#_ == 2;
100              
101 1072         1298 my $x = shift;
102 1072         1187 my $y = shift;
103 1072         1211 my $opts = {};
104 1072 100       1664 if(@_) { $opts = clone(shift); }
  7         39  
105              
106 1072         1527 _Compare($x, $y, $opts);
107             }
108              
109             sub _Compare {
110 2419     2419   3596 my($x, $y, $opts) = @_;
111             my($xparent, $xpos, $yparent, $ypos) = map {
112 2419 100       3962 $opts->{$_} || ''
  9676         22258  
113             } qw(xparent xpos yparent ypos);
114              
115 2419         3319 my $rval = '';
116              
117 2419 100       3342 if(!exists($opts->{recursion_detector})) {
118 1072         3316 %been_there = ();
119 1072         1608 $opts->{recursion_detector} = 0;
120             }
121 2419         2483 $opts->{recursion_detector}++;
122              
123 2419 100       4033 warn "Yaroo! deep recursion!\n" if($opts->{recursion_detector} == 99);
124            
125 2419 100 100     11871 if(
      100        
      100        
      66        
      66        
126             (ref($x) && exists($been_there{"$x-$xpos-$xparent"}) && $been_there{"$x-$xpos-$xparent"} > 1) ||
127             (ref($y) && exists($been_there{"$y-$ypos-$yparent"}) && $been_there{"$y-$ypos-$yparent"} > 1)
128             ) {
129 6         7 $opts->{recursion_detector}--;
130 6         16 return 1; # we bail as soon as possible, so if we've *not* bailed and have got here, say we're OK and go to the next sub-structure
131             } else {
132 2413 100       4894 $been_there{"$x-$xpos-$xparent"}++ if(ref($x));
133 2413 100       4497 $been_there{"$y-$ypos-$yparent"}++ if(ref($y));
134              
135             $opts->{ignore_hash_keys} = { map {
136 4         9 ($_, 1)
137 2413 100       3794 } @{$opts->{ignore_hash_keys}} } if(ref($opts->{ignore_hash_keys}) eq 'ARRAY');
  4         5  
138              
139 2413         2771 my $refx = ref $x;
140 2413         3217 my $refy = ref $y;
141              
142 2413 50 66     11983 if(exists($handler{$refx}) && exists($handler{$refx}{$refy})) {
    50 66        
    100 66        
    100 100        
    100          
    100          
    100          
    100          
    100          
    50          
    100          
143 0         0 $rval = &{$handler{$refx}{$refy}}($x, $y, $opts);
  0         0  
144             } elsif(exists($handler{$refy}) && exists($handler{$refy}{$refx})) {
145 0         0 $rval = &{$handler{$refy}{$refx}}($x, $y, $opts);
  0         0  
146             }
147              
148             elsif(!$refx && !$refy) { # both are scalars
149 1151 100 66     2533 if(defined $x && defined $y) { # both are defined
150 1147         1529 $rval = $x eq $y;
151 4   66     20 } else { $rval = !(defined $x || defined $y); }
152             }
153             elsif ($refx ne $refy) { # not the same type
154 8         8 $rval = 0;
155             }
156             elsif (Scalar::Util::refaddr($x) == Scalar::Util::refaddr($y)) { # exactly the same reference
157 14         20 $rval = 1;
158             }
159             elsif ($refx eq 'SCALAR' || $refx eq 'REF') {
160 22         21 $rval = _Compare(${$x}, ${$y}, $opts);
  22         28  
  22         161  
161             }
162             elsif ($refx eq 'ARRAY') {
163 1127 100       1366 if ($#{$x} == $#{$y}) { # same length
  1127         1645  
  1127         1918  
164 1124         1234 my $i = -1;
165 1124         1170 $rval = 1;
166 1124         1514 for (@$x) {
167 1139         1479 $i++;
168 1139 100       1714 $rval = 0 unless _Compare($x->[$i], $y->[$i], { %{$opts}, xparent => $x, xpos => $i, yparent => $y, ypos => $i});
  1139         18460  
169             }
170             }
171             else {
172 3         7 $rval = 0;
173             }
174             }
175             elsif ($refx eq 'HASH') {
176 61         176 my @kx = grep { !$opts->{ignore_hash_keys}->{$_} } keys %$x;
  169         353  
177 61         166 my @ky = grep { !$opts->{ignore_hash_keys}->{$_} } keys %$y; # heh, KY
  173         275  
178 61         94 $rval = 1;
179 61 100       118 $rval = 0 unless scalar @kx == scalar @ky;
180              
181 61         131 for (@kx) {
182 166 100 66     314 next unless defined $x->{$_} || defined $y->{$_};
183 164 100 100     305 $rval = 0 unless defined $y->{$_} && _Compare($x->{$_}, $y->{$_}, { %{$opts}, xparent => $x, xpos => $_, yparent => $y, ypos => $_});
  159         738  
184             }
185             }
186             elsif($refx eq 'Regexp') {
187 2         5 $rval = _Compare($x.'', $y.'', $opts);
188             }
189             elsif ($refx eq 'CODE') {
190 0         0 $rval = 0;
191             }
192             elsif ($refx eq 'GLOB') {
193 1         2 $rval = 0;
194             }
195             else { # a package name (object blessed)
196 27         65 my $type = Scalar::Util::reftype($x);
197 27 100 66     69 if ($type eq 'HASH') {
    100          
    100          
    100          
    50          
198 21         48 my %x = %$x;
199 21         50 my %y = %$y;
200 21         37 $rval = _Compare(\%x, \%y, { %{$opts}, xparent => $xparent, xpos => $xpos, yparent => $yparent, ypos => $ypos});
  21         188  
201 21         69 $been_there{\%x."-$xpos-$xparent"}--; # decrement count for temp structures
202 21         59 $been_there{\%y."-$ypos-$yparent"}--;
203             }
204             elsif ($type eq 'ARRAY') {
205 2         5 my @x = @$x;
206 2         4 my @y = @$y;
207 2         9 $rval = _Compare(\@x, \@y, { %{$opts}, xparent => $xparent, xpos => $xpos, yparent => $yparent, ypos => $ypos});
  2         12  
208 2         8 $been_there{\@x."-$xpos-$xparent"}--;
209 2         5 $been_there{\@y."-$ypos-$yparent"}--;
210             }
211             elsif ($type eq 'SCALAR' || $type eq 'REF') {
212 2         2 my $x = ${$x};
  2         3  
213 2         2 my $y = ${$y};
  2         3  
214 2         3 $rval = _Compare($x, $y, $opts);
215             # $been_there{\$x}--;
216             # $been_there{\$y}--;
217             }
218             elsif ($type eq 'GLOB') {
219 1         2 $rval = 0;
220             }
221             elsif ($type eq 'CODE') {
222 1         2 $rval = 0;
223             }
224             else {
225 0         0 croak "Can't handle $type type.";
226 0         0 $rval = 0;
227             }
228             }
229             }
230 2413         2787 $opts->{recursion_detector}--;
231 2413         6520 return $rval;
232             }
233              
234             sub plugins {
235 3 100   3 1 49 return { map { (($_ eq '') ? '[scalar]' : $_, [map { $_ eq '' ? '[scalar]' : $_ } keys %{$handler{$_}}]) } keys %handler };
  4 50       13  
  4         19  
  4         11  
236             }
237              
238             sub plugins_printable {
239 0     0 1   my $r = "The following comparisons are available through plugins\n\n";
240 0           foreach my $key (sort keys %handler) {
241 0           foreach(sort keys %{$handler{$key}}) {
  0            
242 0 0         $r .= join(":\t", map { $_ eq '' ? '[scalar]' : $_ } ($key, $_))."\n";
  0            
243             }
244             }
245 0           return $r;
246             }
247              
248             1;
249              
250             =head1 NAME
251              
252             Data::Compare - compare perl data structures
253              
254             =head1 SYNOPSIS
255              
256             use Data::Compare;
257              
258             my $h1 = { 'foo' => [ 'bar', 'baz' ], 'FOO' => [ 'one', 'two' ] };
259             my $h2 = { 'foo' => [ 'bar', 'barf' ], 'FOO' => [ 'one', 'two' ] };
260             my @a1 = ('one', 'two');
261             my @a2 = ('bar', 'baz');
262             my %v = ( 'FOO', \@a1, 'foo', \@a2 );
263              
264             # simple procedural interface
265             print 'structures of $h1 and \%v are ',
266             Compare($h1, \%v) ? "" : "not ", "identical.\n";
267              
268             print 'structures of $h1 and $h2 are ',
269             Compare($h1, $h2, { ignore_hash_keys => [qw(foo)] }) ? '' : 'not ',
270             "close enough to identical.\n";
271              
272             # OO usage
273             my $c = new Data::Compare($h1, \%v);
274             print 'structures of $h1 and \%v are ',
275             $c->Cmp ? "" : "not ", "identical.\n";
276             # or
277             my $c = new Data::Compare;
278             print 'structures of $h and \%v are ',
279             $c->Cmp($h1, \%v) ? "" : "not ", "identical.\n";
280              
281             =head1 DESCRIPTION
282              
283             Compare two perl data structures recursively. Returns 0 if the
284             structures differ, else returns 1.
285              
286             A few data types are treated as special cases:
287              
288             =over 4
289              
290             =item Scalar::Properties objects
291              
292             This has been moved into a plugin, although functionality remains the
293             same as with the previous version. Full documentation is in
294             L.
295              
296             =item Compiled regular expressions, eg qr/foo/
297              
298             These are stringified before comparison, so the following will match:
299              
300             $r = qr/abc/i;
301             $s = qr/abc/i;
302             Compare($r, $s);
303              
304             and the following won't, despite them matching *exactly* the same text:
305              
306             $r = qr/abc/i;
307             $s = qr/[aA][bB][cC]/;
308             Compare($r, $s);
309              
310             Sorry, that's the best we can do.
311              
312             =item CODE and GLOB references
313              
314             These are assumed not to match unless the references are identical - ie,
315             both are references to the same thing.
316              
317             =back
318              
319             You may also customise how we compare structures by supplying options in
320             a hashref as a third parameter to the C function. This is not
321             yet available through the OO-ish interface. These options will be in
322             force for the *whole* of your comparison, so will apply to structures
323             that are lurking deep down in your data as well as at the top level, so
324             beware!
325              
326             =over 4
327              
328             =item ignore_hash_keys
329              
330             an arrayref of strings. When comparing two hashes, any keys mentioned in
331             this list will be ignored.
332              
333             =back
334              
335             =head1 CIRCULAR STRUCTURES
336              
337             Comparing a circular structure to itself returns true:
338              
339             $x = \$y;
340             $y = \$x;
341             Compare([$x, $y], [$x, $y]);
342              
343             And on a sort-of-related note, if you try to compare insanely deeply nested
344             structures, the module will spit a warning. For this to affect you, you need to go
345             around a hundred levels deep though, and if you do that you have bigger
346             problems which I can't help you with ;-)
347              
348             =head1 PLUGINS
349              
350             The module takes plug-ins so you can provide specialised routines for
351             comparing your own objects and data-types. For details see
352             L.
353              
354             Plugins are *not* available when running in "taint" mode. You may
355             also make it not load plugins by providing an empty list as the
356             argument to import() - ie, by doing this:
357              
358             use Data::Compare ();
359              
360             A couple of functions are provided to examine what goodies have been
361             made available through plugins:
362              
363             =over 4
364              
365             =item plugins
366              
367             Returns a structure (a hash ref) describing all the comparisons made
368             available through plugins.
369             This function is *not* exported, so should be called as Data::Compare::plugins().
370             It takes no parameters.
371              
372             =item plugins_printable
373              
374             Returns formatted text
375              
376             =back
377              
378             =head1 EXPORTS
379              
380             For historical reasons, the Compare() function is exported. If you
381             don't want this, then pass an empty list to import() as explained
382             under PLUGINS. If you want no export but do want plugins, then pass
383             the empty list, and then call the register_plugins class method:
384              
385             use Data::Compare ();
386             Data::Compare->register_plugins;
387              
388             or you could call it as a function if that floats your boat.
389              
390             =head1 SOURCE CODE REPOSITORY
391              
392             L
393              
394             =head1 BUGS
395              
396             Plugin support is not quite finished (see the the Github
397             L
398             for details) but is usable. The missing bits are bells and whistles rather than
399             core functionality.
400              
401             Plugins are unavailable if you can't change to the current directory. This
402             might happen if you started your process as a priveleged user and then dropped
403             priveleges. If this affects you, please supply a portable patch with tests.
404              
405             Bug reports should be made on Github or by email.
406              
407             =head1 AUTHOR
408              
409             Fabien Tassin Efta@sofaraway.orgE
410              
411             Portions by David Cantrell Edavid@cantrell.org.ukE
412              
413             =head1 COPYRIGHT and LICENCE
414              
415             Copyright (c) 1999-2001 Fabien Tassin. All rights reserved.
416             This program is free software; you can redistribute it and/or
417             modify it under the same terms as Perl itself.
418              
419             Some parts copyright 2003 - 2014 David Cantrell.
420              
421             Seeing that Fabien seems to have disappeared, David Cantrell has become
422             a co-maintainer so he can apply needed patches. The licence, of course,
423             remains the same. As the "perl licence" is "Artistic or GPL, your choice",
424             you can find them as the files ARTISTIC.txt and GPL2.txt in the
425             distribution.
426              
427             =head1 SEE ALSO
428              
429             L
430              
431             perl(1), perlref(1)
432              
433             =cut