File Coverage

blib/lib/Devel/Git/MultiBisect/Transitions.pm
Criterion Covered Total %
statement 29 147 19.7
branch 0 26 0.0
condition 0 9 0.0
subroutine 10 17 58.8
pod 3 3 100.0
total 42 202 20.7


line stmt bran cond sub pod time code
1             package Devel::Git::MultiBisect::Transitions;
2 1     1   776 use strict;
  1         3  
  1         38  
3 1     1   6 use warnings;
  1         2  
  1         36  
4 1     1   17 use v5.14.0;
  1         3  
5 1     1   5 use parent ( qw| Devel::Git::MultiBisect | );
  1         3  
  1         8  
6 1     1   90 use Devel::Git::MultiBisect::Opts qw( process_options );
  1         2  
  1         54  
7 1         61 use Devel::Git::MultiBisect::Auxiliary qw(
8             clean_outputfile
9             hexdigest_one_file
10             validate_list_sequence
11 1     1   7 );
  1         3  
12 1     1   6 use Carp;
  1         3  
  1         78  
13 1     1   6 use Cwd;
  1         2  
  1         61  
14 1     1   7 use File::Temp;
  1         2  
  1         65  
15 1     1   6 use List::Util qw(first sum);
  1         3  
  1         1721  
16              
17             our $VERSION = '0.16';
18              
19             =head1 NAME
20              
21             Devel::Git::MultiBisect::Transitions - Gather test output where it changes over a range of F commits
22              
23             =head1 SYNOPSIS
24              
25             use Devel::Git::MultiBisect::Transitions;
26              
27             $self = Devel::Git::MultiBisect::Transitions->new(\%parameters);
28              
29             $commit_range = $self->get_commits_range();
30              
31             $full_targets = $self->set_targets(\@target_args);
32              
33             $self->multisect_all_targets();
34              
35             $multisected_outputs = $self->get_multisected_outputs();
36              
37             $transitions = $self->inspect_transitions();
38             }
39              
40             =head1 DESCRIPTION
41              
42             Given a Perl library or application kept in F for version control, it is
43             often useful to be able to compare the output collected from running one or
44             several test files over a range of F commits. If that range is sufficiently
45             large, a test may fail in B over that range.
46              
47             If that is the case, then simply asking, I<"When did this file start to
48             fail?"> is insufficient. We may want to capture the test output for each
49             commit, or, more usefully, may want to capture the test output only at those
50             commits where the output changed. F
51             provides methods for the second of those objectives. That is:
52              
53             =over 4
54              
55             =item *
56              
57             When the number of commits in the specified range is large and you only need
58             the test output at those commits where the output materially changed, you can
59             use this package, F.
60              
61             =item *
62              
63             When you want to capture the test output for each commit in a specified range,
64             you can use another package in this library, F.
65              
66             =back
67              
68             =head1 METHODS
69              
70             This package inherits methods from F. Only methods unique to
71             F are documented here. See the documentation for
72             F for all other methods, including:
73              
74             new()
75             get_commits_range()
76             set_targets()
77              
78             =head2 C
79              
80             =over 4
81              
82             =item * Purpose
83              
84             For selected files within an application's test suite, determine the points
85             within a specified range of F commits where the output of a run of each
86             test materially changes. Store the test output at those transition points for
87             human inspection.
88              
89             =item * Arguments
90              
91             $self->multisect_all_targets();
92              
93             None; all data needed is already present in the object.
94              
95             =item * Return Value
96              
97             Returns true value upon success.
98              
99             =item * Comment
100              
101             As C runs it does two kinds of things:
102              
103             =over 4
104              
105             =item *
106              
107             It stores results data within the object which you can subsequently access through method calls.
108              
109             =item *
110              
111             It captures each test output and writes it to a file on disk for later human inspection.
112              
113             =back
114              
115             =back
116              
117             =cut
118              
119             sub multisect_all_targets {
120 0     0 1   my ($self) = @_;
121              
122             # Prepare data structures in the object to hold results of test runs on a
123             # per target, per commit basis.
124             # Also, "prime" the data structure by performing test runs for each target
125             # on the first and last commits in the commit range, storing that test
126             # output on disk as well.
127              
128 0           my $start_time = time();
129 0           $self->_prepare_for_multisection();
130              
131 0           my $target_count = scalar(@{$self->{targets}});
  0            
132 0           my $max_target_idx = $#{$self->{targets}};
  0            
133              
134             # 1 element per test target file, keyed on stub, value 0 or 1
135 0           my %overall_status = map { $self->{targets}->[$_]->{stub} => 0 } (0 .. $max_target_idx);
  0            
136              
137             # Overall success criterion: We must have completed multisection --
138             # identified all transitional commits -- for each target and recorded that
139             # completion with a '1' in its element in %overall_status. If we have
140             # achieved that, then each element in %overall_status will have the value
141             # '1' and they will sum up to the total number of test files being
142             # targeted.
143              
144 0           until (sum(values(%overall_status)) == $target_count) {
145 0 0         if ($self->{verbose}) {
146 0           say "target count|sum of status values: ",
147             join('|' => $target_count, sum(values(%overall_status)));
148             }
149              
150             # Target and process one file at a time. To multisect a target is to
151             # identify all its transitional commits over the commit range.
152              
153 0           for my $target_idx (0 .. $max_target_idx) {
154 0           my $target = $self->{targets}->[$target_idx];
155 0 0         if ($self->{verbose}) {
156 0           say "Targeting file: $target->{path}";
157             }
158              
159 0           my $rv = $self->_multisect_one_target($target_idx);
160 0 0         if ($rv) {
161 0           $overall_status{$target->{stub}}++;
162             }
163             }
164             } # END until loop
165 0           my $end_time = time();
166 0           my %distinct_commits = ();
167 0           for my $target (keys %{$self->{multisected_outputs}}) {
  0            
168 0           for my $el (@{$self->{multisected_outputs}->{$target}}) {
  0            
169 0 0         if (defined $el) {
170 0           $distinct_commits{$el->{commit}} = 1;
171             }
172             }
173             }
174 0           my %timings = (
175             elapsed => $end_time - $start_time,
176             runs => scalar(keys(%distinct_commits)),
177             );
178 0           $timings{mean} = sprintf("%.02f" => $timings{elapsed} / $timings{runs});
179 0 0         if ($self->{verbose}) {
180 0           say "Ran $timings{runs} runs; elapsed: $timings{elapsed} sec; mean: $timings{mean} sec";
181             }
182 0           $self->{timings} = \%timings;
183              
184 0           return 1;
185             }
186              
187             sub _prepare_for_multisection {
188 0     0     my $self = shift;
189              
190             # get_commits_range is inherited from parent
191              
192 0           my $all_commits = $self->get_commits_range();
193 0           $self->{all_outputs} = [ (undef) x scalar(@{$all_commits}) ];
  0            
194              
195 0           my %multisected_outputs_table;
196 0           for my $idx (0, $#{$all_commits}) {
  0            
197              
198             # run_test_files_on_one_commit is inherited from parent
199              
200 0           my $outputs = $self->run_test_files_on_one_commit($all_commits->[$idx]);
201 0           $self->{all_outputs}->[$idx] = $outputs;
202 0           for my $target (@{$outputs}) {
  0            
203 0           my @other_keys = grep { $_ ne 'file_stub' } keys %{$target};
  0            
  0            
204             $multisected_outputs_table{$target->{file_stub}}[$idx] =
205 0           { map { $_ => $target->{$_} } @other_keys };
  0            
206             }
207             }
208 0           $self->{multisected_outputs} = { %multisected_outputs_table };
209 0           return \%multisected_outputs_table;
210             }
211              
212             sub _multisect_one_target {
213 0     0     my ($self, $target_idx) = @_;
214 0 0 0       croak "Must supply index of test file within targets list"
215             unless(defined $target_idx and $target_idx =~ m/^\d+$/);
216             croak "You must run _prepare_for_multisection() before any stand-alone run of _multisect_one_target()"
217 0 0         unless exists $self->{multisected_outputs};
218 0           my $target = $self->{targets}->[$target_idx];
219 0           my $stub = $target->{stub};
220              
221             # The condition for successful multisection of one particular test file
222             # target is that the list of md5_hex values for files holding the output of TAP
223             # run over the commit range exhibit the following behavior:
224              
225             # The list is composed of sub-sequences (a) whose elements are either (i)
226             # the md5_hex value for the TAP outputfiles at a given commit or (ii)
227             # undefined; (b) if defined, the md5_values are all identical; (c) the
228             # first and last elements of the sub-sequence are both defined; and (d)
229             # the sub-sequence's unique defined value never reoccurs in any subsequent
230             # sub-sequence.
231              
232             # For each run of _multisect_one_target() over a given target, it will
233             # return a true value (1) if the above condition(s) are met and 0
234             # otherwise. The caller (multisect_all_targets()) will handle that return
235             # value appropriately. The caller will then call _multisect_one_target()
236             # on the next target, if any.
237              
238             # The objective of multisection is to identify the git commits at which
239             # the test output targeted materially changed. We are using
240             # an md5_hex value for that test file as a presumably valid unique
241             # identifier for that file's content. A transition point is a commit at
242             # which the output file's md5_hex differs from that of the immediately
243             # preceding commit. So, to identify the first transition point for a
244             # given target, we need to locate the commit at which the md5_hex changed
245             # from that found in the very first commit in the designated commit range.
246             # Once we've identified the first transition point, we'll look for the
247             # second transition point, i.e., that where the md5_hex changed from that
248             # observed at the first transition point. We'll continue that process
249             # until we get to a transition point where the md5_hex is identical to
250             # that of the very last commit in the commit range.
251              
252             # This entails checking out the source code at each commit calculated by
253             # the bisection algorithm, configuring and building the code, running the
254             # test targets at that commit, computing their md5_hex values and storing
255             # them in the 'multisected_outputs' structure. The _prepare_for_multisection()
256             # method will pre-populate that structure with md5_hexes for each test
257             # file for each of the first and last commits in the commit range.
258              
259             # Since the configuration and build at a particular commit may be
260             # time-consuming, once we have completed those steps we will run all the
261             # test files at once and store their results in 'multisected_outputs'
262             # immediately. We will make our bisection decision based only on analysis
263             # of the current target. But when we come to the second target file we
264             # will be able to skip configuration, build and test-running at commits
265             # visited during the pass over the first target file.
266              
267 0           my ($min_idx, $max_idx) = (0, $#{$self->{commits}});
  0            
268 0           my $this_target_status = 0;
269 0           my $current_start_idx = $min_idx;
270 0           my $current_end_idx = $max_idx;
271             my $overall_start_md5_hex =
272 0           $self->{multisected_outputs}->{$stub}->[$min_idx]->{md5_hex};
273             my $overall_end_md5_hex =
274 0           $self->{multisected_outputs}->{$stub}->[$max_idx]->{md5_hex};
275 0           my $excluded_targets = {};
276 0           my $n = 0;
277              
278 0           while (! $this_target_status) {
279              
280             # Start multisecting on this test target file: one transition point at
281             # a time until we've got them all for this test file.
282              
283             # What gets (or may get) updated or assigned to in the course of one rep of this loop:
284             # $current_start_idx
285             # $current_end_idx
286             # $n
287             # $excluded_targets
288             # $self->{all_outputs}
289             # $self->{multisected_outputs}
290              
291 0           my $h = sprintf("%d" => (($current_start_idx + $current_end_idx) / 2));
292 0           $self->_run_one_commit_and_assign($h);
293              
294             my $current_start_md5_hex =
295 0           $self->{multisected_outputs}->{$stub}->[$current_start_idx]->{md5_hex};
296             my $target_h_md5_hex =
297 0           $self->{multisected_outputs}->{$stub}->[$h]->{md5_hex};
298              
299             # Decision criteria:
300             # If $target_h_md5_hex eq $current_start_md5_hex, then the first
301             # transition is *after* index $h. Hence bisection should go upwards.
302              
303             # If $target_h_md5_hex ne $current_start_md5_hex, then the first
304             # transition has come *before* index $h. Hence bisection should go
305             # downwards. However, since the test of where the first transition is
306             # is that index j-1 has the same md5_hex as $current_start_md5_hex but
307             # index j has a different md5_hex, we have to do a run on
308             # j-1 as well.
309              
310             ($current_start_idx, $current_end_idx, $n) =
311             $self->_bisection_decision(
312             $target_h_md5_hex, $current_start_md5_hex, $h,
313 0           $self->{multisected_outputs}->{$stub},
314             $overall_end_md5_hex, $current_start_idx, $current_end_idx,
315             $max_idx, $n,
316             );
317 0           $this_target_status = $self->_evaluate_status_one_target_run($target_idx);
318             }
319 0           return 1;
320             }
321              
322             sub _evaluate_status_one_target_run {
323 0     0     my ($self, $target_idx) = @_;
324 0           my @trans = ();
325 0           for my $o (@{$self->{all_outputs}}) {
  0            
326             push @trans,
327 0 0         defined $o ? $o->[$target_idx]->{md5_hex} : undef;
328             }
329 0           my $vls = validate_list_sequence(\@trans);
330 0 0 0       return ( (scalar(@{$vls}) == 1 ) and ($vls->[0])) ? 1 : 0;
331             }
332              
333             sub _run_one_commit_and_assign {
334              
335             # If we've already stashed a particular commit's outputs in
336             # all_outputs (and, simultaneously) in multisected_outputs,
337             # then we don't need to actually perform a run.
338              
339             # This internal method assigns to all_outputs and multisected_outputs in
340             # place.
341              
342 0     0     my ($self, $idx) = @_;
343 0           my $this_commit = $self->{commits}->[$idx]->{sha};
344 0 0         unless (defined $self->{all_outputs}->[$idx]) {
345 0           say "\nAt commit counter $self->{commit_counter}, preparing to test commit ", $idx + 1, " of ", scalar(@{$self->{commits}})
346 0 0         if $self->{verbose};
347 0           my $these_outputs = $self->run_test_files_on_one_commit($this_commit);
348 0           $self->{all_outputs}->[$idx] = $these_outputs;
349              
350 0           for my $target (@{$these_outputs}) {
  0            
351 0           my @other_keys = grep { $_ ne 'file_stub' } keys %{$target};
  0            
  0            
352             $self->{multisected_outputs}->{$target->{file_stub}}->[$idx] =
353 0           { map { $_ => $target->{$_} } @other_keys };
  0            
354             }
355             }
356             }
357              
358             =head2 C
359              
360             =over 4
361              
362             =item * Purpose
363              
364             Get results of C (other than test output files
365             created) reported on a per target/per commit basis.
366              
367             =item * Arguments
368              
369             my $multisected_outputs = $self->get_multisected_outputs();
370              
371             None; all data needed is already present in the object.
372              
373             =item * Return Value
374              
375             Reference to a hash with one element for each targeted test file.
376              
377             Each element's key is a "stub" version of the target's relative path below the
378             F checkout directory in which forward slashes and dot characters have
379             been replaced with underscores. So,
380              
381             t/44_func_hashes_mult_unsorted.t
382              
383             ... becomes:
384              
385             t_44_func_hashes_mult_unsorted_t
386              
387             Each element's value is a reference to an array with one element for each
388             commit in the commit range.
389              
390             =over 4
391              
392             =item *
393              
394             If a particular commit B in the course of
395             C, then the array element is undefined. (The point
396             of multisection, of course, is to B have to visit every commit in the
397             commit range in order to figure out the commits at which test output changed.)
398              
399             =item *
400              
401             If a particular commit B in the course of
402             C, then the array element is a hash reference whose
403             elements have the following keys:
404              
405             commit
406             commit_short
407             file
408             md5_hex
409              
410             =back
411              
412             Example:
413              
414             {
415             t_001_load_t => [
416             {
417             commit => "d2bd2c75a2fd9afd3ac65a808eea2886d0e41d01",
418             commit_short => "d2bd2c7",
419             file => "/tmp/LHEG4uXfj1/d2bd2c7.t_001_load_t.output.txt",
420             md5_hex => "318ce8b2ccb3e92a6e516e18d1481066",
421             },
422             undef,
423             {
424             commit => "f2bc0ec377776b42928a29cebe04954975a30eb2",
425             commit_short => "f2bc0ec",
426             file => "/tmp/LHEG4uXfj1/f2bc0ec.t_001_load_t.output.txt",
427             md5_hex => "318ce8b2ccb3e92a6e516e18d1481066",
428             },
429             # ...
430             },
431             {
432             commit => "199494ee204dd78ed69490f9e54115b0e83e7d39",
433             commit_short => "199494e",
434             file => "/tmp/LHEG4uXfj1/199494e.t_001_load_t.output.txt",
435             md5_hex => "d7125615b2e5dbb4750ff107bbc1bad3",
436             },
437             ],
438             t_002_add_t => [
439             {
440             commit => "d2bd2c75a2fd9afd3ac65a808eea2886d0e41d01",
441             commit_short => "d2bd2c7",
442             file => "/tmp/LHEG4uXfj1/d2bd2c7.t_002_add_t.output.txt",
443             md5_hex => "0823e5d7628802e5a489661090109c56",
444             },
445             undef,
446             {
447             commit => "f2bc0ec377776b42928a29cebe04954975a30eb2",
448             commit_short => "f2bc0ec",
449             file => "/tmp/LHEG4uXfj1/f2bc0ec.t_002_add_t.output.txt",
450             md5_hex => "0823e5d7628802e5a489661090109c56",
451             },
452             # ...
453             {
454             commit => "199494ee204dd78ed69490f9e54115b0e83e7d39",
455             commit_short => "199494e",
456             file => "/tmp/LHEG4uXfj1/199494e.t_002_add_t.output.txt",
457             md5_hex => "7716009f1af9a562a3edad9e2af7dedc",
458             },
459             ],
460             }
461              
462             =back
463              
464             =cut
465              
466             sub get_multisected_outputs {
467 0     0 1   my $self = shift;
468 0           return $self->{multisected_outputs};
469             }
470              
471             =head2 C
472              
473             =over 4
474              
475             =item * Purpose
476              
477             Get a data structure which reports on the most meaningful results of
478             C, namely, the first commit, the last commit and all
479             transitional commits.
480              
481             =item * Arguments
482              
483             my $transitions = $self->inspect_transitions();
484              
485             None; all data needed is already present in the object.
486              
487             =item * Return Value
488              
489             Reference to a hash with one element per target. Each element's key is a
490             "stub" version of the target's relative path below the F checkout
491             directory. (See example in documentation for C
492             above.)
493              
494             Each element's value is another hash reference. The elements of that hash
495             will have the following keys:
496              
497             =over 4
498              
499             =item * C
500              
501             Value is reference to hash keyed on C, C and C, whose
502             values are, respectively, the index position of the very first commit in the
503             commit range, the digest of that commit's test output and the path to the file
504             holding that output.
505              
506             =item * C
507              
508             Value is reference to hash keyed on C, C and C, whose
509             values are, respectively, the index position of the very last commit in the
510             commit range, the digest of that commit's test output and the path to the file
511             holding that output.
512              
513             =item * C
514              
515             Value is reference to an array with one element for each transitional commit.
516             Each such element is a reference to a hash with keys C and C.
517             In this context C refers to the last commit in a sub-sequence with a
518             particular digest; C refers to the next immediate commit which is the
519             first commit in a new sub-sequence with a new digest.
520              
521             The values of C and C are, in turn, references to hashes with
522             keys C, C and C. Their values are, respectively, the index
523             position of the particular commit in the commit range, the digest of that
524             commit's test output and the path to the file holding that output.
525              
526             =back
527              
528             Example:
529              
530             {
531             t_001_load_t => {
532             newest => {
533             file => "/tmp/IvD3Zwn3FJ/199494e.t_001_load_t.output.txt",
534             idx => 13,
535             md5_hex => "d7125615b2e5dbb4750ff107bbc1bad3",
536             },
537             oldest => {
538             file => "/tmp/IvD3Zwn3FJ/d2bd2c7.t_001_load_t.output.txt",
539             idx => 0,
540             md5_hex => "318ce8b2ccb3e92a6e516e18d1481066",
541             },
542             transitions => [
543             {
544             newer => {
545             file => "/tmp/IvD3Zwn3FJ/1debd8a.t_001_load_t.output.txt",
546             idx => 5,
547             md5_hex => "e5a839ea2e34b8976000c78c258299b0",
548             },
549             older => {
550             file => "/tmp/IvD3Zwn3FJ/707da97.t_001_load_t.output.txt",
551             idx => 4,
552             md5_hex => "318ce8b2ccb3e92a6e516e18d1481066",
553             },
554             },
555             {
556             newer => {
557             file => "/tmp/IvD3Zwn3FJ/6653d84.t_001_load_t.output.txt",
558             idx => 8,
559             md5_hex => "f4920ddfdd9f1e6fc21ebfab09b5fcfe",
560             },
561             older => {
562             file => "/tmp/IvD3Zwn3FJ/b35b4d7.t_001_load_t.output.txt",
563             idx => 7,
564             md5_hex => "e5a839ea2e34b8976000c78c258299b0",
565             },
566             },
567             {
568             newer => {
569             file => "/tmp/IvD3Zwn3FJ/aa1ed28.t_001_load_t.output.txt",
570             idx => 12,
571             md5_hex => "d7125615b2e5dbb4750ff107bbc1bad3",
572             },
573             older => {
574             file => "/tmp/IvD3Zwn3FJ/65bf77c.t_001_load_t.output.txt",
575             idx => 11,
576             md5_hex => "f4920ddfdd9f1e6fc21ebfab09b5fcfe",
577             },
578             },
579             ],
580             },
581             t_002_add_t => {
582             newest => {
583             file => "/tmp/IvD3Zwn3FJ/199494e.t_002_add_t.output.txt",
584             idx => 13,
585             md5_hex => "7716009f1af9a562a3edad9e2af7dedc",
586             },
587             oldest => {
588             file => "/tmp/IvD3Zwn3FJ/d2bd2c7.t_002_add_t.output.txt",
589             idx => 0,
590             md5_hex => "0823e5d7628802e5a489661090109c56",
591             },
592             transitions => [
593             {
594             newer => {
595             file => "/tmp/IvD3Zwn3FJ/646fd8a.t_002_add_t.output.txt",
596             idx => 3,
597             md5_hex => "dbd8c7a70877b3c8d3fd93a7a66d8468",
598             },
599             older => {
600             file => "/tmp/IvD3Zwn3FJ/f2bc0ec.t_002_add_t.output.txt",
601             idx => 2,
602             md5_hex => "0823e5d7628802e5a489661090109c56",
603             },
604             },
605             {
606             newer => {
607             file => "/tmp/IvD3Zwn3FJ/b35b4d7.t_002_add_t.output.txt",
608             idx => 7,
609             md5_hex => "50aac31686ac930aad7fdd23df679f28",
610             },
611             older => {
612             file => "/tmp/IvD3Zwn3FJ/55ab1f9.t_002_add_t.output.txt",
613             idx => 6,
614             md5_hex => "dbd8c7a70877b3c8d3fd93a7a66d8468",
615             },
616             },
617             {
618             newer => {
619             file => "/tmp/IvD3Zwn3FJ/6653d84.t_002_add_t.output.txt",
620             idx => 8,
621             md5_hex => "256f466d35533555dce93a838ba5ab9d",
622             },
623             older => {
624             file => "/tmp/IvD3Zwn3FJ/b35b4d7.t_002_add_t.output.txt",
625             idx => 7,
626             md5_hex => "50aac31686ac930aad7fdd23df679f28",
627             },
628             },
629             {
630             newer => {
631             file => "/tmp/IvD3Zwn3FJ/abc336e.t_002_add_t.output.txt",
632             idx => 9,
633             md5_hex => "037be971470cb5d96a7a7f9764a6f3aa",
634             },
635             older => {
636             file => "/tmp/IvD3Zwn3FJ/6653d84.t_002_add_t.output.txt",
637             idx => 8,
638             md5_hex => "256f466d35533555dce93a838ba5ab9d",
639             },
640             },
641             {
642             newer => {
643             file => "/tmp/IvD3Zwn3FJ/65bf77c.t_002_add_t.output.txt",
644             idx => 11,
645             md5_hex => "7716009f1af9a562a3edad9e2af7dedc",
646             },
647             older => {
648             file => "/tmp/IvD3Zwn3FJ/bbe25f4.t_002_add_t.output.txt",
649             idx => 10,
650             md5_hex => "037be971470cb5d96a7a7f9764a6f3aa",
651             },
652             },
653             ],
654             },
655             }
656              
657             =item * Comment
658              
659             The return value of C should be useful to the developer
660             trying to determine the various points in a long series of commits where a
661             target's test output changed in meaningful ways. Hence, it is really the
662             whole point of F.
663              
664             =back
665              
666             =cut
667              
668             sub inspect_transitions {
669 0     0 1   my ($self) = @_;
670 0           my $multisected_outputs = $self->get_multisected_outputs();
671 0           my %transitions;
672 0           for my $k (sort keys %{$multisected_outputs}) {
  0            
673 0           my $arr = $multisected_outputs->{$k};
674 0           my $max_index = $#{$arr};
  0            
675 0           $transitions{$k}{transitions} = [];
676             $transitions{$k}{oldest} = {
677             idx => 0,
678             md5_hex => $arr->[0]->{md5_hex},
679             file => $arr->[0]->{file},
680 0           };
681             $transitions{$k}{newest} = {
682             idx => $max_index,
683             md5_hex => $arr->[$max_index]->{md5_hex},
684             file => $arr->[$max_index]->{file},
685 0           };
686 0           for (my $j = 1; $j <= $max_index; $j++) {
687 0           my $i = $j - 1;
688 0 0 0       next unless ((defined $arr->[$i]) and (defined $arr->[$j]));
689 0           my $older_md5_hex = $arr->[$i]->{md5_hex};
690 0           my $newer_md5_hex = $arr->[$j]->{md5_hex};
691 0           my $older_file = $arr->[$i]->{file};
692 0           my $newer_file = $arr->[$j]->{file};
693 0 0         unless ($older_md5_hex eq $newer_md5_hex) {
694 0           push @{$transitions{$k}{transitions}}, {
  0            
695             older => { idx => $i, md5_hex => $older_md5_hex, file => $older_file },
696             newer => { idx => $j, md5_hex => $newer_md5_hex, file => $newer_file },
697             }
698             }
699             }
700             }
701 0           return \%transitions;
702             }
703              
704             1;
705              
706             __END__