File Coverage

blib/lib/Test2/Compare/Delta.pm
Criterion Covered Total %
statement 180 181 99.4
branch 101 104 97.1
condition 42 46 91.3
subroutine 28 28 100.0
pod 13 15 86.6
total 364 374 97.3


line stmt bran cond sub pod time code
1             package Test2::Compare::Delta;
2 169     169   1191 use strict;
  169         316  
  169         4833  
3 169     169   1107 use warnings;
  169         362  
  169         8656  
4              
5             our $VERSION = '0.000155';
6              
7 169     169   1147 use Test2::Util::HashBase qw{verified id got chk children dne exception note};
  169         452  
  169         1635  
8              
9 169     169   149488 use Test2::EventFacet::Info::Table;
  169         103653  
  169         4181  
10              
11 169     169   66862 use Test2::Util::Table();
  169         583  
  169         5767  
12 169     169   1076 use Test2::API qw/context/;
  169         343  
  169         10993  
13              
14 169     169   1065 use Test2::Util::Ref qw/render_ref rtype/;
  169         312  
  169         8667  
15 169     169   1107 use Carp qw/croak/;
  169         320  
  169         8518  
16              
17             # 'CHECK' constant would not work, but I like exposing 'check()' to people
18             # using this class.
19             BEGIN {
20 169     169   950 no warnings 'once';
  169         458  
  169         9472  
21 169     169   769 *check = \&chk;
22 169         316126 *set_check = \&set_chk;
23             }
24              
25             my @COLUMN_ORDER = qw/PATH GLNs GOT OP CHECK CLNs/;
26             my %COLUMNS = (
27             GOT => {name => 'GOT', value => sub { $_[0]->render_got }, no_collapse => 1},
28             CHECK => {name => 'CHECK', value => sub { $_[0]->render_check }, no_collapse => 1},
29             OP => {name => 'OP', value => sub { $_[0]->table_op } },
30             PATH => {name => 'PATH', value => sub { $_[1] } },
31              
32             'GLNs' => {name => 'GLNs', alias => 'LNs', value => sub { $_[0]->table_got_lines } },
33             'CLNs' => {name => 'CLNs', alias => 'LNs', value => sub { $_[0]->table_check_lines }},
34             );
35             {
36             my $i = 0;
37             $COLUMNS{$_}->{id} = $i++ for @COLUMN_ORDER;
38             }
39              
40             sub remove_column {
41 5     5 1 59 my $class = shift;
42 5         7 my $header = shift;
43 5         13 @COLUMN_ORDER = grep { $_ ne $header } @COLUMN_ORDER;
  33         70  
44 5 100       42 delete $COLUMNS{$header} ? 1 : 0;
45             }
46              
47             sub add_column {
48 8     8 1 377 my $class = shift;
49 8         15 my $name = shift;
50              
51 8 100       194 croak "Column name is required"
52             unless $name;
53              
54             croak "Column '$name' is already defined"
55 7 100       133 if $COLUMNS{$name};
56              
57 6         11 my %params;
58 6 100       19 if (@_ == 1) {
59 4         15 %params = (value => @_, name => $name);
60             }
61             else {
62 2         20 %params = (@_, name => $name);
63             }
64              
65 6         15 my $value = $params{value};
66              
67 6 100       100 croak "You must specify a 'value' callback"
68             unless $value;
69              
70 5 100       16 croak "'value' callback must be a CODE reference"
71             unless rtype($value) eq 'CODE';
72              
73 4 100       14 if ($params{prefix}) {
74 1         14 unshift @COLUMN_ORDER => $name;
75             }
76             else {
77 3         10 push @COLUMN_ORDER => $name;
78             }
79              
80 4         16 $COLUMNS{$name} = \%params;
81             }
82              
83             sub set_column_alias {
84 5     5 1 282 my ($class, $name, $alias) = @_;
85              
86             croak "Tried to alias a non-existent column"
87 5 100       105 unless exists $COLUMNS{$name};
88              
89 4 100       115 croak "Missing alias" unless defined $alias;
90              
91 3         10 $COLUMNS{$name}->{alias} = $alias;
92             }
93              
94             sub init {
95 2339     2339 0 30841 my $self = shift;
96              
97             croak "Cannot specify both 'check' and 'chk' as arguments"
98 2339 100 100     8212 if exists($self->{check}) && exists($self->{+CHK});
99              
100             # Allow 'check' as an argument
101             $self->{+CHK} ||= delete $self->{check}
102 2338 100 66     11417 if exists $self->{check};
103             }
104              
105             sub render_got {
106 324     324 1 475 my $self = shift;
107              
108 324         490 my $exp = $self->{+EXCEPTION};
109 324 100       652 if ($exp) {
110 1         3 chomp($exp = "$exp");
111 1         4 $exp =~ s/\n.*$//g;
112 1         7 return "";
113             }
114              
115 323         493 my $dne = $self->{+DNE};
116 323 100 100     772 return '' if $dne && $dne eq 'got';
117              
118 302         484 my $got = $self->{+GOT};
119 302 100       1000 return '' unless defined $got;
120              
121 288         456 my $check = $self->{+CHK};
122 288   100     1186 my $stringify = defined( $check ) && $check->stringify_got;
123              
124 288 100 100     1096 return render_ref($got) if ref $got && !$stringify;
125              
126 222         634 return "$got";
127             }
128              
129             sub render_check {
130 322     322 1 499 my $self = shift;
131              
132 322         499 my $dne = $self->{+DNE};
133 322 100 100     829 return '' if $dne && $dne eq 'check';
134              
135 310         484 my $check = $self->{+CHK};
136 310 100       604 return '' unless defined $check;
137              
138 308         923 return $check->render;
139             }
140              
141             sub _full_id {
142 167     167   520 my ($type, $id) = @_;
143 167 100 100     701 return "<$id>" if !$type || $type eq 'META';
144 130 100       296 return $id if $type eq 'SCALAR';
145 121 100       328 return "{$id}" if $type eq 'HASH';
146 78 100       168 return "{$id} " if $type eq 'HASHKEY';
147 75 100       263 return "[$id]" if $type eq 'ARRAY';
148 19 100       84 return "$id()" if $type eq 'METHOD';
149 2 50       6 return "$id" if $type eq 'DEREF';
150 0         0 return "<$id>";
151             }
152              
153             sub _arrow_id {
154 176     176   586 my ($path, $type) = @_;
155 176 100       427 return '' unless $path;
156              
157 65 100 100     255 return ' ' if !$type || $type eq 'META'; # Meta gets a space, not an arrow
158              
159 50 100       119 return '->' if $type eq 'METHOD'; # Method always needs an arrow
160 43 100       107 return '->' if $type eq 'SCALAR'; # Scalar always needs an arrow
161 36 100       82 return '->' if $type eq 'DEREF'; # deref always needs arrow
162 35 100       226 return '->' if $path =~ m/(>|\(\))$/; # Need an arrow after meta, or after a method
163 29 100       73 return '->' if $path eq '$VAR'; # Need an arrow after the initial ref
164              
165             # Hash and array need an arrow unless they follow another hash/array
166 26 100 66     200 return '->' if $type =~ m/^(HASH|ARRAY)$/ && $path !~ m/(\]|\})$/;
167              
168             # No arrow needed
169 21         70 return '';
170             }
171              
172             sub _join_id {
173 161     161   780 my ($path, $parts) = @_;
174 161         316 my ($type, $key) = @$parts;
175              
176 161         356 my $id = _full_id($type, $key);
177 161         340 my $join = _arrow_id($path, $type);
178              
179 161         680 return "${path}${join}${id}";
180             }
181              
182             sub should_show {
183 350     350 1 607 my $self = shift;
184 350 100       851 return 1 unless $self->verified;
185 76 100       408 defined( my $check = $self->check ) || return 0;
186 73 50       710 return 0 unless $check->lines;
187 73   100     256 my $file = $check->file || return 0;
188              
189 50         169 my $ctx = context();
190 50         4068 my $cfile = $ctx->trace->file;
191 50         496 $ctx->release;
192 50 100       635 return 0 unless $file eq $cfile;
193              
194 49         434 return 1;
195             }
196              
197             sub filter_visible {
198 225     225 1 352 my $self = shift;
199              
200 225         468 my @deltas;
201 225         507 my @queue = (['', $self]);
202              
203 225         675 while (my $set = shift @queue) {
204 344         746 my ($path, $delta) = @$set;
205              
206 344 100       697 push @deltas => [$path, $delta] if $delta->should_show;
207              
208 344   100     2416 my $children = $delta->children || next;
209 321 100       1888 next unless @$children;
210              
211 71         120 my @new;
212 71         164 for my $child (@$children) {
213 119         281 my $cpath = _join_id($path, $child->id);
214 119         337 push @new => [$cpath, $child];
215             }
216 71         284 unshift @queue => @new;
217             }
218              
219 225         568 return \@deltas;
220             }
221              
222 231 100   231 1 695 sub table_header { [map {$COLUMNS{$_}->{alias} || $_} @COLUMN_ORDER] }
  1392         4541  
