File Coverage

blib/lib/Data/Compare.pm
Criterion Covered Total %
statement 147 167 88.0
branch 72 88 81.8
condition 43 63 68.2
subroutine 16 17 94.1
pod 2 6 33.3
total 280 341 82.1


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