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   1011 use strict;
  100         179  
  100         2634  
3 100     100   527 use warnings;
  100         187  
  100         3565  
4              
5             use Test::Stream::HashBase(
6 100         1044 accessors => [qw/verified id got chk children dne exception/]
7 100     100   571 );
  100         170  
8              
9 100     100   1077 use Test::Stream::Table();
  100         185  
  100         1578  
10 100     100   1155 use Test::Stream::Context();
  100         180  
  100         1921  
11              
12 100     100   481 use Test::Stream::Util qw/render_ref/;
  100         183  
  100         672  
13 100     100   525 use Scalar::Util qw/reftype blessed refaddr/;
  100         166  
  100         5933  
14              
15 100     100   500 use Carp qw/croak/;
  100         184  
  100         4583  
16              
17             # 'CHECK' constant would not work, but I like exposing 'check()' to people
18             # using this class.
19             BEGIN {
20 100     100   502 no warnings 'once';
  100         191  
  100         5267  
21 100     100   293 *check = \&chk;
22 100         195661 *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 9 my $class = shift;
38 5         11 my $header = shift;
39 5         9 @COLUMN_ORDER = grep { $_ ne $header } @COLUMN_ORDER;
  33         77  
40 5 100       34 delete $COLUMNS{$header} ? 1 : 0;
41             }
42              
43             sub add_column {
44 7     7 1 50 my $class = shift;
45 7         10 my $name = shift;
46              
47 7 100       209 croak "Column name is required"
48             unless $name;
49              
50             croak "Column '$name' is already defined"
51 6 100       146 if $COLUMNS{$name};
52              
53 5         6 my %params;
54 5 100       14 if (@_ == 1) {
55 3         13 %params = (value => @_, name => $name);
56             }
57             else {
58 2         10 %params = (@_, name => $name);
59             }
60              
61 5         9 my $value = $params{value};
62              
63 5 100       120 croak "You must specify a 'value' callback"
64             unless $value;
65              
66 4 100 66     139 croak "'value' callback must be a CODE reference"
67             unless ref($value) && reftype($value) eq 'CODE';
68              
69 3 100       9 if ($params{prefix}) {
70 1         4 unshift @COLUMN_ORDER => $name;
71             }
72             else {
73 2         6 push @COLUMN_ORDER => $name;
74             }
75              
76 3         11 $COLUMNS{$name} = \%params;
77             }
78              
79             sub init {
80 248     248 0 358 my $self = shift;
81              
82             croak "Cannot specify both 'check' and 'chk' as arguments"
83 248 100 66     1347 if exists($self->{check}) && exists($self->{+CHK});
84              
85             # Allow 'check' as an argument
86             $self->{+CHK} ||= delete $self->{check}
87 247 100 66     1593 if exists $self->{check};
88             }
89              
90             sub render_got {
91 160     160 1 206 my $self = shift;
92              
93 160         272 my $exp = $self->{+EXCEPTION};
94 160 100       341 if ($exp) {
95 1         4 chomp($exp = "$exp");
96 1         4 $exp =~ s/\n.*$//g;
97 1         6 return "";
98             }
99              
100 159         206 my $dne = $self->{+DNE};
101 159 100 100     453 return '' if $dne && $dne eq 'got';
102              
103 150         251 my $got = $self->{+GOT};
104 150 100       325 return '' unless defined $got;
105              
106 138         188 my $check = $self->{+CHK};
107 138   100     667 my $stringify = $check && $check->stringify_got;
108              
109 138 100 100     565 return render_ref($got) if ref $got && !$stringify;
110              
111 94         265 return "$got";
112             }
113              
114             sub render_check {
115 158     158 1 205 my $self = shift;
116              
117 158         219 my $dne = $self->{+DNE};
118 158 100 100     405 return '' if $dne && $dne eq 'check';
119              
120 152         214 my $check = $self->{+CHK};
121 152 100       305 return '' unless defined $check;
122              
123 150         577 return $check->render;
124             }
125              
126             sub _full_id {
127 128     128   196 my ($type, $id) = @_;
128 128 100 100     653 return "<$id>" if !$type || $type eq 'META';
129 95 100       215 return $id if $type eq 'SCALAR';
130 87 100       240 return "{$id}" if $type eq 'HASH';
131 55 100       199 return "[$id]" if $type eq 'ARRAY';
132 17 50       72 return "$id()" if $type eq 'METHOD';
133 0         0 return "<$id>";
134             }
135              
136             sub _arrow_id {
137 137     137   220 my ($path, $type) = @_;
138 137 100       368 return '' unless $path;
139              
140 62 100 100     274 return ' ' if !$type || $type eq 'META'; # Meta gets a space, not an arrow
141              
142 47 100       97 return '->' if $type eq 'METHOD'; # Method always needs an arrow
143 40 100       88 return '->' if $type eq 'SCALAR'; # Scalar always needs an arrow
144 33 100       166 return '->' if $path =~ m/(>|\(\))$/; # Need an arrow after meta, or after a method
145 27 100       72 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     204 return '->' if $type =~ m/^(HASH|ARRAY)$/ && $path !~ m/(\]|\})$/;
149              
150             # No arrow needed
151 20         50 return '';
152             }
153              
154             sub _join_id {
155 122     122   379 my ($path, $parts) = @_;
156 122         209 my ($type, $key) = @$parts;
157              
158 122         245 my $id = _full_id($type, $key);
159 122         277 my $join = _arrow_id($path, $type);
160              
161 122         426 return "${path}${join}${id}";
162             }
163              
164             sub should_show {
165 179     179 1 320 my $self = shift;
166 179 100       531 return 1 unless $self->verified;
167 58   100     381 my $check = $self->check || return 0;
168 55 50       412 return 0 unless $check->lines;
169 55   100     259 my $file = $check->file || return 0;
170              
171 39         137 my $ctx = Test::Stream::Context::context();
172 39         139 my $cfile = $ctx->debug->file;
173 39         129 $ctx->release;
174 39 100       122 return 0 unless $file eq $cfile;
175              
176 38         170 return 1;
177             }
178              
179             sub filter_visible {
180 93     93 1 163 my $self = shift;
181              
182 93         114 my @deltas;
183 93         235 my @queue = (['', $self]);
184              
185 93         272 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     1630 my $children = $delta->children || next;
191 164 100       1187 next unless @$children;
192              
193 53         64 my @new;
194 53         104 for my $child (@$children) {
195 80         226 my $cpath = _join_id($path, $child->id);
196 80         236 push @new => [$cpath, $child];
197             }
198 53         231 unshift @queue => @new;
199             }
200              
201 93         240 return \@deltas;
202             }
203              
204 96 100   96 1 191 sub table_header { [map {$COLUMNS{$_}->{alias} || $_} @COLUMN_ORDER] };
  580         2062  
