File Coverage

blib/lib/Test/Stream/Delta.pm
Criterion Covered Total %
statement 159 160 99.3
branch 77 80 96.2
condition 47 53 88.6
subroutine 26 26 100.0
pod 12 13 92.3
total 321 332 96.6


line stmt bran cond sub pod time code
1             package Test::Stream::Delta;
2 100     100   1003 use strict;
  100         165  
  100         2473  
3 100     100   489 use warnings;
  100         216  
  100         3535  
4              
5             use Test::Stream::HashBase(
6 100         1090 accessors => [qw/verified id got chk children dne exception/]
7 100     100   514 );
  100         231  
8              
9 100     100   1089 use Test::Stream::Table();
  100         179  
  100         1693  
10 100     100   1146 use Test::Stream::Context();
  100         175  
  100         1955  
11              
12 100     100   535 use Test::Stream::Util qw/render_ref/;
  100         168  
  100         642  
13 100     100   645 use Scalar::Util qw/reftype blessed refaddr/;
  100         181  
  100         5926  
14              
15 100     100   505 use Carp qw/croak/;
  100         170  
  100         4613  
16              
17             # 'CHECK' constant would not work, but I like exposing 'check()' to people
18             # using this class.
19             BEGIN {
20 100     100   516 no warnings 'once';
  100         193  
  100         5248  
21 100     100   287 *check = \&chk;
22 100         194953 *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             sub remove_column {
37 5     5 1 11 my $class = shift;
38 5         7 my $header = shift;
39 5         11 @COLUMN_ORDER = grep { $_ ne $header } @COLUMN_ORDER;
  33         97  
40 5 100       31 delete $COLUMNS{$header} ? 1 : 0;
41             }
42              
43             sub add_column {
44 7     7 1 52 my $class = shift;
45 7         11 my $name = shift;
46              
47 7 100       191 croak "Column name is required"
48             unless $name;
49              
50             croak "Column '$name' is already defined"
51 6 100       144 if $COLUMNS{$name};
52              
53 5         7 my %params;
54 5 100       15 if (@_ == 1) {
55 3         12 %params = (value => @_, name => $name);
56             }
57             else {
58 2         10 %params = (@_, name => $name);
59             }
60              
61 5         8 my $value = $params{value};
62              
63 5 100       114 croak "You must specify a 'value' callback"
64             unless $value;
65              
66 4 100 66     131 croak "'value' callback must be a CODE reference"
67             unless ref($value) && reftype($value) eq 'CODE';
68              
69 3 100       8 if ($params{prefix}) {
70 1         4 unshift @COLUMN_ORDER => $name;
71             }
72             else {
73 2         6 push @COLUMN_ORDER => $name;
74             }
75              
76 3         10 $COLUMNS{$name} = \%params;
77             }
78              
79             sub init {
80 248     248 0 338 my $self = shift;
81              
82             croak "Cannot specify both 'check' and 'chk' as arguments"
83 248 100 66     1264 if exists($self->{check}) && exists($self->{+CHK});
84              
85             # Allow 'check' as an argument
86             $self->{+CHK} ||= delete $self->{check}
87 247 100 66     1617 if exists $self->{check};
88             }
89              
90             sub render_got {
91 160     160 1 219 my $self = shift;
92              
93 160         309 my $exp = $self->{+EXCEPTION};
94 160 100       313 if ($exp) {
95 1         3 chomp($exp = "$exp");
96 1         4 $exp =~ s/\n.*$//g;
97 1         6 return "";
98             }
99              
100 159         277 my $dne = $self->{+DNE};
101 159 100 100     434 return '' if $dne && $dne eq 'got';
102              
103 150         223 my $got = $self->{+GOT};
104 150 100       348 return '' unless defined $got;
105              
106 138         177 my $check = $self->{+CHK};
107 138   100     647 my $stringify = $check && $check->stringify_got;
108              
109 138 100 100     554 return render_ref($got) if ref $got && !$stringify;
110              
111 94         255 return "$got";
112             }
113              
114             sub render_check {
115 158     158 1 192 my $self = shift;
116              
117 158         260 my $dne = $self->{+DNE};
118 158 100 100     431 return '' if $dne && $dne eq 'check';
119              
120 152         202 my $check = $self->{+CHK};
121 152 100       300 return '' unless defined $check;
122              
123 150         523 return $check->render;
124             }
125              
126             sub _full_id {
127 128     128   201 my ($type, $id) = @_;
128 128 100 100     656 return "<$id>" if !$type || $type eq 'META';
129 95 100       210 return $id if $type eq 'SCALAR';
130 87 100       249 return "{$id}" if $type eq 'HASH';
131 55 100       185 return "[$id]" if $type eq 'ARRAY';
132 17 50       67 return "$id()" if $type eq 'METHOD';
133 0         0 return "<$id>";
134             }
135              
136             sub _arrow_id {
137 137     137   242 my ($path, $type) = @_;
138 137 100       330 return '' unless $path;
139              
140 62 100 100     276 return ' ' if !$type || $type eq 'META'; # Meta gets a space, not an arrow
141              
142 47 100       105 return '->' if $type eq 'METHOD'; # Method always needs an arrow
143 40 100       93 return '->' if $type eq 'SCALAR'; # Scalar always needs an arrow
144 33 100       270 return '->' if $path =~ m/(>|\(\))$/; # Need an arrow after meta, or after a method
145 27 100       65 return '->' if $path eq '$VAR'; # Need an arrow after the initial ref
146              
147             # Hash and array need an arrow unless they follow another hash/array
148 24 100 66     225 return '->' if $type =~ m/^(HASH|ARRAY)$/ && $path !~ m/(\]|\})$/;
149              
150             # No arrow needed
151 20         53 return '';
152             }
153              
154             sub _join_id {
155 122     122   389 my ($path, $parts) = @_;
156 122         223 my ($type, $key) = @$parts;
157              
158 122         249 my $id = _full_id($type, $key);
159 122         266 my $join = _arrow_id($path, $type);
160              
161 122         442 return "${path}${join}${id}";
162             }
163              
164             sub should_show {
165 179     179 1 271 my $self = shift;
166 179 100       517 return 1 unless $self->verified;
167 58   100     403 my $check = $self->check || return 0;
168 55 50       387 return 0 unless $check->lines;
169 55   100     211 my $file = $check->file || return 0;
170              
171 39         120 my $ctx = Test::Stream::Context::context();
172 39         136 my $cfile = $ctx->debug->file;
173 39         120 $ctx->release;
174 39 100       101 return 0 unless $file eq $cfile;
175              
176 38         169 return 1;
177             }
178              
179             sub filter_visible {
180 93     93 1 141 my $self = shift;
181              
182 93         106 my @deltas;
183 93         264 my @queue = (['', $self]);
184              
185 93         281 while (my $set = shift @queue) {
186 173         308 my ($path, $delta) = @$set;
187              
188 173 100       366 push @deltas => [$path, $delta] if $delta->should_show;
189              
190 173   100     1270 my $children = $delta->children || next;
191 164 100       1182 next unless @$children;
192              
193 53         81 my @new;
194 53         102 for my $child (@$children) {
195 80         234 my $cpath = _join_id($path, $child->id);
196 80         242 push @new => [$cpath, $child];
197             }
198 53         228 unshift @queue => @new;
199             }
200              
201 93         235 return \@deltas;
202             }
203              
204 96 100   96 1 213 sub table_header { [map {$COLUMNS{$_}->{alias} || $_} @COLUMN_ORDER] };
  580         2074  
