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