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 168     168   1126 use strict;
  168         335  
  168         4592  
3 168     168   812 use warnings;
  168         323  
  168         8148  
4              
5             our $VERSION = '0.000153';
6              
7 168     168   1232 use Test2::Util::HashBase qw{verified id got chk children dne exception note};
  168         392  
  168         1300  
8              
9 168     168   136389 use Test2::EventFacet::Info::Table;
  168         97615  
  168         4044  
10              
11 168     168   60220 use Test2::Util::Table();
  168         570  
  168         4853  
12 168     168   1016 use Test2::API qw/context/;
  168         321  
  168         10197  
13              
14 168     168   1028 use Test2::Util::Ref qw/render_ref rtype/;
  168         309  
  168         8462  
15 168     168   946 use Carp qw/croak/;
  168         283  
  168         8156  
16              
17             # 'CHECK' constant would not work, but I like exposing 'check()' to people
18             # using this class.
19             BEGIN {
20 168     168   910 no warnings 'once';
  168         282  
  168         8642  
21 168     168   787 *check = \&chk;
22 168         293167 *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 60 my $class = shift;
42 5         12 my $header = shift;
43 5         14 @COLUMN_ORDER = grep { $_ ne $header } @COLUMN_ORDER;
  33         97  
44 5 100       55 delete $COLUMNS{$header} ? 1 : 0;
45             }
46              
47             sub add_column {
48 8     8 1 318 my $class = shift;
49 8         16 my $name = shift;
50              
51 8 100       216 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         14 my %params;
58 6 100       21 if (@_ == 1) {
59 4         17 %params = (value => @_, name => $name);
60             }
61             else {
62 2         15 %params = (@_, name => $name);
63             }
64              
65 6         13 my $value = $params{value};
66              
67 6 100       99 croak "You must specify a 'value' callback"
68             unless $value;
69              
70 5 100       18 croak "'value' callback must be a CODE reference"
71             unless rtype($value) eq 'CODE';
72              
73 4 100       18 if ($params{prefix}) {
74 1         7 unshift @COLUMN_ORDER => $name;
75             }
76             else {
77 3         9 push @COLUMN_ORDER => $name;
78             }
79              
80 4         23 $COLUMNS{$name} = \%params;
81             }
82              
83             sub set_column_alias {
84 5     5 1 275 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       98 croak "Missing alias" unless defined $alias;
90              
91 3         9 $COLUMNS{$name}->{alias} = $alias;
92             }
93              
94             sub init {
95 2335     2335 0 26468 my $self = shift;
96              
97             croak "Cannot specify both 'check' and 'chk' as arguments"
98 2335 100 100     6752 if exists($self->{check}) && exists($self->{+CHK});
99              
100             # Allow 'check' as an argument
101             $self->{+CHK} ||= delete $self->{check}
102 2334 100 66     8870 if exists $self->{check};
103             }
104              
105             sub render_got {
106 320     320 1 414 my $self = shift;
107              
108 320         416 my $exp = $self->{+EXCEPTION};
109 320 100       502 if ($exp) {
110 1         4 chomp($exp = "$exp");
111 1         6 $exp =~ s/\n.*$//g;
112 1         8 return "";
113             }
114              
115 319         449 my $dne = $self->{+DNE};
116 319 100 100     678 return '' if $dne && $dne eq 'got';
117              
118 298         452 my $got = $self->{+GOT};
119 298 100       526 return '' unless defined $got;
120              
121 284         413 my $check = $self->{+CHK};
122 284   100     1173 my $stringify = defined( $check ) && $check->stringify_got;
123              
124 284 100 100     909 return render_ref($got) if ref $got && !$stringify;
125              
126 222         561 return "$got";
127             }
128              
129             sub render_check {
130 318     318 1 405 my $self = shift;
131              
132 318         425 my $dne = $self->{+DNE};
133 318 100 100     631 return '' if $dne && $dne eq 'check';
134              
135 306         421 my $check = $self->{+CHK};
136 306 100       605 return '' unless defined $check;
137              
138 304         722 return $check->render;
139             }
140              
141             sub _full_id {
142 165     165   454 my ($type, $id) = @_;
143 165 100 100     625 return "<$id>" if !$type || $type eq 'META';
144 128 100       242 return $id if $type eq 'SCALAR';
145 119 100       270 return "{$id}" if $type eq 'HASH';
146 78 100       156 return "{$id} " if $type eq 'HASHKEY';
147 75 100       204 return "[$id]" if $type eq 'ARRAY';
148 19 100       59 return "$id()" if $type eq 'METHOD';
149 2 50       5 return "$id" if $type eq 'DEREF';
150 0         0 return "<$id>";
151             }
152              
153             sub _arrow_id {
154 174     174   510 my ($path, $type) = @_;
155 174 100       387 return '' unless $path;
156              
157 65 100 100     238 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       90 return '->' if $type eq 'SCALAR'; # Scalar always needs an arrow
161 36 100       79 return '->' if $type eq 'DEREF'; # deref always needs arrow
162 35 100       162 return '->' if $path =~ m/(>|\(\))$/; # Need an arrow after meta, or after a method
163 29 100       66 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     184 return '->' if $type =~ m/^(HASH|ARRAY)$/ && $path !~ m/(\]|\})$/;
167              
168             # No arrow needed
169 21         51 return '';
170             }
171              
172             sub _join_id {
173 159     159   683 my ($path, $parts) = @_;
174 159         253 my ($type, $key) = @$parts;
175              
176 159         313 my $id = _full_id($type, $key);
177 159         336 my $join = _arrow_id($path, $type);
178              
179 159         471 return "${path}${join}${id}";
180             }
181              
182             sub should_show {
183 346     346 1 506 my $self = shift;
184 346 100       604 return 1 unless $self->verified;
185 74 100       336 defined( my $check = $self->check ) || return 0;
186 71 50       395 return 0 unless $check->lines;
187 71   100     221 my $file = $check->file || return 0;
188              
189 48         144 my $ctx = context();
190 48         3106 my $cfile = $ctx->trace->file;
191 48         400 $ctx->release;
192 48 100       520 return 0 unless $file eq $cfile;
193              
194 47         147 return 1;
195             }
196              
197             sub filter_visible {
198 223     223 1 590 my $self = shift;
199              
200 223         294 my @deltas;
201 223         471 my @queue = (['', $self]);
202              
203 223         654 while (my $set = shift @queue) {
204 340         625 my ($path, $delta) = @$set;
205              
206 340 100       658 push @deltas => [$path, $delta] if $delta->should_show;
207              
208 340   100     1993 my $children = $delta->children || next;
209 317 100       1570 next unless @$children;
210              
211 69         87 my @new;
212 69         143 for my $child (@$children) {
213 117         244 my $cpath = _join_id($path, $child->id);
214 117         302 push @new => [$cpath, $child];
215             }
216 69         236 unshift @queue => @new;
217             }
218              
219 223         475 return \@deltas;
220             }
221              
222 229 100   229 1 600 sub table_header { [map {$COLUMNS{$_}->{alias} || $_} @COLUMN_ORDER] }
  1380         3721  
