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   1211 use strict;
  169         330  
  169         5076  
3 169     169   950 use warnings;
  169         391  
  169         8911  
4              
5             our $VERSION = '0.000156';
6              
7 169     169   1278 use Test2::Util::HashBase qw{verified id got chk children dne exception note};
  169         542  
  169         1345  
8              
9 169     169   150737 use Test2::EventFacet::Info::Table;
  169         105804  
  169         4256  
10              
11 169     169   69086 use Test2::Util::Table();
  169         628  
  169         5652  
12 169     169   1095 use Test2::API qw/context/;
  169         321  
  169         11912  
13              
14 169     169   1139 use Test2::Util::Ref qw/render_ref rtype/;
  169         317  
  169         8307  
15 169     169   908 use Carp qw/croak/;
  169         326  
  169         8371  
16              
17             # 'CHECK' constant would not work, but I like exposing 'check()' to people
18             # using this class.
19             BEGIN {
20 169     169   978 no warnings 'once';
  169         287  
  169         8935  
21 169     169   767 *check = \&chk;
22 169         315238 *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 44 my $class = shift;
42 5         9 my $header = shift;
43 5         11 @COLUMN_ORDER = grep { $_ ne $header } @COLUMN_ORDER;
  33         67  
44 5 100       39 delete $COLUMNS{$header} ? 1 : 0;
45             }
46              
47             sub add_column {
48 8     8 1 277 my $class = shift;
49 8         16 my $name = shift;
50              
51 8 100       138 croak "Column name is required"
52             unless $name;
53              
54             croak "Column '$name' is already defined"
55 7 100       141 if $COLUMNS{$name};
56              
57 6         10 my %params;
58 6 100       14 if (@_ == 1) {
59 4         14 %params = (value => @_, name => $name);
60             }
61             else {
62 2         13 %params = (@_, name => $name);
63             }
64              
65 6         13 my $value = $params{value};
66              
67 6 100       101 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       13 if ($params{prefix}) {
74 1         20 unshift @COLUMN_ORDER => $name;
75             }
76             else {
77 3         7 push @COLUMN_ORDER => $name;
78             }
79              
80 4         14 $COLUMNS{$name} = \%params;
81             }
82              
83             sub set_column_alias {
84 5     5 1 264 my ($class, $name, $alias) = @_;
85              
86             croak "Tried to alias a non-existent column"
87 5 100       106 unless exists $COLUMNS{$name};
88              
89 4 100       100 croak "Missing alias" unless defined $alias;
90              
91 3         11 $COLUMNS{$name}->{alias} = $alias;
92             }
93              
94             sub init {
95 2467     2467 0 33766 my $self = shift;
96              
97             croak "Cannot specify both 'check' and 'chk' as arguments"
98 2467 100 100     8775 if exists($self->{check}) && exists($self->{+CHK});
99              
100             # Allow 'check' as an argument
101             $self->{+CHK} ||= delete $self->{check}
102 2466 100 66     11988 if exists $self->{check};
103             }
104              
105             sub render_got {
106 332     332 1 529 my $self = shift;
107              
108 332         572 my $exp = $self->{+EXCEPTION};
109 332 100       635 if ($exp) {
110 1         4 chomp($exp = "$exp");
111 1         4 $exp =~ s/\n.*$//g;
112 1         7 return "";
113             }
114              
115 331         524 my $dne = $self->{+DNE};
116 331 100 100     809 return '' if $dne && $dne eq 'got';
117              
118 310         505 my $got = $self->{+GOT};
119 310 100       636 return '' unless defined $got;
120              
121 296         412 my $check = $self->{+CHK};
122 296   100     1186 my $stringify = defined( $check ) && $check->stringify_got;
123              
124 296 100 100     1094 return render_ref($got) if ref $got && !$stringify;
125              
126 230         640 return "$got";
127             }
128              
129             sub render_check {
130 330     330 1 503 my $self = shift;
131              
132 330         514 my $dne = $self->{+DNE};
133 330 100 100     890 return '' if $dne && $dne eq 'check';
134              
135 318         511 my $check = $self->{+CHK};
136 318 100       697 return '' unless defined $check;
137              
138 316         1051 return $check->render;
139             }
140              
141             sub _full_id {
142 167     167   498 my ($type, $id) = @_;
143 167 100 100     697 return "<$id>" if !$type || $type eq 'META';
144 130 100       282 return $id if $type eq 'SCALAR';
145 121 100       348 return "{$id}" if $type eq 'HASH';
146 78 100       159 return "{$id} " if $type eq 'HASHKEY';
147 75 100       243 return "[$id]" if $type eq 'ARRAY';
148 19 100       73 return "$id()" if $type eq 'METHOD';
149 2 50       8 return "$id" if $type eq 'DEREF';
150 0         0 return "<$id>";
151             }
152              
153             sub _arrow_id {
154 176     176   529 my ($path, $type) = @_;
155 176 100       407 return '' unless $path;
156              
157 65 100 100     247 return ' ' if !$type || $type eq 'META'; # Meta gets a space, not an arrow
158              
159 50 100       109 return '->' if $type eq 'METHOD'; # Method always needs an arrow
160 43 100       88 return '->' if $type eq 'SCALAR'; # Scalar always needs an arrow
161 36 100       88 return '->' if $type eq 'DEREF'; # deref always needs arrow
162 35 100       179 return '->' if $path =~ m/(>|\(\))$/; # Need an arrow after meta, or after a method
163 29 100       72 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     210 return '->' if $type =~ m/^(HASH|ARRAY)$/ && $path !~ m/(\]|\})$/;
167              
168             # No arrow needed
169 21         82 return '';
170             }
171              
172             sub _join_id {
173 161     161   756 my ($path, $parts) = @_;
174 161         325 my ($type, $key) = @$parts;
175              
176 161         360 my $id = _full_id($type, $key);
177 161         354 my $join = _arrow_id($path, $type);
178              
179 161         691 return "${path}${join}${id}";
180             }
181              
182             sub should_show {
183 358     358 1 560 my $self = shift;
184 358 100       783 return 1 unless $self->verified;
185 76 100       421 defined( my $check = $self->check ) || return 0;
186 73 50       492 return 0 unless $check->lines;
187 73   100     250 my $file = $check->file || return 0;
188              
189 50         161 my $ctx = context();
190 50         3798 my $cfile = $ctx->trace->file;
191 50         477 $ctx->release;
192 50 100       646 return 0 unless $file eq $cfile;
193              
194 49         402 return 1;
195             }
196              
197             sub filter_visible {
198 233     233 1 543 my $self = shift;
199              
200 233         339 my @deltas;
201 233         498 my @queue = (['', $self]);
202              
203 233         620 while (my $set = shift @queue) {
204 352         794 my ($path, $delta) = @$set;
205              
206 352 100       721 push @deltas => [$path, $delta] if $delta->should_show;
207              
208 352   100     2446 my $children = $delta->children || next;
209 329 100       2035 next unless @$children;
210              
211 71         122 my @new;
212 71         155 for my $child (@$children) {
213 119         323 my $cpath = _join_id($path, $child->id);
214 119         358 push @new => [$cpath, $child];
215             }
216 71         304 unshift @queue => @new;
217             }
218              
219 233         585 return \@deltas;
220             }
221              
222 239 100   239 1 709 sub table_header { [map {$COLUMNS{$_}->{alias} || $_} @COLUMN_ORDER] }
  1440         4596  
