File Coverage

blib/lib/Devel/Git/MultiBisect/BuildTransitions.pm
Criterion Covered Total %
statement 20 180 11.1
branch 0 58 0.0
condition 0 9 0.0
subroutine 7 18 38.8
pod 4 5 80.0
total 31 270 11.4


line stmt bran cond sub pod time code
1             package Devel::Git::MultiBisect::BuildTransitions;
2 3     3   1603 use v5.14.0;
  3         25  
3 3     3   13 use warnings;
  3         5  
  3         72  
4 3     3   1123 use parent ( qw| Devel::Git::MultiBisect | );
  3         782  
  3         12  
5 3         110 use Devel::Git::MultiBisect::Auxiliary qw(
6             hexdigest_one_file
7             validate_list_sequence
8 3     3   135 );
  3         5  
9 3     3   15 use Carp;
  3         5  
  3         106  
10 3     3   12 use File::Spec;
  3         4  
  3         47  
11 3     3   12 use File::Temp qw( tempdir );
  3         3  
  3         5887  
12              
13             our $VERSION = '0.20';
14             $VERSION = eval $VERSION;
15              
16             =head1 NAME
17              
18             Devel::Git::MultiBisect::BuildTransitions - Gather build-time output where it changes over a range of F commits
19              
20             =head1 SYNOPSIS
21              
22             use Devel::Git::MultiBisect::BuildTransitions;
23              
24             $self = Devel::Git::MultiBisect::BuildTransitions->new(\%parameters);
25              
26             $commit_range = $self->get_commits_range();
27              
28             $self->multisect_builds();
29              
30             $multisected_outputs = $self->get_multisected_outputs();
31              
32             $transitions = $self->inspect_transitions();
33             }
34              
35             =head1 DESCRIPTION
36              
37             Whereas F is concerned with B
38             failures, F is concerned with
39             B phenomena: exceptions and warnings. We can identify three such
40             cases:
41              
42             =over 4
43              
44             =item * Build-time failures: C 'error'>
45              
46             While running your C-compiler over C source code via F, an exception may
47             be thrown which causes the build to fail. Over a large number of commits,
48             different exceptions may be thrown at various commits. Identify those
49             commits.
50              
51             =item * Build-time C-level warnings: C 'warning'>
52              
53             Your C-compiler may identify sub-optimal C source code and emit warnings.
54             Over a large number of commits, different warnings may be thrown at various
55             commits. Identify the commits where the warnings changed.
56              
57             =item * Build-time non-C-level warnings: C 'stderr'>
58              
59             At build time F is not limited to running a C compiler; it may also
60             execute statements in Perl, shell or other languages. Those statements may
61             themselves generate warnings. Identify the commits where the F output
62             from F changes.
63              
64             =back
65              
66             These three cases are distinguished by the C key-value pair passed to
67             C, the constructor, or to the C method described
68             below. The default value for C is C<'error'>, I to multisect
69             for build-time exceptions.
70              
71             =head1 METHODS
72              
73             =head2 C
74              
75             =over 4
76              
77             =item * Purpose
78              
79             Constructor.
80              
81             =item * Arguments
82              
83             $self = Devel::Git::MultiBisect::BuildTransitions->new(\%params);
84              
85             Reference to a hash, typically the return value of
86             C. Example:
87              
88             %args = (
89             gitdir => "~/gitwork/perl",
90             outputdir => tempdir(),
91             first => 'd4bf6b07402c770d61a5f8692f24fe944655d99f',
92             last => '9be343bf32d0921e5c792cbaa2b0038f43c6e463',
93             branch => 'blead',
94             configure_command => 'sh ./Configure -des -Dusedevel',
95             verbose => 1,
96             probe => 'stderr,
97             );
98             $params = Devel::Git::MultiBisect::opts::process_options(%args);
99             $self = Devel::Git::MultiBisect::BuildTransitions->new($params);
100              
101             =item * Return Value
102              
103             Object of Devel::Git::MultiBisect child class.
104              
105             =back
106              
107             =cut
108              
109             sub new {
110 0     0 1   my ($class, $params) = @_;
111              
112 0           my $data = Devel::Git::MultiBisect::Init::init($params);
113              
114 0           delete $data->{targets};
115 0           delete $data->{test_command};
116              
117 0           return bless $data, $class;
118             }
119              
120             =head2 C
121              
122             =over 4
123              
124             =item * Purpose
125              
126             With a given set of configuration options and a specified range of F
127             commits, identify the points where the output of the "build command" --
128             typically, F -- materially changed.
129              
130             A B would be either (a) the emergence or correction of
131             C-level exceptions (C); (b) the emergence or correction of C-level
132             warnings (C); (c) the emergence or correction of F output
133             emitted during F by Perl, shell or other non-C code (C).
134              
135             =item * B
136              
137             Up through version 0.19 of F, the
138             recommended (indeed, only) way to select among C, C and
139             C was to pass your choice as the value of a hash reference keyed on
140             C to the C method. As of version 0.20, the
141             recommended way is to provide the C 'value'> key-value pair as
142             one of the elements passed to C, the constructor (typically via
143             C). Beginning in
144             January 2022, C will no longer do anything with arguments
145             passed to it.
146              
147             =item * Arguments
148              
149             $self->multisect_builds();
150              
151             $self->multisect_builds({ probe => 'error' }); # DEPRECATED
152              
153             $self->multisect_builds({ probe => 'warning' }); # DEPRECATED
154              
155             $self->multisect_builds({ probe => 'stderr' }); # DEPRECATED
156              
157             =item * Return Value
158              
159             Returns true value upon success.
160              
161             =item * Comment
162              
163             As C runs it does two kinds of things:
164              
165             =over 4
166              
167             =item *
168              
169             It stores results data within the object which you can subsequently access
170             through method calls.
171              
172             =item *
173              
174             Depending on the value assigned to C, the method captures build-time
175             error messages (C) or warnings (C) from each commit run and
176             writes them to a file on disk for later human inspection. If you have
177             selected C 'stderr'>, all content directed to F is
178             written to that file.
179              
180             =back
181              
182             =item *
183              
184             If you are using F to diagnose
185             problems in the Perl 5 core distribution, C will take some
186             time to run, as F will have to be configured and built for each commit
187             run.
188              
189             =back
190              
191             =cut
192              
193             sub multisect_builds {
194 0     0 1   my ($self, $args) = @_;
195              
196             # Methods called within multisect_builds:
197             # _validate_multisect_builds_args
198             # _prepare_for_multisection
199             # get_commits_range
200             # run_build_on_one_commit
201             # _configure_one_commit
202             # _build_one_commit
203             # _filter_build_log
204             # _run_one_commit_and_assign
205             # _bisection_decision
206             # _evaluate_status_of_build_runs
207              
208 0           my $probe_validated = $self->_validate_multisect_builds_args($args);
209              
210             # Prepare data structures in the object to hold results of build runs on a
211             # per target, per commit basis.
212             # Also, "prime" the data structure by performing build runs for each target
213             # on the first and last commits in the commit range, storing that build
214             # output on disk as well.
215              
216 0           my $start_time = time();
217 0           my $all_outputs = $self->_prepare_for_multisection();
218              
219             # At this point, C<$all_outputs> is an array ref with one
220             # element per commit in the commit range. If a commit has been visited, the
221             # element is a hash ref with 4 key-value pairs like the ones below. If the
222             # commit has not yet been visited, the element is C.
223             #
224             # [
225             # {
226             # commit => "7c9c5138c6a704d1caf5908650193f777b81ad23",
227             # commit_short => "7c9c513",
228             # file => "/home/jkeenan/learn/perl/multisect/7c9c513.make.errors.rpt.txt",
229             # md5_hex => "d41d8cd98f00b204e9800998ecf8427e",
230             # },
231             # undef,
232             # undef,
233             # ...
234             # undef,
235             # {
236             # commit => "8f6628e3029399ac1e48dfcb59c3cd30e5127c3e",
237             # commit_short => "8f6628e",
238             # file => "/home/jkeenan/learn/perl/multisect/8f6628e.make.errors.rpt.txt",
239             # md5_hex => "fdce7ff2f07a0a8cd64005857f4060d4",
240             # },
241             # ]
242             #
243             # Unlike F -- where we could have been
244             # testing multiple test files on each commit -- here we're only concerned with
245             # recording the presence or absence of build-time errors. Hence, we only need
246             # an array of hash refs rather than an array of arrays of hash refs.
247             #
248             # The multisection process will entail running C over
249             # each commit selected by the multisection algorithm. Each run will insert a hash
250             # ref with the 4 KVPs into C<@{$self-E{all_outputs}}>. At the end of the
251             # multisection process those elements which we did not need to visit will still be
252             # C. We will then analyze the defined elements to identify the
253             # transitional commits.
254             #
255             # B
256             # build output> -- as reflected in a file on disk holding a list of normalized
257             # errors, normalized warnings or C -- B We are using
258             # an md5_hex value for that error file as a presumably valid unique identifier
259             # for that file's content. A transition point is a commit at which the output
260             # file's md5_hex differs from that of the immediately preceding commit. So, to
261             # identify the first transition point, we need to locate the commit at which the
262             # md5_hex changed from that found in the very first commit in the designated
263             # commit range. Once we've identified the first transition point, we'll look
264             # for the second transition point, i.e., that where the md5_hex changed from
265             # that observed at the first transition point. We'll continue that process
266             # until we get to a transition point where the md5_hex is identical to that of
267             # the very last commit in the commit range.
268              
269 0           my ($min_idx, $max_idx) = (0, $#{$self->{commits}});
  0            
270 0           my $this_target_status = 0;
271 0           my $current_start_idx = $min_idx;
272 0           my $current_end_idx = $max_idx;
273 0           my $overall_start_md5_hex = $self->{all_outputs}->[$min_idx]->{md5_hex};
274 0           my $overall_end_md5_hex = $self->{all_outputs}->[$max_idx]->{md5_hex};
275 0           my $n = 0;
276              
277 0           while (! $this_target_status) {
278              
279             # What gets (or may get) updated or assigned to in the course of one rep of this loop:
280             # $current_start_idx
281             # $current_end_idx
282             # $n
283             # $self->{all_outputs}
284              
285 0           my $h = sprintf("%d" => (($current_start_idx + $current_end_idx) / 2));
286 0           $self->_run_one_commit_and_assign($h);
287              
288 0           my $current_start_md5_hex = $self->{all_outputs}->[$current_start_idx]->{md5_hex};
289 0           my $target_h_md5_hex = $self->{all_outputs}->[$h]->{md5_hex};
290              
291             # Decision criteria:
292             # If $target_h_md5_hex eq $current_start_md5_hex, then the first
293             # transition is *after* index $h. Hence bisection should go upwards.
294              
295             # If $target_h_md5_hex ne $current_start_md5_hex, then the first
296             # transition has come *before* index $h. Hence bisection should go
297             # downwards. However, since the test of where the first transition is
298             # is that index j-1 has the same md5_hex as $current_start_md5_hex but
299             # index j has a different md5_hex, we have to do a run on
300             # j-1 as well.
301              
302             ($current_start_idx, $current_end_idx, $n) =
303             $self->_bisection_decision(
304             $target_h_md5_hex, $current_start_md5_hex, $h,
305             $self->{all_outputs},
306 0           $overall_end_md5_hex, $current_start_idx, $current_end_idx,
307             $max_idx, $n,
308             );
309 0           $this_target_status = $self->_evaluate_status_of_build_runs();
310             }
311              
312              
313 0           my $end_time = time();
314             my %timings = (
315             elapsed => $end_time - $start_time,
316 0           runs => scalar( grep {defined $_} @{$self->{all_outputs}} ),
  0            
  0            
317             );
318 0           $timings{mean} = sprintf("%.02f" => $timings{elapsed} / $timings{runs});
319 0 0         if ($self->{verbose}) {
320 0           say "Ran $timings{runs} runs; elapsed: $timings{elapsed} sec; mean: $timings{mean} sec";
321             }
322 0           $self->{timings} = \%timings;
323              
324 0           return 1;
325             }
326              
327             sub _validate_multisect_builds_args {
328 0     0     my ($self, $args) = @_;
329 0 0         if (defined $args) {
330 0 0         croak "Argument passed to multisect_builds() must be hashref"
331             unless ref($args) eq 'HASH';
332 0           my %good_keys = map {$_ => 1} (qw| probe |);
  0            
333 0           for my $k (keys %{$args}) {
  0            
334             croak "Invalid key '$k' in hashref passed to multisect_builds()"
335 0 0         unless $good_keys{$k};
336             }
337 0           my %good_values = map {$_ => 1} (qw| error warning stderr |);
  0            
338 0           for my $v (values %{$args}) {
  0            
339             croak "Invalid value '$v' in 'probe' element in hashref passed to multisect_builds()"
340 0 0         unless $good_values{$v};
341             }
342 0           $self->{probe} = $args->{probe};
343             }
344             else {
345             # If no $args passed to multisect_build(), then we rely on either
346             # the value for 'probe' provided by user to new() or the default value
347             # -- 'error' -- now provided in Devel::Git::MultiBisect::Opts.
348             }
349 0           return $self->{probe};
350             }
351              
352             sub _prepare_for_multisection {
353 0     0     my $self = shift;
354              
355             # get_commits_range is inherited from parent
356              
357 0           my $all_commits = $self->get_commits_range();
358 0           $self->{all_outputs} = [ (undef) x scalar(@{$all_commits}) ];
  0            
359              
360 0           my %multisected_outputs_table;
361 0           for my $idx (0, $#{$all_commits}) {
  0            
362              
363 0           my $outputs = $self->run_build_on_one_commit($all_commits->[$idx]);
364 0           $self->{all_outputs}->[$idx] = $outputs;
365             }
366 0           return $self->{all_outputs};
367             }
368              
369             sub run_build_on_one_commit {
370 0     0 0   my ($self, $commit) = @_;
371 0   0       $commit //= $self->{commits}->[0]->{sha};
372 0 0         say "Building commit: $commit" if ($self->{verbose});
373              
374 0           my $starting_branch = $self->_configure_one_commit($commit);
375              
376 0           my $outputsref = $self->_build_one_commit($commit);
377             say "Tested commit: $commit; returning to: $starting_branch"
378 0 0         if ($self->{verbose});
379              
380             # We want to return to our basic branch (e.g., 'master', 'blead')
381             # before checking out a new commit.
382              
383 0 0         system(qq|git checkout --quiet $starting_branch|)
384             and croak "Unable to 'git checkout --quiet $starting_branch";
385              
386 0           $self->{commit_counter}++;
387 0 0         say "Commit counter: $self->{commit_counter}" if $self->{verbose};
388              
389 0           return $outputsref;
390             }
391              
392             sub _build_one_commit {
393 0     0     my ($self, $commit) = @_;
394 0           my $short_sha = substr($commit,0,$self->{short});
395 0           my $command_raw = $self->{make_command};
396              
397             # If probe => error or probe => warning, we are capturing the entire
398             # (2>&1) output of 'make' in a file and then filtering that file (in
399             # _filter_build_log() for either C-level exceptions or C-level warnings.
400             # Hence, that file's name should end in 'make.output.txt'.
401             #
402             # If, however, probe => stderr, we are directly filtering the output of
403             # 'make' for STDERR and saving that in a file for subsequent
404             # commit-by-commit comparison of the STDERR output. Hence, the file for
405             # each commit should end in 'make.stderr.txt'.
406              
407 0           my ($build_log, $cmd);
408 0 0         if ($self->{probe} eq 'stderr') {
409             $build_log = File::Spec->catfile(
410             $self->{outputdir},
411 0           join('.' => (
412             $short_sha,
413             'make',
414             'stderr',
415             'txt'
416             )),
417             );
418 0           $cmd = qq|$command_raw 2>$build_log|;
419             }
420             else {
421             $build_log = File::Spec->catfile(
422             $self->{outputdir},
423 0           join('.' => (
424             $short_sha,
425             'make',
426             'output',
427             'txt'
428             )),
429             );
430 0           $cmd = qq|$command_raw >$build_log 2>&1|;
431             }
432 0 0         say "Actual 'make' command: $cmd" if $self->{verbose};
433 0           my $rv = system($cmd);
434 0           my $filtered_probes_file = $self->_filter_build_log($build_log, $short_sha);
435 0 0         say "Created $filtered_probes_file" if $self->{verbose};
436             return {
437 0           commit => $commit,
438             commit_short => $short_sha,
439             file => $filtered_probes_file,
440             md5_hex => hexdigest_one_file($filtered_probes_file),
441             };
442             }
443              
444             sub _filter_build_log {
445 0     0     my ($self, $buildlog, $short_sha) = @_;
446 0           my $tdir = tempdir( CLEANUP => 1 );
447              
448 0 0         if ($self->{probe} eq 'error') {
    0          
449             # the default case: probing for build-time errors
450 0           my $ackpattern = q|-A2 '^[^:]+:\d+:\d+:\s+error:'|;
451 0           my @raw_acklines = grep { ! m/^--\n/ } `ack $ackpattern $buildlog`;
  0            
452 0           chomp(@raw_acklines);
453 0 0         croak "Got incorrect count of lines from ack; should be divisible by 3"
454             unless scalar(@raw_acklines) % 3 == 0;
455              
456 0           my @refined_errors = ();
457 0           for (my $i=0; $i <= $#raw_acklines; $i += 3) {
458 0           my $j = $i + 2;
459 0           my @this_error = ();
460 0           my ($normalized) =
461             $raw_acklines[$i] =~ s/^([^:]+):\d+:\d+:(.*)$/$1:_:_:$2/r;
462 0           push @this_error, ($normalized, @raw_acklines[$i+1 .. $j]);
463 0           push @refined_errors, \@this_error;
464             }
465              
466             my $error_report_file =
467 0           File::Spec->catfile($self->{outputdir}, "$short_sha.make.errors.rpt.txt");
468 0           say "rpt: $error_report_file";
469 0 0         open my $OUT, '>', $error_report_file
470             or croak "Unable to open $error_report_file for writing";
471 0 0         if (@refined_errors) {
472 0           for (my $i=0; $i<=($#refined_errors -1); $i++) {
473 0           say $OUT join "\n" => @{$refined_errors[$i]};
  0            
474 0           say $OUT "--";
475             }
476 0           say $OUT join "\n" => @{$refined_errors[-1]};
  0            
477             }
478 0 0         close $OUT or croak "Unable to close $error_report_file after writing";
479 0           return $error_report_file;
480             }
481             elsif ($self->{probe} eq 'warning') {
482 0           my $ackpattern = qr/^
483             ([^:]+):
484             (\d+):
485             (\d+):\s+warning:\s+
486             (.*?)\s+\[-
487             (W.*)]$
488             /x;
489              
490 0           my @refined_warnings = ();
491 0 0         open my $IN, '<', $buildlog or croak "Unable to open $buildlog for reading";
492 0           while (my $l = <$IN>) {
493 0           chomp $l;
494 0 0         next unless $l =~ m/$ackpattern/;
495 0           my ($source, $line, $character, $text, $class) = ($1, $2, $3, $4, $5);
496 0           my $rl = "$source:_:_: warning: $text [$class]";
497 0           push @refined_warnings, $rl;
498             }
499 0 0         close $IN or croak "Unable to close $buildlog after reading";
500              
501             my $warning_report_file =
502 0           File::Spec->catfile($self->{outputdir}, "$short_sha.make.warnings.rpt.txt");
503 0 0         open my $OUT, '>', $warning_report_file
504             or croak "Unable to open $warning_report_file for writing";
505 0           say $OUT $_ for @refined_warnings;
506 0 0         close $OUT or croak "Unable to close $warning_report_file after writing";
507 0           return $warning_report_file;
508             }
509             else {
510             # $self->{probe} eq 'stderr'
511             # With this option, we simply record all STDERR from 'make' in the
512             # build log and return it.
513 0           return $buildlog;
514             }
515             }
516              
517             sub _evaluate_status_of_build_runs {
518 0     0     my ($self) = @_;
519 0           my @trans = ();
520 0           for my $o (@{$self->{all_outputs}}) {
  0            
521             push @trans,
522 0 0         defined $o ? $o->{md5_hex} : undef;
523             }
524 0           my $vls = validate_list_sequence(\@trans);
525 0 0 0       return ( (scalar(@{$vls}) == 1 ) and ($vls->[0])) ? 1 : 0;
526             }
527              
528             sub _run_one_commit_and_assign {
529              
530             # If we've already stashed a particular commit's outputs in all_outputs,
531             # then we don't need to actually perform a run.
532              
533             # This internal method assigns to all_outputs in place.
534              
535 0     0     my ($self, $idx) = @_;
536 0           my $this_commit = $self->{commits}->[$idx]->{sha};
537 0 0         unless (defined $self->{all_outputs}->[$idx]) {
538 0           say "\nAt commit counter $self->{commit_counter}, preparing to test commit ", $idx + 1, " of ", scalar(@{$self->{commits}})
539 0 0         if $self->{verbose};
540 0           my $these_outputs = $self->run_build_on_one_commit($this_commit);
541 0           $self->{all_outputs}->[$idx] = $these_outputs;
542             }
543             }
544              
545             =head2 C
546              
547             =over 4
548              
549             =item * Purpose
550              
551             Get results of C (other than test output files
552             created) reported on a per commit basis.
553              
554             =item * Arguments
555              
556             my $multisected_outputs = $self->get_multisected_outputs();
557              
558             None; all data needed is already present in the object.
559              
560             =item * Return Value
561              
562             Reference to an array with one element for each commit in the commit range.
563              
564             =over 4
565              
566             =item *
567              
568             If a particular commit B in the course of
569             C, then the array element is undefined. (The point
570             of multisection, of course, is to B have to visit every commit in the
571             commit range in order to figure out the commits at which test output changed.)
572              
573             =item *
574              
575             If a particular commit B in the course of
576             C, then the array element is a hash reference whose
577             elements have the following keys:
578              
579             commit
580             commit_short
581             file
582             md5_hex
583              
584             =back
585              
586             =back
587              
588             =cut
589              
590             sub get_multisected_outputs {
591 0     0 1   my $self = shift;
592 0           return $self->{all_outputs};
593             }
594              
595             =head2 C
596              
597             =over 4
598              
599             =item * Purpose
600              
601             Get a data structure which reports on the most meaningful results of
602             C, namely, the first commit, the last commit and all
603             transitional commits.
604              
605             =item * Arguments
606              
607             my $transitions = $self->inspect_transitions();
608              
609             None; all data needed is already present in the object.
610              
611             =item * Return Value
612              
613             Reference to a hash with 3 key-value pairs. Each element's value is another
614             hash reference. The elements of the top-level hash are:
615              
616             =over 4
617              
618             =item * C
619              
620             Value is reference to hash keyed on C, C and C, whose
621             values are, respectively, the index position of the very first commit in the
622             commit range, the digest of that commit's test output and the path to the file
623             holding that output.
624              
625             =item * C
626              
627             Value is reference to hash keyed on C, C and C, whose
628             values are, respectively, the index position of the very last commit in the
629             commit range, the digest of that commit's test output and the path to the file
630             holding that output.
631              
632             =item * C
633              
634             Value is reference to an array with one element for each transitional commit.
635             Each such element is a reference to a hash with keys C and C.
636             In this context C refers to the last commit in a sub-sequence with a
637             particular digest; C refers to the next immediate commit which is the
638             first commit in a new sub-sequence with a new digest.
639              
640             The values of C and C are, in turn, references to hashes with
641             keys C, C and C. Their values are, respectively, the index
642             position of the particular commit in the commit range, the digest of that
643             commit's test output and the path to the file holding that output.
644              
645             =back
646              
647             Example:
648              
649              
650             =item * Comment
651              
652             The return value of C should be useful to the developer
653             trying to determine the various points in a long series of commits where a
654             target's test output changed in meaningful ways. Hence, it is really the
655             whole point of F.
656              
657             =back
658              
659             =cut
660              
661             sub inspect_transitions {
662 0     0 1   my ($self) = @_;
663 0           my $multisected_outputs = $self->get_multisected_outputs();
664 0           my %transitions;
665 0           my $max_index = $#{$multisected_outputs};
  0            
666 0           $transitions{transitions} = [];
667             $transitions{oldest} = {
668             idx => 0,
669             md5_hex => $multisected_outputs->[0]->{md5_hex},
670             file => $multisected_outputs->[0]->{file},
671 0           };
672             $transitions{newest} = {
673             idx => $max_index,
674             md5_hex => $multisected_outputs->[$max_index]->{md5_hex},
675             file => $multisected_outputs->[$max_index]->{file},
676 0           };
677 0           for (my $j = 1; $j <= $max_index; $j++) {
678 0           my $i = $j - 1;
679             next unless (
680 0 0 0       (defined $multisected_outputs->[$i]) and
681             (defined $multisected_outputs->[$j])
682             );
683 0           my $older_md5_hex = $multisected_outputs->[$i]->{md5_hex};
684 0           my $newer_md5_hex = $multisected_outputs->[$j]->{md5_hex};
685 0           my $older_file = $multisected_outputs->[$i]->{file};
686 0           my $newer_file = $multisected_outputs->[$j]->{file};
687 0 0         unless ($older_md5_hex eq $newer_md5_hex) {
688 0           push @{$transitions{transitions}}, {
  0            
689             older => { idx => $i, md5_hex => $older_md5_hex, file => $older_file },
690             newer => { idx => $j, md5_hex => $newer_md5_hex, file => $newer_file },
691             }
692             }
693             }
694 0           return \%transitions;
695             }
696              
697             1;
698              
699             __END__