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   627 use strict;
  100         108  
  100         2231  
3 100     100   294 use warnings;
  100         100  
  100         2887  
4              
5             use Test::Stream::HashBase(
6 100         702 accessors => [qw/verified id got chk children dne exception/]
7 100     100   328 );
  100         94  
8              
9 100     100   731 use Test::Stream::Table();
  100         101  
  100         1048  
10 100     100   650 use Test::Stream::Context();
  100         101  
  100         1285  
11              
12 100     100   274 use Test::Stream::Util qw/render_ref/;
  100         92  
  100         451  
13 100     100   390 use Scalar::Util qw/reftype blessed refaddr/;
  100         104  
  100         4417  
14              
15 100     100   319 use Carp qw/croak/;
  100         104  
  100         3459  
16              
17             # 'CHECK' constant would not work, but I like exposing 'check()' to people
18             # using this class.
19             BEGIN {
20 100     100   331 no warnings 'once';
  100         103  
  100         3874  
21 100     100   203 *check = \&chk;
22 100         129617 *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 6 my $class = shift;
38 5         5 my $header = shift;
39 5         7 @COLUMN_ORDER = grep { $_ ne $header } @COLUMN_ORDER;
  33         38  
40 5 100       20 delete $COLUMNS{$header} ? 1 : 0;
41             }
42              
43             sub add_column {
44 7     7 1 38 my $class = shift;
45 7         4 my $name = shift;
46              
47 7 100       147 croak "Column name is required"
48             unless $name;
49              
50             croak "Column '$name' is already defined"
51 6 100       115 if $COLUMNS{$name};
52              
53 5         4 my %params;
54 5 100       14 if (@_ == 1) {
55 3         7 %params = (value => @_, name => $name);
56             }
57             else {
58 2         8 %params = (@_, name => $name);
59             }
60              
61 5         7 my $value = $params{value};
62              
63 5 100       117 croak "You must specify a 'value' callback"
64             unless $value;
65              
66 4 100 66     96 croak "'value' callback must be a CODE reference"
67             unless ref($value) && reftype($value) eq 'CODE';
68              
69 3 100       5 if ($params{prefix}) {
70 1         3 unshift @COLUMN_ORDER => $name;
71             }
72             else {
73 2         4 push @COLUMN_ORDER => $name;
74             }
75              
76 3         8 $COLUMNS{$name} = \%params;
77             }
78              
79             sub init {
80 249     249 0 213 my $self = shift;
81              
82             croak "Cannot specify both 'check' and 'chk' as arguments"
83 249 100 66     787 if exists($self->{check}) && exists($self->{+CHK});
84              
85             # Allow 'check' as an argument
86             $self->{+CHK} ||= delete $self->{check}
87 248 100 66     1032 if exists $self->{check};
88             }
89              
90             sub render_got {
91 161     161 1 124 my $self = shift;
92              
93 161         135 my $exp = $self->{+EXCEPTION};
94 161 100       257 if ($exp) {
95 1         2 chomp($exp = "$exp");
96 1         2 $exp =~ s/\n.*$//g;
97 1         4 return "";
98             }
99              
100 160         128 my $dne = $self->{+DNE};
101 160 100 100     323 return '' if $dne && $dne eq 'got';
102              
103 151         127 my $got = $self->{+GOT};
104 151 100       235 return '' unless defined $got;
105              
106 139         103 my $check = $self->{+CHK};
107 139   100     416 my $stringify = $check && $check->stringify_got;
108              
109 139 100 100     407 return render_ref($got) if ref $got && !$stringify;
110              
111 95         149 return "$got";
112             }
113              
114             sub render_check {
115 159     159 1 138 my $self = shift;
116              
117 159         142 my $dne = $self->{+DNE};
118 159 100 100     266 return '' if $dne && $dne eq 'check';
119              
120 153         119 my $check = $self->{+CHK};
121 153 100       223 return '' unless defined $check;
122              
123 151         318 return $check->render;
124             }
125              
126             sub _full_id {
127 128     128   109 my ($type, $id) = @_;
128 128 100 100     429 return "<$id>" if !$type || $type eq 'META';
129 95 100       135 return $id if $type eq 'SCALAR';
130 87 100       153 return "{$id}" if $type eq 'HASH';
131 55 100       132 return "[$id]" if $type eq 'ARRAY';
132 17 50       40 return "$id()" if $type eq 'METHOD';
133 0         0 return "<$id>";
134             }
135              
136             sub _arrow_id {
137 137     137   130 my ($path, $type) = @_;
138 137 100       215 return '' unless $path;
139              
140 62 100 100     185 return ' ' if !$type || $type eq 'META'; # Meta gets a space, not an arrow
141              
142 47 100       80 return '->' if $type eq 'METHOD'; # Method always needs an arrow
143 40 100       63 return '->' if $type eq 'SCALAR'; # Scalar always needs an arrow
144 33 100       130 return '->' if $path =~ m/(>|\(\))$/; # Need an arrow after meta, or after a method
145 27 100       36 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     156 return '->' if $type =~ m/^(HASH|ARRAY)$/ && $path !~ m/(\]|\})$/;
149              
150             # No arrow needed
151 20         32 return '';
152             }
153              
154             sub _join_id {
155 122     122   236 my ($path, $parts) = @_;
156 122         126 my ($type, $key) = @$parts;
157              
158 122         163 my $id = _full_id($type, $key);
159 122         151 my $join = _arrow_id($path, $type);
160              
161 122         250 return "${path}${join}${id}";
162             }
163              
164             sub should_show {
165 180     180 1 151 my $self = shift;
166 180 100       310 return 1 unless $self->verified;
167 58   100     238 my $check = $self->check || return 0;
168 55 50       233 return 0 unless $check->lines;
169 55   100     144 my $file = $check->file || return 0;
170              
171 39         78 my $ctx = Test::Stream::Context::context();
172 39         100 my $cfile = $ctx->debug->file;
173 39         78 $ctx->release;
174 39 100       72 return 0 unless $file eq $cfile;
175              
176 38         119 return 1;
177             }
178              
179             sub filter_visible {
180 94     94 1 82 my $self = shift;
181              
182 94         64 my @deltas;
183 94         139 my @queue = (['', $self]);
184              
185 94         178 while (my $set = shift @queue) {
186 174         186 my ($path, $delta) = @$set;
187              
188 174 100       205 push @deltas => [$path, $delta] if $delta->should_show;
189              
190 174   100     756 my $children = $delta->children || next;
191 165 100       721 next unless @$children;
192              
193 53         44 my @new;
194 53         65 for my $child (@$children) {
195 80         139 my $cpath = _join_id($path, $child->id);
196 80         144 push @new => [$cpath, $child];
197             }
198 53         171 unshift @queue => @new;
199             }
200              
201 94         149 return \@deltas;
202             }
203              
204 97 100   97 1 128 sub table_header { [map {$COLUMNS{$_}->{alias} || $_} @COLUMN_ORDER] };
  586         1273  