205              
206             sub table_op {
207 158     158 1 214 my $self = shift;
208              
209 158   100     401 my $check = $self->{+CHK} || return '!exists';
210              
211             return $check->operator($self->{+GOT})
212 152 100 100     736 unless $self->{+DNE} && $self->{+DNE} eq 'got';
213              
214 9         31 return $check->operator();
215             }
216              
217             sub table_check_lines {
218 158     158 1 257 my $self = shift;
219              
220 158   100     413 my $check = $self->{+CHK} || return '';
221 152   50     440 my $lines = $check->lines || return '';
222              
223 152 100       389 return '' unless @$lines;
224              
225 112         366 return join ', ' => @$lines;
226             }
227              
228             sub table_got_lines {
229 159     159 1 219 my $self = shift;
230              
231 159   100     414 my $check = $self->{+CHK} || return '';
232 153 100 100     427 return '' if $self->{+DNE} && $self->{+DNE} eq 'got';
233              
234 144         623 my @lines = $check->got_lines($self->{+GOT});
235 144 100       479 return '' unless @lines;
236              
237 6         24 return join ', ' => @lines;
238             }
239              
240             sub table_rows {
241 93     93 1 141 my $self = shift;
242              
243 93         215 my $deltas = $self->filter_visible;
244              
245 93         138 my @rows;
246 93         169 for my $set (@$deltas) {
247 156         368 my ($id, $d) = @$set;
248              
249 156         196 my @row;
250 156         253 for my $col (@COLUMN_ORDER) {
251 948         1329 my $spec = $COLUMNS{$col};
252 948         1974 my $val = $spec->{value}->($d, $id);
253 948 50       2190 $val = '' unless defined $val;
254 948         1991 push @row => $val;
255             }
256              
257 156         370 push @rows => \@row;
258             }
259              
260 93         244 return \@rows;
261             }
262              
263             sub table {
264 95     95 1 166 my $self = shift;
265              
266 95         128 my @out;
267              
268 95         225 my $header = $self->table_header;
269 95         283 my $rows = $self->table_rows;
270              
271 95 100       289 my $max = exists $ENV{TS_MAX_DELTA} ? $ENV{TS_MAX_DELTA} : 25;
272 95 100 66     439 if ($max && @$rows > $max) {
273 1         4 @$rows = @{$rows}[0 .. ($max - 1)];
  1         4  
274 1         7 push @out => (
275             "************************************************************",
276             sprintf("* Stopped after %-42.42s *", "$max differences."),
277             "* Set the TS_MAX_DELTA environment var to raise the limit. *",
278             "* Set it to 0 for no limit. *",
279             "************************************************************",
280             );
281             }
282              
283 95         237 my @no_collapse = grep { $COLUMNS{$COLUMN_ORDER[$_]}->{no_collapse} } 0 .. $#COLUMN_ORDER;
  574         1143  
284 95         432 unshift @out => Test::Stream::Table::table(
285             header => $header,
286             rows => $rows,
287             collapse => 1,
288             sanitize => 1,
289             mark_tail => 1,
290             no_collapse => \@no_collapse,
291             );
292              
293 95         1023 return @out;
294             }
295              
296             1;
297              
298             __END__