File Coverage

blib/lib/App/NDTools/NDDiff.pm
Criterion Covered Total %
statement 280 284 98.5
branch 101 104 97.1
condition 34 36 94.4
subroutine 35 39 89.7
pod 0 19 0.0
total 450 482 93.3


line stmt bran cond sub pod time code
1             package App::NDTools::NDDiff;
2              
3 3     3   1938 use strict;
  3         7  
  3         136  
4 3     3   19 use warnings FATAL => 'all';
  3         5  
  3         107  
5 3     3   16 use parent 'App::NDTools::NDTool';
  3         13  
  3         20  
6              
7 3     3   1346 use Algorithm::Diff qw(compact_diff);
  3         9929  
  3         203  
8 3     3   23 use JSON qw();
  3         7  
  3         61  
9 3     3   14 use App::NDTools::Slurp qw(s_dump);
  3         7  
  3         170  
10 3     3   22 use Log::Log4Cli 0.18;
  3         78  
  3         250  
11 3     3   1522 use Struct::Diff 0.96 qw();
  3         20599  
  3         132  
12 3     3   28 use Struct::Path 0.80 qw(path path_delta);
  3         58  
  3         227  
13 3     3   1829 use Struct::Path::PerlStyle 0.80 qw(str2path path2str);
  3         508719  
  3         249  
14 3     3   26 use Term::ANSIColor qw(color);
  3         7  
  3         8276  