205              
206             sub table_op {
207 158     158 1 224 my $self = shift;
208              
209 158   100     404 my $check = $self->{+CHK} || return '!exists';
210              
211             return $check->operator($self->{+GOT})
212 152 100 100     820 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 232 my $self = shift;
219              
220 158   100     428 my $check = $self->{+CHK} || return '';
221 152   50     472 my $lines = $check->lines || return '';
222              
223 152 100       401 return '' unless @$lines;
224              
225 112         420 return join ', ' => @$lines;
226             }
227              
228             sub table_got_lines {
229 159     159 1 222 my $self = shift;
230              
231 159   100     408 my $check = $self->{+CHK} || return '';
232 153 100 100     428 return '' if $self->{+DNE} && $self->{+DNE} eq 'got';
233              
234 144         569 my @lines = $check->got_lines($self->{+GOT});
235 144 100       500 return '' unless @lines;
236              
237 6         29 return join ', ' => @lines;
238             }
239              
240             sub table_rows {
241 93     93 1 139 my $self = shift;
242              
243 93         212 my $deltas = $self->filter_visible;
244              
245 93         126 my @rows;
246 93         169 for my $set (@$deltas) {
247 156         361 my ($id, $d) = @$set;
248              
249 156         202 my @row;
250 156         292 for my $col (@COLUMN_ORDER) {
251 948         1461 my $spec = $COLUMNS{$col};
252 948         2053 my $val = $spec->{value}->($d, $id);
253 948 50       2257 $val = '' unless defined $val;
254 948         2007 push @row => $val;
255             }
256              
257 156         378 push @rows => \@row;
258             }
259              
260 93         245 return \@rows;
261             }
262              
263             sub table {
264 95     95 1 147 my $self = shift;
265              
266 95         115 my @out;
267              
268 95         224 my $header = $self->table_header;
269 95         296 my $rows = $self->table_rows;
270              
271 95 100       257 my $max = exists $ENV{TS_MAX_DELTA} ? $ENV{TS_MAX_DELTA} : 25;
272 95 100 66     513 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         228 my @no_collapse = grep { $COLUMNS{$COLUMN_ORDER[$_]}->{no_collapse} } 0 .. $#COLUMN_ORDER;
  574         1215  
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         1028 return @out;
294             }
295              
296             1;
297              
298             __END__