File Coverage

blib/lib/Devel/Git/MultiBisect/Transitions.pm
Criterion Covered Total %
statement 26 148 17.5
branch 0 26 0.0
condition 0 9 0.0
subroutine 9 17 52.9
pod 4 4 100.0
total 39 204 19.1


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