223              
224             sub table_op {
225 330     330 1 527 my $self = shift;
226              
227 330 100       743 defined( my $check = $self->{+CHK} ) || return '!exists';
228              
229             return $check->operator($self->{+GOT})
230 318 100 100     1446 unless $self->{+DNE} && $self->{+DNE} eq 'got';
231              
232 21         95 return $check->operator();
233             }
234              
235             sub table_check_lines {
236 330     330 1 499 my $self = shift;
237              
238 330 100       718 defined( my $check = $self->{+CHK} ) || return '';
239 318   50     778 my $lines = $check->lines || return '';
240              
241 318 100       814 return '' unless @$lines;
242              
243 279         858 return join ', ' => @$lines;
244             }
245              
246             sub table_got_lines {
247 331     331 1 488 my $self = shift;
248              
249 331 100       766 defined( my $check = $self->{+CHK} ) || return '';
250 319 100 100     858 return '' if $self->{+DNE} && $self->{+DNE} eq 'got';
251              
252 298         1116 my @lines = $check->got_lines($self->{+GOT});
253 298 100       984 return '' unless @lines;
254              
255 5         25 return join ', ' => @lines;
256             }
257              
258             sub table_rows {
259 233     233 1 426 my $self = shift;
260              
261 233         475 my $deltas = $self->filter_visible;
262              
263 233         365 my @rows;
264 233         466 for my $set (@$deltas) {
265 328         641 my ($id, $d) = @$set;
266              
267 328         427 my @row;
268 328         542 for my $col (@COLUMN_ORDER) {
269 1981         3003 my $spec = $COLUMNS{$col};
270 1981         3703 my $val = $spec->{value}->($d, $id);
271 1981 50       3871 $val = '' unless defined $val;
272 1981         4410 push @row => $val;
273             }
274              
275 328         820 push @rows => \@row;
276             }
277              
278 233         631 return \@rows;
279             }
280              
281             sub table {
282 235     235 1 477 my $self = shift;
283              
284 235         327 my @diag;
285 235         483 my $header = $self->table_header;
286 235         559 my $rows = $self->table_rows;
287              
288 235         525 my $render_rows = [@$rows];
289 235 100       636 my $max = exists $ENV{TS_MAX_DELTA} ? $ENV{TS_MAX_DELTA} : 25;
290 235 100 66     919 if ($max && @$render_rows > $max) {
291 1         6 @$render_rows = map { [@$_] } @{$render_rows}[0 .. ($max - 1)];
  2         7  
  1         4  
292 1         11 @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 235         424 my @dne;
302 235         479 for my $row (@$render_rows) {
303 336   100     941 my $got = $row->[$COLUMNS{GOT}->{id}] || '';
304 336   100     807 my $chk = $row->[$COLUMNS{CHECK}->{id}] || '';
305 336 100       982 if ($got eq '') {
    100          
306 20         121 push @dne => "$row->[$COLUMNS{PATH}->{id}]: DOES NOT EXIST";
307             }
308             elsif ($chk eq '') {
309 13         76 push @dne => "$row->[$COLUMNS{PATH}->{id}]: SHOULD NOT EXIST";
310             }
311             }
312              
313 235 100       541 if (@dne) {
314 25         61 unshift @dne => '==== Summary of missing/extra items ====';
315 25         49 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 235         689 no_collapse => [grep { $COLUMNS{$COLUMN_ORDER[$_]}->{no_collapse} } 0 .. $#COLUMN_ORDER],
  1415         3215  
324             };
325              
326 235         1306 my $render = join "\n" => (
327             Test2::Util::Table::table(%$table_args, rows => $render_rows),
328             @dne,
329             @diag,
330             );
331              
332 235         1220125 my $table = Test2::EventFacet::Info::Table->new(
333             %$table_args,
334             rows => $rows,
335             as_string => $render,
336             );
337              
338 235         7155 return $table;
339             }
340              
341 227     227 0 519 sub diag { shift->table }
342              
343             1;
344              
345             __END__