205              
206             sub table_op {
207 159     159 1 125 my $self = shift;
208              
209 159   100     250 my $check = $self->{+CHK} || return '!exists';
210              
211             return $check->operator($self->{+GOT})
212 153 100 100     597 unless $self->{+DNE} && $self->{+DNE} eq 'got';
213              
214 9         18 return $check->operator();
215             }
216              
217             sub table_check_lines {
218 159     159 1 136 my $self = shift;
219              
220 159   100     235 my $check = $self->{+CHK} || return '';
221 153   50     310 my $lines = $check->lines || return '';
222              
223 153 100       240 return '' unless @$lines;
224              
225 112         235 return join ', ' => @$lines;
226             }
227              
228             sub table_got_lines {
229 160     160 1 135 my $self = shift;
230              
231 160   100     244 my $check = $self->{+CHK} || return '';
232 154 100 100     294 return '' if $self->{+DNE} && $self->{+DNE} eq 'got';
233              
234 145         357 my @lines = $check->got_lines($self->{+GOT});
235 145 100       303 return '' unless @lines;
236              
237 6         21 return join ', ' => @lines;
238             }
239              
240             sub table_rows {
241 94     94 1 92 my $self = shift;
242              
243 94         142 my $deltas = $self->filter_visible;
244              
245 94         75 my @rows;
246 94         102 for my $set (@$deltas) {
247 157         206 my ($id, $d) = @$set;
248              
249 157         115 my @row;
250 157         167 for my $col (@COLUMN_ORDER) {
251 954         753 my $spec = $COLUMNS{$col};
252 954         1055 my $val = $spec->{value}->($d, $id);
253 954 50       1304 $val = '' unless defined $val;
254 954         1103 push @row => $val;
255             }
256              
257 157         231 push @rows => \@row;
258             }
259              
260 94         169 return \@rows;
261             }
262              
263             sub table {
264 96     96 1 104 my $self = shift;
265              
266 96         90 my @out;
267              
268 96         135 my $header = $self->table_header;
269 96         177 my $rows = $self->table_rows;
270              
271 96 100       205 my $max = exists $ENV{TS_MAX_DELTA} ? $ENV{TS_MAX_DELTA} : 25;
272 96 100 66     367 if ($max && @$rows > $max) {
273 1         3 @$rows = @{$rows}[0 .. ($max - 1)];
  1         3  
274 1         6 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 96         147 my @no_collapse = grep { $COLUMNS{$COLUMN_ORDER[$_]}->{no_collapse} } 0 .. $#COLUMN_ORDER;
  580         677  
284 96         293 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 96         650 return @out;
294             }
295              
296             1;
297              
298             __END__