File Coverage

blib/lib/Test/BinaryData.pm
Criterion Covered Total %
statement 73 74 98.6
branch 20 26 76.9
condition 12 16 75.0
subroutine 8 8 100.0
pod 1 1 100.0
total 114 125 91.2


line stmt bran cond sub pod time code
1 1     1   65908 use strict;
  1         11  
  1         25  
2 1     1   4 use warnings;
  1         1  
  1         38  
3             package Test::BinaryData 0.015;
4             # ABSTRACT: compare two things, give hex dumps if they differ
5              
6 1     1   27 use 5.006;
  1         4  
7              
8             #pod =head1 SYNOPSIS
9             #pod
10             #pod use Test::BinaryData;
11             #pod
12             #pod my $computed_data = do_something_complicated;
13             #pod my $expected_data = read_file('correct.data');
14             #pod
15             #pod is_binary(
16             #pod $computed_data,
17             #pod $expected_data,
18             #pod "basic data computation",
19             #pod );
20             #pod
21             #pod =head1 DESCRIPTION
22             #pod
23             #pod Sometimes using Test::More's C test isn't good enough. Its diagnostics may
24             #pod make it easy to miss differences between strings.
25             #pod
26             #pod For example, given two strings which differ only in their line endings, you can
27             #pod end up with diagnostic output like this:
28             #pod
29             #pod not ok 1
30             #pod # Failed test in demo.t at line 8.
31             #pod # got: 'foo
32             #pod # bar
33             #pod # '
34             #pod # expected: 'foo
35             #pod # bar
36             #pod # '
37             #pod
38             #pod That's not very helpful, except to tell you that the alphanumeric characters
39             #pod seem to be in the right place. By using C instead of C, this
40             #pod output would be generated instead:
41             #pod
42             #pod not ok 2
43             #pod # Failed test in demo.t at line 10.
44             #pod # have (hex) have want (hex) want
45             #pod # 666f6f0a6261720a---- foo.bar. ! 666f6f0d0a6261720d0a foo..bar..
46             #pod
47             #pod The "!" tells us that the lines differ, and we can quickly scan the bytes that
48             #pod make up the line to see which differ.
49             #pod
50             #pod When comparing very long strings, we can stop after we've seen a few
51             #pod differences. Here, we'll just look for two:
52             #pod
53             #pod # have (hex) have want (hex) want
54             #pod # 416c6c20435220616e64 All CR and = 416c6c20435220616e64 All CR and
55             #pod # 206e6f204c46206d616b no LF mak = 206e6f204c46206d616b no LF mak
56             #pod # 6573204d616320612064 es Mac a d = 6573204d616320612064 es Mac a d
57             #pod # 756c6c20626f792e0d41 ull boy..A = 756c6c20626f792e0d41 ull boy..A
58             #pod # 6c6c20435220616e6420 ll CR and = 6c6c20435220616e6420 ll CR and
59             #pod # 6e6f204c46206d616b65 no LF make = 6e6f204c46206d616b65 no LF make
60             #pod # 73204d61632061206475 s Mac a du = 73204d61632061206475 s Mac a du
61             #pod # 6c6c20626f792e0d416c ll boy..Al ! 6c6c20626f792e0a416c ll boy..Al
62             #pod # 6c20435220616e64206e l CR and n = 6c20435220616e64206e l CR and n
63             #pod # 6f204c46206d616b6573 o LF makes = 6f204c46206d616b6573 o LF makes
64             #pod # 204d616320612064756c Mac a dul = 204d616320612064756c Mac a dul
65             #pod # 6c20626f792e0d416c6c l boy..All ! 6c20626f792e0a416c6c l boy..All
66             #pod # 20435220616e64206e6f CR and no = 20435220616e64206e6f CR and no
67             #pod # ...
68             #pod
69             #pod =head1 WARNING
70             #pod
71             #pod This library is for comparing B data. That is, B.
72             #pod Often, in Perl 5, it is not clear whether a scalar contains a byte string or a
73             #pod character strings. You should use this library for comparing byte strings
74             #pod only. If either the "have" or "want" values contain wide characters -- that is,
75             #pod characters that won't fit in one byte -- then the test will fail.
76             #pod
77             #pod =cut
78              
79 1     1   5 use Carp ();
  1         2  
  1         13  
