File Coverage

blib/lib/App/NDTools/NDDiff.pm
Criterion Covered Total %
statement 285 289 98.6
branch 106 106 100.0
condition 39 39 100.0
subroutine 36 40 90.0
pod 0 19 0.0
total 466 493 94.5


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