File Coverage

blib/lib/Devel/Git/MultiBisect/Transitions.pm
Criterion Covered Total %
statement 26 144 18.0
branch 0 26 0.0
condition 0 9 0.0
subroutine 9 16 56.2
pod 3 3 100.0
total 38 198 19.1


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