15              
16             our $VERSION = '0.46';
17              
18             my $JSON = JSON->new->canonical->allow_nonref;
19             my %COLOR;
20              
21             sub arg_opts {
22 66     66 0 133 my $self = shift;
23              
24             return (
25             $self->SUPER::arg_opts(),
26             'A!' => \$self->{OPTS}->{diff}->{A},
27             'N!' => \$self->{OPTS}->{diff}->{N},
28             'O!' => \$self->{OPTS}->{diff}->{O},
29             'R!' => \$self->{OPTS}->{diff}->{R},
30             'U!' => \$self->{OPTS}->{diff}->{U},
31 4     4   5999 'brief' => sub { $self->{OPTS}->{ofmt} = $_[0] },
32             'colors!' => \$self->{OPTS}->{colors},
33             'ctx-text=i' => \$self->{OPTS}->{'ctx-text'},
34             'full' => \$self->{OPTS}->{full}, # deprecated
35             'full-headers' => \$self->{OPTS}->{'full-headers'},
36             'grep=s@' => \$self->{OPTS}->{grep},
37 9     9   13984 'json' => sub { $self->{OPTS}->{ofmt} = $_[0] },
38             'ignore=s@' => \$self->{OPTS}->{ignore},
39 1     1   1406 'rules' => sub { $self->{OPTS}->{ofmt} = $_[0] },
40             'quiet|q' => \$self->{OPTS}->{quiet},
41             'show' => \$self->{OPTS}->{show},
42             )
43 66         266 }
44              
45             sub check_args {
46 66     66 0 114 my $self = shift;
47              
48 66 100       248 if ($self->{OPTS}->{show}) {
    100          
49 3 50       13 die_fatal "At least one argument expected when --show used", 1
50             unless (@_);
51             } elsif (@_ < 2) {
52 1         5 die_fatal "At least two arguments expected for diff", 1;
53             }
54              
55 65         142 return $self;
56             }
57              
58             sub configure {
59 66     66 0 1494 my $self = shift;
60              
61 66         245 $self->SUPER::configure();
62              
63             $self->{OPTS}->{colors} = $self->{TTY}
64 65 100       262 unless (defined $self->{OPTS}->{colors});
65              
66             # resolve colors
67 65         130 while (my ($k, $v) = each %{$self->{OPTS}->{term}->{line}}) {
  520         1985  
68 455 100       865 if ($self->{OPTS}->{colors}) {
69 21         50 $COLOR{$k} = color($v);
70 21         394 $COLOR{"B$k"} = color("bold $v");
71             } else {
72 434         1083 $COLOR{$k} = $COLOR{"B$k"} = '';
73             }
74             }
75              
76             $COLOR{head} = $self->{OPTS}->{colors}
77 65 100       188 ? color($self->{OPTS}->{term}->{head}) : "";
78 65 100       238 $COLOR{reset} = $self->{OPTS}->{colors} ? color('reset') : "";
79              
80             # resolve paths
81 65         151 for (@{$self->{OPTS}->{grep}}, @{$self->{OPTS}->{ignore}}) {
  65         145  
  65         176  
82 7         15 my $tmp = eval { str2path($_) };
  7         42  
83 7 100       26331 die_fatal "Failed to parse '$_'", 4 if ($@);
84 6         16 $_ = $tmp;
85             }
86              
87 64         169 return $self;
88             }
89              
90             sub defaults {
91 66     66 0 132 my $self = shift;
92              
93             my $out = {
94 66         118 %{$self->SUPER::defaults()},
  66         226  
95             'ctx-text' => 3,
96             'term' => {
97             'head' => 'yellow',
98             'line' => {
99             'A' => 'green',
100             'D' => 'yellow',
101             'U' => 'white',
102             'R' => 'red',
103             '@' => 'magenta',
104             },
105             'sign' => {
106             'A' => '+ ',
107             'D' => '! ',
108             'U' => ' ',
109             'R' => '- ',
110             '@' => ' ',
111             },
112             },
113             'ofmt' => 'term',
114             'diff' => {
115             'A' => 1,
116             'N' => 1,
117             'O' => 1,
118             'R' => 1,
119             'U' => 0,
120             },
121             };
122              
123 66         302 $out->{term}{line}{N} = $out->{term}{line}{A};
124 66         148 $out->{term}{line}{O} = $out->{term}{line}{R};
125 66         134 $out->{term}{sign}{N} = $out->{term}{sign}{A};
126 66         151 $out->{term}{sign}{O} = $out->{term}{sign}{R};
127              
128 66         226 return $out;
129             }
130              
131             sub diff {
132 59     59 0 124 my ($self, $old, $new) = @_;
133              
134 59 100       153 if ($self->{OPTS}->{full}) {
135             log_alert {
136 1     1   74 '--full opt is deprecated and will be removed soon. ' .
137             '--U should be used instead'
138 1         11 };
139 1         7 map {$self->{OPTS}->{diff}->{$_} = 1 } keys %{$self->{OPTS}->{diff}};
  5         11  
  1         7  
140             }
141              
142 59     0   381 log_debug { "Calculating diff for structure" };
  0         0  
143             my $diff = Struct::Diff::diff(
144             $old, $new,
145 64         330 map { ("no$_" => 1) } grep { !$self->{OPTS}->{diff}->{$_} }
  295         633  
146 59         312 keys %{$self->{OPTS}->{diff}},
  59         247  
147             );
148              
149             # retrieve result from wrapper (see load() for more info)
150 59 100       57869 if (exists $diff->{D}) {
    100          
151 54         175 $diff = $diff->{D}->[0];
152             } elsif (exists $diff->{U}) {
153 1         3 $diff->{U} = $diff->{U}->[0];
154             }
155              
156 59 100       358 $self->diff_term($diff) if ($self->{OPTS}->{ofmt} eq 'term');
157              
158 59         233 return $diff;
159             }
160              
161             sub diff_term {
162 45     45 0 108 my ($self, $diff) = @_;
163              
164 45     0   293 log_debug { "Calculating diffs for text values" };
  0         0  
165              
166 45         214 my $dref; # ref to diff
167 45         157 my @list = Struct::Diff::list_diff($diff);
168              
169 45         2618 while (@list) {
170 72         154 (undef, $dref) = splice @list, 0, 2;
171              
172 72 100       163 next unless (exists ${$dref}->{N});
  72         214  
173 47 100 66     67 next if (ref ${$dref}->{O} or ref ${$dref}->{N});
  47         146  
  42         133  
174              
175 42 100       71 my @old = split($/, ${$dref}->{O}, -1) if (${$dref}->{O});
  37         195  
  42         477  
176 42 50       79 my @new = split($/, ${$dref}->{N}, -1) if (${$dref}->{N});
  42         179  
  42         330  
177              
178 42 100 100     188 if (@old > 1 or @new > 1) {
179 23         37 delete ${$dref}->{O};
  23         61  
180 23         38 delete ${$dref}->{N};
  23         41  
181              
182 23 100 100     102 if ($old[-1] eq '' and $new[-1] eq '') {
183 13         20 pop @old; # because split by newline and -1 for LIMIT
184 13         26 pop @new; # -"-
185             }
186              
187 23         88 my @cdiff = compact_diff(\@old, \@new);
188 23         5996 my ($match, $header);
189              
190 23         70 while (@cdiff > 2) {
191 104         275 my @del = @old[$cdiff[0] .. $cdiff[2] - 1];
192 104         240 my @add = @new[$cdiff[1] .. $cdiff[3] - 1];
193              
194 104 100       244 if ($match = !$match) {
195             # trailing context
196 56 100       123 if ($header) {
197 33         79 my @tail = splice @del, 0, $self->{OPTS}->{'ctx-text'};
198 33         59 push @{${$dref}->{T}}, 'U', \@tail;
  33         48  
  33         92  
199              
200 33         59 $header->[1] += @tail;
201 33         59 $header->[3] += @tail;
202             }
203              
204             # leading context
205 56 100       127 if (@cdiff > 4) {
206             my @rest = splice @del, 0, $self->{OPTS}->{'ctx-text'}
207 48 100       157 ? $self->{OPTS}->{'ctx-text'} * -1 : scalar @del;
208              
209 48 100 100     176 if (@rest or !$header) {
210 30         51 push @{${$dref}->{T}}, '@', $header = [
  30         49  
  30         149  
211             $cdiff[2] - @del + 1, 0,
212             $cdiff[3] - @del + 1, 0,
213             ];
214             }
215              
216 48 100       124 if (@del) {
217 17         25 push @{${$dref}->{T}}, 'U', \@del;
  17         28  
  17         41  
218 17         28 $header->[1] += @del;
219 17         33 $header->[3] += @del;
220             }
221             }
222             } else {
223 48 100       99 if (@del) {
224 40         59 push @{${$dref}->{T}}, 'R', \@del;
  40         67  
  40         100  
225 40         75 $header->[1] += @del;
226             }
227              
228 48 100       94 if (@add) {
229 40         68 push @{${$dref}->{T}}, 'A', \@add;
  40         57  
  40         93  
230 40         65 $header->[3] += @add;
231             }
232             }
233              
234 104         334 splice @cdiff, 0, 2;
235             }
236             }
237             }
238              
239 45         95 return $self;
240             }
241              
242             sub dump {
243 57     57 0 142 my ($self, $diff) = @_;
244              
245 57     0   348 log_debug { "Dumping results" };
  0         0  
246              
247 57 100       357 if ($self->{OPTS}->{ofmt} eq 'term') {
    100          
    100          
248 42         126 $self->dump_term($diff);
249             } elsif ($self->{OPTS}->{ofmt} eq 'brief') {
250 4         50 $self->dump_brief($diff);
251             } elsif ($self->{OPTS}->{ofmt} eq 'rules') {
252 1         17 $self->dump_rules($diff);
253             } else {
254             s_dump(\*STDOUT, $self->{OPTS}->{ofmt},
255 10         200 {pretty => $self->{OPTS}->{pretty}}, $diff);
256             }
257              
258 57         190 return $self;
259             }
260              
261             sub dump_brief {
262 4     4 0 11 my ($self, $diff) = @_;
263              
264 4         8 my ($path, $dref, $tag);
265 4         14 my @list = Struct::Diff::list_diff($diff, sort => 1);
266              
267 4         276 while (@list) {
268 10         27 ($path, $dref) = splice @list, 0, 2;
269 10         28 for $tag (qw{R N A}) {
270             $self->print_brief_block($path, $tag)
271 30 100       48 if (exists ${$dref}->{$tag});
  30         102  
272             }
273             }
274             }
275              
276             sub dump_rules {
277 1     1 0 3 my ($self, $diff) = @_;
278              
279 1         2 my ($path, $dref, $item, @out);
280 1         5 my @list = Struct::Diff::list_diff($diff, sort => 1);
281              
282 1         95 while (@list) {
283 4         10 ($path, $dref) = splice @list, 0, 2;
284 4         12 for (qw{R N A}) {
285 12 100       17 next unless (exists ${$dref}->{$_});
  12         30  
286             unshift @out, {
287             modname => $_ eq "R" ? "Remove" : "Insert",
288             path => $self->dump_rules_path($path),
289 4 50       17 value => ${$dref}->{$_}
  4         271  
290             };
291             }
292             }
293              
294 1         8 s_dump(\*STDOUT, 'JSON', {pretty => $self->{OPTS}->{pretty}}, \@out);
295             }
296              
297             sub dump_rules_path { # to be able to override
298 4     4 0 16 return path2str($_[1]);
299             }
300              
301             sub dump_term {
302 42     42 0 88 my ($self, $diff) = @_;
303              
304 42         74 my ($path, $dref, $tag);
305 42         111 my @list = Struct::Diff::list_diff($diff, sort => 1);
306              
307 42         2451 while (@list) {
308 69         183 ($path, $dref) = splice @list, 0, 2;
309 69         174 for $tag (qw{R O N A T}) {
310 89         268 $self->print_term_block(${$dref}->{$tag}, $path, $tag)
311 345 100       533 if (exists ${$dref}->{$tag});
  345         949  
312             }
313             }
314             }
315              
316             sub exec {
317 59     59 0 112 my $self = shift;
318 59         90 my @items;
319              
320 59         109 while (@{$self->{ARGV}}) {
  173         505  
321 115         185 my $name = shift @{$self->{ARGV}};
  115         224  
322 115         295 my $data = $self->load($name);
323              
324 115         187 my $diff;
325              
326 115 100       263 if ($self->{OPTS}->{show}) {
327 3         14 $self->print_term_header($name);
328 3         8 $diff = $data->[0];
329              
330 3 100       33 if (my @errs = Struct::Diff::valid_diff($diff)) {
331 1         80 while (@errs) {
332 2         107 my ($path, $type) = splice @errs, 0, 2;
333 2     2   16 log_error { "$name: $type " . path2str($path) };
  2         95  
334             }
335              
336 1         74 die_fatal "Diff validation failed", 1;
337             }
338             } else {
339 112         383 push @items, { name => $name, data => $data };
340 112 100       314 next unless (@items > 1);
341              
342 56         234 $self->print_term_header($items[0]->{name}, $items[1]->{name});
343              
344 56         183 $diff = $self->diff($items[0]->{data}, $items[1]->{data});
345              
346 56         112 shift @items;
347             }
348              
349 58 100       691 $self->dump($diff) unless ($self->{OPTS}->{quiet});
350              
351 58 100 100     127 $self->{status} = 8 unless (not keys %{$diff} or exists $diff->{U});
  58         588  
352             }
353              
354 58 100       164 die_info "All done, no difference found", 0 unless ($self->{status});
355 54         249 die_info "Difference found", 8;
356             }
357              
358             sub load {
359 121     121 0 633 my $self = shift;
360              
361 121         455 my @data = $self->load_struct($_[0], $self->{OPTS}->{ifmt});
362              
363             # array used to indicate absent value for grep result
364             @data = $self->grep($self->{OPTS}->{grep}, $data[0])
365 121 100       220 if (@{$self->{OPTS}->{grep}});
  121         404  
366              
367 121 100 100     580 if (@data and ref $data[0]) {
368 104         199 map { path($data[0], $_, delete => 1) } @{$self->{OPTS}->{ignore}}
  4         25  
  104         296  
369             }
370              
371 121         1529 return \@data;
372             }
373              
374             sub print_brief_block {
375 10     10 0 23 my ($self, $path, $status) = @_;
376              
377 10 100       26 $status = 'D' if ($status eq 'N');
378              
379             my $line = $self->{OPTS}->{term}->{sign}->{$status} .
380 10         29 $COLOR{U} . path2str([splice @{$path}, 0, -1]) . $COLOR{reset};
  10         42  
381 10         342 $line .= $COLOR{"B$status"} . path2str($path) . $COLOR{reset};
382              
383 10         597 print $line . "\n";
384             }
385              
386             sub print_term_block {
387 89     89 0 247 my ($self, $value, $path, $status) = @_;
388              
389 89     0   645 log_trace { "'" . path2str($path) . "' ($status)" };
  0         0  
390              
391 89         479 my @lines;
392 89         237 my $dsign = $self->{OPTS}->{term}->{sign}->{$status};
393              
394             # diff for path
395 89 100 100     148 if (@{$path} and my @delta = path_delta($self->{'hdr_path'}, $path)) {
  89         503  
396 62         2457 $self->{'hdr_path'} = [@{$path}];
  62         193  
397 62         131 for (my $s = 0; $s < @{$path}; $s++) {
  172         420  
398 110 100 100     280 next if (not $self->{OPTS}->{'full-headers'} and $s < @{$path} - @delta);
  104         385  
399              
400 103         444 my $line = " " x $s . path2str([$path->[$s]]);
401 103 100 100     5547 if (($status eq 'A' or $status eq 'R') and $s == $#{$path}) {
  36   100     153  
402 17         70 $line = $COLOR{"B$status"} . $dsign . $line . $COLOR{reset};
403             } else {
404 86         229 $line = " $line";
405             }
406              
407 103         244 push @lines, $line;
408             }
409             }
410              
411             # diff for value
412 89         1500 push @lines, $self->term_value_diff($value, $status, " " x @{$path});
  89         289  
413              
414 89         2754 print join("\n", @lines) . "\n";
415             }
416              
417             sub print_term_header {
418 64     64 0 9975 my ($self, @names) = @_;
419              
420 64 100 100     240 if ($self->{TTY} and not $self->{OPTS}->{quiet}) {
421             print $COLOR{head} .
422             (@names == 1 ? "!!! $names[0]" : "--- $names[0]\n+++ $names[1]") .
423 3 100       115 $COLOR{reset}. "\n";
424             }
425             }
426              
427             sub term_value_diff {
428 89     89 0 290 my ($self, $value, $status, $indent) = @_;
429              
430 89 100       254 return $self->term_value_diff_text($value, $indent)
431             if ($status eq 'T');
432              
433 68         159 return $self->term_value_diff_default($value, $status, $indent);
434             }
435              
436             sub term_value_diff_default {
437 68     68 0 137 my ($self, $value, $status, $indent) = @_;
438 68         98 my @out;
439              
440 68 100 66     349 $value = $JSON->pretty($self->{OPTS}->{pretty})->encode($value)
441             if (ref $value or not defined $value);
442              
443 68         939 for my $line (split($/, $value)) {
444 82         370 substr($line, 0, 0, $self->{OPTS}->{term}->{sign}->{$status} . $indent);
445 82         273 push @out, $COLOR{$status} . $line . $COLOR{reset};
446             }
447              
448 68         208 return @out;
449             }
450              
451             sub term_value_diff_text {
452 21     21 0 50 my ($self, $diff, $indent) = @_;
453 21         39 my (@hdr, $lines, @out, $pfx, $sfx, $status);
454              
455 21         47 $sfx = $COLOR{reset};
456              
457 21         39 while (@{$diff}) {
  175         355  
458 154         216 ($status, $lines) = splice @{$diff}, 0, 2;
  154         330  
459              
460 154         387 $pfx = $COLOR{$status} . $self->{OPTS}->{term}->{sign}->{$status} .
461             $indent;
462              
463 154 100       298 if ($status eq '@') {
464 28         42 @hdr = splice @{$lines};
  28         68  
465 28 100       181 $lines->[0] = "@@ -$hdr[0]" . ($hdr[1] > 1 ? ",$hdr[1] " : "") .
    100          
466             " +$hdr[2]" . ($hdr[3] > 1 ? ",$hdr[3] @@" : " @@");
467             }
468              
469 154         226 map { substr($_ , 0, 0, $pfx); $_ .= $sfx; push @out, $_ } @{$lines};
  244         534  
  244         350  
  244         462  
  154         244  
470             }
471              
472 21         122 return @out;
473             }
474              
475             1; # End of App::NDTools::NDDiff