223              
224             sub table_op {
225 322     322 1 500 my $self = shift;
226              
227 322 100       719 defined( my $check = $self->{+CHK} ) || return '!exists';
228              
229             return $check->operator($self->{+GOT})
230 310 100 100     1320 unless $self->{+DNE} && $self->{+DNE} eq 'got';
231              
232 21         88 return $check->operator();
233             }
234              
235             sub table_check_lines {
236 322     322 1 552 my $self = shift;
237              
238 322 100       725 defined( my $check = $self->{+CHK} ) || return '';
239 310   50     802 my $lines = $check->lines || return '';
240              
241 310 100       757 return '' unless @$lines;
242              
243 271         931 return join ', ' => @$lines;
244             }
245              
246             sub table_got_lines {
247 323     323 1 558 my $self = shift;
248              
249 323 100       798 defined( my $check = $self->{+CHK} ) || return '';
250 311 100 100     791 return '' if $self->{+DNE} && $self->{+DNE} eq 'got';
251              
252 290         1123 my @lines = $check->got_lines($self->{+GOT});
253 290 100       890 return '' unless @lines;
254              
255 5         24 return join ', ' => @lines;
256             }
257              
258             sub table_rows {
259 225     225 1 397 my $self = shift;
260              
261 225         466 my $deltas = $self->filter_visible;
262              
263 225         371 my @rows;
264 225         463 for my $set (@$deltas) {
265 320         655 my ($id, $d) = @$set;
266              
267 320         442 my @row;
268 320         559 for my $col (@COLUMN_ORDER) {
269 1933         3024 my $spec = $COLUMNS{$col};
270 1933         3467 my $val = $spec->{value}->($d, $id);
271 1933 50       3742 $val = '' unless defined $val;
272 1933         3808 push @row => $val;
273             }
274              
275 320         772 push @rows => \@row;
276             }
277              
278 225         599 return \@rows;
279             }
280              
281             sub table {
282 227     227 1 445 my $self = shift;
283              
284 227         338 my @diag;
285 227         483 my $header = $self->table_header;
286 227         512 my $rows = $self->table_rows;
287              
288 227         439 my $render_rows = [@$rows];
289 227 100       627 my $max = exists $ENV{TS_MAX_DELTA} ? $ENV{TS_MAX_DELTA} : 25;
290 227 100 66     920 if ($max && @$render_rows > $max) {
291 1         4 @$render_rows = map { [@$_] } @{$render_rows}[0 .. ($max - 1)];
  2         7  
  1         4  
292 1         13 @diag = (
293             "************************************************************",
294             sprintf("* Stopped after %-42.42s *", "$max differences."),
295             "* Set the TS_MAX_DELTA environment var to raise the limit. *",
296             "* Set it to 0 for no limit. *",
297             "************************************************************",
298             );
299             }
300              
301 227         362 my @dne;
302 227         443 for my $row (@$render_rows) {
303 328   100     852 my $got = $row->[$COLUMNS{GOT}->{id}] || '';
304 328   100     843 my $chk = $row->[$COLUMNS{CHECK}->{id}] || '';
305 328 100       935 if ($got eq '') {
    100          
306 20         102 push @dne => "$row->[$COLUMNS{PATH}->{id}]: DOES NOT EXIST";
307             }
308             elsif ($chk eq '') {
309 13         65 push @dne => "$row->[$COLUMNS{PATH}->{id}]: SHOULD NOT EXIST";
310             }
311             }
312              
313 227 100       513 if (@dne) {
314 25         83 unshift @dne => '==== Summary of missing/extra items ====';
315 25         76 push @dne => '== end summary of missing/extra items ==';
316             }
317              
318             my $table_args = {
319             header => $header,
320             collapse => 1,
321             sanitize => 1,
322             mark_tail => 1,
323 227         880 no_collapse => [grep { $COLUMNS{$COLUMN_ORDER[$_]}->{no_collapse} } 0 .. $#COLUMN_ORDER],
  1367         3086  
324             };
325              
326 227         1246 my $render = join "\n" => (
327             Test2::Util::Table::table(%$table_args, rows => $render_rows),
328             @dne,
329             @diag,
330             );
331              
332 227         1190655 my $table = Test2::EventFacet::Info::Table->new(
333             %$table_args,
334             rows => $rows,
335             as_string => $render,
336             );
337              
338 227         6555 return $table;
339             }
340              
341 219     219 0 541 sub diag { shift->table }
342              
343             1;
344              
345             __END__