80 1     1   5 use Test::Builder;
  1         1  
  1         737  
81             require Exporter;
82             @Test::BinaryData::ISA = qw(Exporter);
83             @Test::BinaryData::EXPORT = qw(is_binary);
84              
85             sub import {
86 1     1   5 my($self) = shift;
87 1         2 my $pack = caller;
88              
89 1         4 my $Test = Test::Builder->new;
90              
91 1         7 $Test->exported_to($pack);
92 1 50       25 $Test->plan(@_) if @_;
93              
94 1         108 $self->export_to_level(1, $self, @Test::BinaryData::EXPORT);
95             }
96              
97             #pod =func is_binary
98             #pod
99             #pod is_binary($have, $want, $comment, \%arg);
100             #pod
101             #pod This test behaves like Test::More's C test, but if the given data are not
102             #pod string equal, the diagnostics emits four columns, describing the strings in
103             #pod parallel, showing a simplified ASCII representation and a hexadecimal dump.
104             #pod
105             #pod If C<$want> is an arrayref, it's treated as a sequence of strings giving hex
106             #pod values for expected bytes. For example, this is a passing test:
107             #pod
108             #pod is_binary(
109             #pod "Mumblefrotz",
110             #pod [ qw(4d75 6d62 6c65 6672 6f74 7a0a) ],
111             #pod );
112             #pod
113             #pod Notice that each string in the sequence is broken into two-character pieces.
114             #pod This makes this interface accept the kind of dumps produced by F or
115             #pod Test::BinaryData itself.
116             #pod
117             #pod Between the got and expected data for each line, a "=" or "!" indicates whether
118             #pod the chunks are identical or different.
119             #pod
120             #pod The C<$comment> and C<%arg> arguments are optional. Valid arguments are:
121             #pod
122             #pod columns - the number of screen columns available
123             #pod if the COLUMNS environment variable is an positive integer, then
124             #pod COLUMNS - is used; otherwise, the default is 79
125             #pod
126             #pod max_diffs - if given, this is the maximum number of differing lines that will
127             #pod be compared; if output would have been given beyond this line,
128             #pod it will be replaced with an elipsis ("...")
129             #pod
130             #pod =cut
131              
132             sub _widths {
133 10     10   20 my ($total) = @_;
134              
135 10         15 $total = $total
136             - 2 # the "# " that begins each diagnostics line
137             - 3 # the " ! " or " = " line between got / expected
138             - 2 # the space between hex/ascii representations
139             ;
140              
141 10         23 my $sixth = int($total / 6);
142 10         21 return ($sixth * 2, $sixth);
143             }
144              
145             sub is_binary {
146 10     10 1 22506 my ($have, $want, $comment, $arg) = @_;
147              
148 10         30 my $Test = Test::Builder->new;
149              
150 10   100     60 $arg ||= {};
151              
152 10 50       25 unless (defined $arg->{columns}) {
153 10 50 50     79 if (($ENV{COLUMNS}||'') =~ /\A\d+\z/ and $ENV{COLUMNS} > 0) {
      33        
154 10         23 $arg->{columns} = $ENV{COLUMNS} - 1;
155             } else {
156 0         0 $arg->{columns} = 79;
157             }
158             }
159              
160 10 50       26 Carp::croak 'minimum columns is 44' if $arg->{columns} < 44;
161              
162 10         22 my ($hw, $aw) = _widths($arg->{columns});
163              
164 10 100       36 if (ref $want) {
165 1         4 $want = join q{}, map { chr hex } map { unpack "(a2)*", $_ } @$want;
  12         21  
  6         16  
166             }
167              
168 10         198 my $have_is_wide = grep { ord > 255 } split //, $have;
  1523         1842  
169 10         173 my $want_is_wide = grep { ord > 255 } split //, $want;
  1553         1854  
170              
171 10 100 66     86 if ($have_is_wide or $want_is_wide) {
172 1         7 $Test->ok(0, $comment);
173              
174 1 50       317 $Test->diag("value for 'have' contains wide bytes") if $have_is_wide;
175 1 50       113 $Test->diag("value for 'want' contains wide bytes") if $want_is_wide;
176              
177 1         104 return;
178             }
179              
180 9 100       20 if ($have eq $want) {
181 2         15 return $Test->ok(1, $comment);
182             }
183              
184 7         40 $Test->ok(0, $comment);
185              
186 7         2296 my $max_length = (sort map { length($_) } $have, $want)[1];
  14         40  
187              
188             $Test->diag(
189             sprintf "%-${hw}s %-${aw}s %-${hw}s %-${aw}s",
190 7         27 map {; "$_ (hex)", "$_" } qw(have want)
  14         74  
191             );
192              
193 7         853 my $seen_diffs = 0;
194 7         22 CHUNK: for (my $pos = 0; $pos < $max_length; $pos += $aw) {
195 80 100 100     167 if ($arg->{max_diffs} and $seen_diffs == $arg->{max_diffs}) {
196 2         10 $Test->diag("...");
197 2         204 last CHUNK;
198             }
199              
200 78         140 my $g_substr = substr($have, $pos, $aw);
201 78         112 my $e_substr = substr($want, $pos, $aw);
202              
203 78         99 my $eq = $g_substr eq $e_substr;
204              
205             my $g_hex =
206             join q{},
207 78         145 map { sprintf '%02x', ord(substr($g_substr, $_, 1)) }
  891         1586  
208             0 .. length($g_substr) - 1;
209              
210             my $e_hex =
211             join q{},
212 78         172 map { sprintf '%02x', ord(substr($e_substr, $_, 1)) }
  907         1415  
213             0 .. length($e_substr) - 1;
214              
215 78         188 for my $str ($g_substr, $e_substr) {
216 156         220 for my $pos (0 .. length($str) - 1) {
217 1798         2027 my $c = substr($str, $pos, 1);
218 1798 100 100     3821 substr($str, $pos, 1, q{.}) if ord($c) < 0x20 or ord($c) > 0x7e;
219             }
220             }
221              
222 78         223 $_ = sprintf "%-${aw}s", $_ for ($g_substr, $e_substr);
223 78         159 $_ .= q{-} x ($hw - length($_)) for ($g_hex, $e_hex);
224              
225 78 100       405 $Test->diag(
226             "$g_hex $g_substr",
227             ($eq ? q{ = } : q{ ! }),
228             "$e_hex $e_substr"
229             );
230              
231 78 100       8522 $seen_diffs++ unless $eq;
232             }
233              
234 7         23 return;
235             }
236              
237             #pod =head1 TODO
238             #pod
239             #pod =begin :list
240             #pod
241             #pod * optional position markers
242             #pod
243             #pod have (hex) have want (hex) want
244             #pod 00 46726f6d206d6169 From mai = 46726f6d206d6169 From mai
245             #pod 08 3130353239406c6f 10529@lo = 3130353239406c6f 10529@lo
246             #pod 16 63616c686f737420 calhost = 63616c686f737420 calhost
247             #pod 24 5765642044656320 Wed Dec = 5765642044656320 Wed Dec
248             #pod 32 31382031323a3037 18 12:07 = 31382031323a3037 18 12:07
249             #pod 40 3a35352032303032 :55 2002 = 3a35352032303032 :55 2002
250             #pod 48 0a52656365697665 .Receive ! 0d0a526563656976 ..Receiv
251             #pod
252             #pod * investigate probably bugs with wide chars, multibyte strings
253             #pod
254             #pod I wrote this primarily for detecting CRLF problems, but it's also very useful
255             #pod for dealing with encoded strings.
256             #pod
257             #pod =end :list
258             #pod
259             #pod =cut
260              
261             1;
262              
263             __END__