223              
224             sub table_op {
225 318     318 1 434 my $self = shift;
226              
227 318 100       638 defined( my $check = $self->{+CHK} ) || return '!exists';
228              
229             return $check->operator($self->{+GOT})
230 306 100 100     1379 unless $self->{+DNE} && $self->{+DNE} eq 'got';
231              
232 21         63 return $check->operator();
233             }
234              
235             sub table_check_lines {
236 318     318 1 398 my $self = shift;
237              
238 318 100       582 defined( my $check = $self->{+CHK} ) || return '';
239 306   50     683 my $lines = $check->lines || return '';
240              
241 306 100       635 return '' unless @$lines;
242              
243 267         688 return join ', ' => @$lines;
244             }
245              
246             sub table_got_lines {
247 319     319 1 417 my $self = shift;
248              
249 319 100       764 defined( my $check = $self->{+CHK} ) || return '';
250 307 100 100     695 return '' if $self->{+DNE} && $self->{+DNE} eq 'got';
251              
252 286         809 my @lines = $check->got_lines($self->{+GOT});
253 286 100       746 return '' unless @lines;
254              
255 5         19 return join ', ' => @lines;
256             }
257              
258             sub table_rows {
259 223     223 1 295 my $self = shift;
260              
261 223         429 my $deltas = $self->filter_visible;
262              
263 223         348 my @rows;
264 223         415 for my $set (@$deltas) {
265 316         579 my ($id, $d) = @$set;
266              
267 316         402 my @row;
268 316         442 for my $col (@COLUMN_ORDER) {
269 1909         2486 my $spec = $COLUMNS{$col};
270 1909         3094 my $val = $spec->{value}->($d, $id);
271 1909 50       3523 $val = '' unless defined $val;
272 1909         3199 push @row => $val;
273             }
274              
275 316         737 push @rows => \@row;
276             }
277              
278 223         498 return \@rows;
279             }
280              
281             sub table {
282 225     225 1 445 my $self = shift;
283              
284 225         316 my @diag;
285 225         454 my $header = $self->table_header;
286 225         542 my $rows = $self->table_rows;
287              
288 225         386 my $render_rows = [@$rows];
289 225 100       622 my $max = exists $ENV{TS_MAX_DELTA} ? $ENV{TS_MAX_DELTA} : 25;
290 225 100 66     838 if ($max && @$render_rows > $max) {
291 1         5 @$render_rows = map { [@$_] } @{$render_rows}[0 .. ($max - 1)];
  2         6  
  1         3  
292 1         10 @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 225         356 my @dne;
302 225         396 for my $row (@$render_rows) {
303 324   100     883 my $got = $row->[$COLUMNS{GOT}->{id}] || '';
304 324   100     679 my $chk = $row->[$COLUMNS{CHECK}->{id}] || '';
305 324 100       911 if ($got eq '') {
    100          
306 20         67 push @dne => "$row->[$COLUMNS{PATH}->{id}]: DOES NOT EXIST";
307             }
308             elsif ($chk eq '') {
309 13         55 push @dne => "$row->[$COLUMNS{PATH}->{id}]: SHOULD NOT EXIST";
310             }
311             }
312              
313 225 100       443 if (@dne) {
314 25         51 unshift @dne => '==== Summary of missing/extra items ====';
315 25         43 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 225         640 no_collapse => [grep { $COLUMNS{$COLUMN_ORDER[$_]}->{no_collapse} } 0 .. $#COLUMN_ORDER],
  1355         2605  
324             };
325              
326 225         1311 my $render = join "\n" => (
327             Test2::Util::Table::table(%$table_args, rows => $render_rows),
328             @dne,
329             @diag,
330             );
331              
332 225         991188 my $table = Test2::EventFacet::Info::Table->new(
333             %$table_args,
334             rows => $rows,
335             as_string => $render,
336             );
337              
338 225         5753 return $table;
339             }
340              
341 217     217 0 467 sub diag { shift->table }
342              
343             1;
344              
345             __END__