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   272278 use strict;
  15         121  
  15         467  
8 15     15   72 use warnings;
  15         24  
  15         603  
9              
10 15     15   85 use vars qw(@ISA @EXPORT $VERSION $DEBUG %been_there);
  15         24  
  15         1245  
11 15     15   152 use Exporter;
  15         43  
  15         537  
12 15     15   143 use Carp;
  15         36  
  15         1194  
13 15     15   6152 use Clone qw(clone);
  15         33936  
  15         902  
14 15     15   101 use Scalar::Util qw(tainted);
  15         30  
  15         1439  
15 15     15   7211 use File::Find::Rule;
  15         117095  
  15         105  
16              
17             @ISA = qw(Exporter);
18             @EXPORT = qw(Compare);
19             $VERSION = 1.28;
20             $DEBUG = $ENV{PERL_DATA_COMPARE_DEBUG} || 0;
21              
22             my %handler;
23              
24 15     15   1285 use Cwd;
  15         37  
  15         26184  
25              
26             sub import {
27 27     27   353 my $cwd = getcwd();
28 27 50 33     763 register_plugins() unless(tainted getcwd() || !chdir $cwd);
29 27         22853 __PACKAGE__->export_to_level(1, @EXPORT);
30             }
31              
32             # finds and registers plugins
33             sub register_plugins {
34 28     28 0 752 foreach my $file (
35             File::Find::Rule->file()->name('*.pm')->in(
36 88         336 map { "$_/Data/Compare/Plugins" }
37 318         7443 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         43629 $file =~ s!.*(Data/Compare/Plugins/.*)\.pm$!$1!;
44 88         312 $file =~ s!/!::!g;
45             # ignore badly named example from earlier version, oops
46 88 50       212 next if($file eq 'Data::Compare::Plugins::Scalar-Properties');
47 88         3860 my $requires = eval "require $file";
48 88 100       380 next if($requires eq '1'); # already loaded this plugin?
49              
50             # not an arrayref? bail
51 14 50       117 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       84 if(ref((@{$requires})[0]) ne 'ARRAY') { $requires = [$requires] }
  14         82  
  0         0  
57              
58             # register all the handlers
59 14         31 foreach my $require (@{$requires}) {
  14         33  
60 28         40 my($handler, $type1, $type2, $cruft) = reverse @{$require};
  28         68  
61 28 100       70 $type2 = $type1 unless(defined($type2));
62 28         86 ($type1, $type2) = sort($type1, $type2);
63 28 50 33     261 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         104 $handler{$type1}{$type2} = $handler;
71             }
72             }
73             }
74             }
75              
76             sub new {
77 3     3 0 33 my $this = shift;
78 3   33     10 my $class = ref($this) || $this;
79 3         4 my $self = {};
80 3         3 bless $self, $class;
81 3         9 $self->{'x'} = shift;
82 3         3 $self->{'y'} = shift;
83 3         6 return $self;
84             }
85              
86             sub Cmp {
87 7     7 0 30 my $self = shift;
88              
89 7 50 66     18 croak "Usage: DataCompareObj->Cmp(x, y)" unless $#_ == 1 || $#_ == -1;
90 7   100     12 my $x = shift || $self->{'x'};
91 7   100     12 my $y = shift || $self->{'y'};
92              
93 7         12 return Compare($x, $y);
94             }
95              
96             sub Compare {
97 1073 50 66 1073 0 7717 croak "Usage: Data::Compare::Compare(x, y, [opts])\n" unless $#_ == 1 || $#_ == 2;
98              
99 1073         1516 my $x = shift;
100 1073         1268 my $y = shift;
101 1073         1372 my $opts = {};
102 1073 100       1898 if(@_) { $opts = clone(shift); }
  7         43  
103              
104 1073         1758 _Compare($x, $y, $opts);
105             }
106              
107             sub _Compare {
108 2423     2423   3970 my($x, $y, $opts) = @_;
109             my($xparent, $xpos, $yparent, $ypos) = map {
110 2423 100       3529 $opts->{$_} || ''
  9692         23849  
111             } qw(xparent xpos yparent ypos);
112              
113 2423         4071 my $rval = '';
114              
115 2423 100       4208 if(!exists($opts->{recursion_detector})) {
116 1073         2034 %been_there = ();
117 1073         1713 $opts->{recursion_detector} = 0;
118             }
119 2423         3089 $opts->{recursion_detector}++;
120              
121 2423 100       4204 warn "Yaroo! deep recursion!\n" if($opts->{recursion_detector} == 99);
122              
123 2423 100 100     13007 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         8 $opts->{recursion_detector}--;
128 6         20 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 2417 100       5944 $been_there{"$x-$xpos-$xparent"}++ if(ref($x));
131 2417 100       5183 $been_there{"$y-$ypos-$yparent"}++ if(ref($y));
132              
133             $opts->{ignore_hash_keys} = { map {
134 4         18 ($_, 1)
135 2417 100       4377 } @{$opts->{ignore_hash_keys}} } if(ref($opts->{ignore_hash_keys}) eq 'ARRAY');
  4         7  
136              
137 2417         3333 my $refx = ref $x;
138 2417         3017 my $refy = ref $y;
139              
140 2417 50 66     13482 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 1153 100 66     2780 if(defined $x && defined $y) { # both are defined
148 1147         1815 $rval = $x eq $y;
149 6   66     25 } else { $rval = !(defined $x || defined $y); }
150             }
151             elsif ($refx ne $refy) { # not the same type
152 8         22 $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         27 $rval = _Compare(${$x}, ${$y}, $opts);
  22         28  
  22         61  
159             }
160             elsif ($refx eq 'ARRAY') {
161 1127 100       1565 if ($#{$x} == $#{$y}) { # same length
  1127         1518  
  1127         1708  
162 1124         1392 my $i = -1;
163 1124         1350 $rval = 1;
164 1124         1678 for (@$x) {
165 1139         1429 $i++;
166 1139 100       1629 $rval = 0 unless _Compare($x->[$i], $y->[$i], { %{$opts}, xparent => $x, xpos => $i, yparent => $y, ypos => $i});
  1139         6074  
167             }
168             }
169             else {
170 3         8 $rval = 0;
171             }
172             }
173             elsif ($refx eq 'HASH') {
174 63         241 my @kx = grep { !$opts->{ignore_hash_keys}->{$_} } keys %$x;
  171         382  
175 63         156 my @ky = grep { !$opts->{ignore_hash_keys}->{$_} } keys %$y; # heh, KY
  175         343  
176 63         106 $rval = 1;
177 63 100       149 $rval = 0 unless scalar @kx == scalar @ky;
178              
179 63         120 for (@kx) {
180 168 100       312 if(!exists($y->{$_})) {
181 6         9 $rval = 0;
182 6         14 last;
183             }
184 162 100       228 $rval = 0 unless _Compare($x->{$_}, $y->{$_}, { %{$opts}, xparent => $x, xpos => $_, yparent => $y, ypos => $_});
  162         931  
185             }
186             }
187             elsif($refx eq 'Regexp') {
188 2         8 $rval = _Compare($x.'', $y.'', $opts);
189             }
190             elsif ($refx eq 'CODE') {
191 0         0 $rval = 0;
192             }
193             elsif ($refx eq 'GLOB') {
194 1         12 $rval = 0;
195             }
196             else { # a package name (object blessed)
197 27         68 my $type = Scalar::Util::reftype($x);
198 27 100 66     70 if ($type eq 'HASH') {
    100          
    100          
    100          
    50          
199 21         63 my %x = %$x;
200 21         77 my %y = %$y;
201 21         40 $rval = _Compare(\%x, \%y, { %{$opts}, xparent => $xparent, xpos => $xpos, yparent => $yparent, ypos => $ypos});
  21         217  
202 21         78 $been_there{\%x."-$xpos-$xparent"}--; # decrement count for temp structures
203 21         65 $been_there{\%y."-$ypos-$yparent"}--;
204             }
205             elsif ($type eq 'ARRAY') {
206 2         5 my @x = @$x;
207 2         4 my @y = @$y;
208 2         4 $rval = _Compare(\@x, \@y, { %{$opts}, xparent => $xparent, xpos => $xpos, yparent => $yparent, ypos => $ypos});
  2         8  
209 2         9 $been_there{\@x."-$xpos-$xparent"}--;
210 2         8 $been_there{\@y."-$ypos-$yparent"}--;
211             }
212             elsif ($type eq 'SCALAR' || $type eq 'REF') {
213 2         3 my $x = ${$x};
  2         3  
214 2         53 my $y = ${$y};
  2         5  
215 2         5 $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         3 $rval = 0;
224             }
225             else {
226 0         0 croak "Can't handle $type type.";
227 0         0 $rval = 0;
228             }
229             }
230             }
231 2417         3234 $opts->{recursion_detector}--;
232 2417         7835 return $rval;
233             }
234              
235             sub plugins {
236 3 100   3 1 52 return { map { (($_ eq '') ? '[scalar]' : $_, [map { $_ eq '' ? '[scalar]' : $_ } keys %{$handler{$_}}]) } keys %handler };
  4 50       12  
  4         20  
  4         10  
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