File Coverage

blib/lib/Perl5/TestEachCommit.pm
Criterion Covered Total %
statement 52 140 37.1
branch 26 78 33.3
condition 7 8 87.5
subroutine 7 15 46.6
pod 10 10 100.0
total 102 251 40.6


line stmt bran cond sub pod time code
1             package Perl5::TestEachCommit;
2 2     2   11446 use 5.014;
  2         7  
3 2     2   8 use warnings;
  2         3  
  2         211  
4             our $VERSION = '0.07';
5             $VERSION = eval $VERSION;
6 2     2   15 use Carp;
  2         2  
  2         172  
7 2     2   939 use Data::Dump ( qw| dd pp| );
  2         26478  
  2         164  
8 2     2   981 use File::Spec::Functions;
  2         1628  
  2         6710  
9              
10             =encoding utf8
11              
12             =head1 NAME
13              
14             Perl5::TestEachCommit - Test each commit in a pull request to Perl core
15              
16             =head1 SYNOPSIS
17              
18             use Perl5::TestEachCommit;
19              
20             $self = Perl5::TestEachCommit->new();
21              
22             $self->prepare_repository();
23             $self->display_plan();
24             $self->get_commits();
25             $self->display_commits();
26             $self->examine_all_commits();
27             $self->get_results();
28             $self->display_results();
29              
30             =head1 DESCRIPTION
31              
32             This library is intended for use by people working to maintain the
33             L.
34              
35             Commits to C, the main development branch in the Perl repository, are
36             most often done by pull requests. Most such p.r.s consist of a single commit,
37             but commits of forty or eighty are not unknown. A continuous integration
38             system (CI) ensures that each p.r. is configured, built and tested on
39             submission and on subsequent modifications. That CI system, however, only
40             executes that cycle on the *final* commit in each p.r. It cannot detect any
41             failure in a *non-final* commit. This library provides a way to test each
42             commit in the p.r. to the same extent that the CI system tests the final
43             commit.
44              
45             Why is this important? Suppose that we have a pull request that consists of 5
46             commits. In commit 3 the developer makes an error which causes F to
47             fail. The developer notices that and corrects the error in commit 4. Commit
48             5 configures, builds and tests satisfactorily, so the CI system gives the p.r.
49             as a whole a PASS. The committer uses that PASS as the basis for approving a
50             merge of the branch into C.
51              
52             Commit Configure Build Test
53             ------------------------------------
54             1abcd X X X
55             2efab X X X
56             3cdef X 0 -
57             4dcba X X X
58             5fedc X X X
59              
60             If, for any reason (*e.g.,* bisection), some other developer in the future
61             needs to say F, they will discover that at that commit the
62             build was actually broken.
63              
64             =head2 Additional Functionality
65              
66             As of version 0.06, this distribution now provides the functionality to
67             analyze breakage at the F level. If it is known (or suspected) that
68             tests will break during F or F, then runtime
69             can be considerably reduced from the more typical case where breakage occurs
70             during F or F. However, the two cases
71             cannot be mixed.
72              
73             =head1 METHODS
74              
75             =head2 C
76              
77             =over 4
78              
79             =item * Purpose
80              
81             Perl5::TestEachCommit constructor. Ensures that supplied arguments are
82             plausible, I directories needed can be located.
83              
84             =item * Arguments
85              
86             my $self = Perl5::TestEachCommit->new( { %opts } );
87              
88             Single hash reference. That hash B include the following key-value
89             pairs:
90              
91             =over 4
92              
93             * C
94              
95             String holding path to a directory which is a F checkout of the Perl core
96             distribution. If you have previously set an environmental variable
97             C holding the path to such a directory, that will be
98             used; otherwise, path must be specified.
99              
100             * C
101              
102             String holding SHA of the first commit in the series on which you wish
103             reporting.
104              
105             * C
106              
107             String holding SHA of the last commit in the series on which you wish
108             reporting.
109              
110             =back
111              
112             In addition, that hash B include the following key-value pairs:
113              
114             =over 4
115              
116             * C
117              
118             F branch which must exist and be available for C in the
119             directory specified by C. Defaults to C.
120              
121             * C
122              
123             String holding arguments to be passed to F<./Configure>. Defaults to C
124             ./Configure -des -Dusedevel>. Add < 1/dev/null> to that string if you
125             don't need voluminous output to C.
126              
127             * C
128              
129             String holding arguments to be passed to F. Defaults to
130             C. Add < 1/dev/null> to that string if you don't need
131             voluminous output to C.
132              
133             * C
134              
135             String holding arguments to be passed to F. Defaults to
136             C. Add < 1/dev/null> to that string if you don't need
137             voluminous output to C.
138              
139             * C
140              
141             True/false value. Defaults to false. If true, when proceeding through a
142             series of commits in a branch or pull request, the C stage
143             will be skipped on the assumption that any significant failures are going to
144             appear in the first two stages.
145              
146             * C
147              
148             String holding arguments to be passed to F. Defaults to
149             C. Add < 1/dev/null> to that string if you don't
150             need voluminous output to C. B If this key-value pair is passed
151             to C, then any value passed to C or
152             C is ignored.
153              
154             * C
155              
156             String holding arguments to be passed to F. Defaults to C
157             minitest>. Add < 1/dev/null> to that string if you don't need voluminous
158             output to C. B If this key-value pair is passed to C,
159             then any value passed to C or
160             C is ignored.
161              
162             * C
163              
164             True/false value. Defaults to false. If true, prints to C a summary of
165             switches in use and commits being tested.
166              
167             =back
168              
169             =item * Return Value
170              
171             Perl5::TestEachCommit object (blessed hash reference).
172              
173             =back
174              
175             =cut
176              
177             sub new {
178 13     13 1 391207 my ($class, $params) = @_;
179 13         30 my $args = {};
180              
181 13         29 for my $k (keys %{$params}) { $args->{$k} = $params->{$k}; }
  13         52  
  102         237  
182 13         34 my %data;
183             croak "Must supply SHA of first commit to be studied to 'start'"
184 13 100       249 unless $args->{start};
185             croak "Must supply SHA of last commit to be studied to 'end'"
186 12 100       179 unless $args->{end};
187 11         32 $data{start} = delete $args->{start};
188 11         25 $data{end} = delete $args->{end};
189              
190             # workdir: First see if it has been assigned and exists
191             # later: see whether it is a git checkout (and of perl)
192 11   100     48 $args->{workdir} ||= ($ENV{SECONDARY_CHECKOUT_DIR} || '');
      100        
193 11 100       424 -d $args->{workdir} or croak "Unable to locate workdir";
194              
195 10         34 $data{workdir} = delete $args->{workdir};
196              
197 10 100       51 $data{branch} = $args->{branch} ? delete $args->{branch} : 'blead';
198             $data{configure_command} = $args->{configure_command}
199             ? delete $args->{configure_command}
200 10 100       32 : 'sh ./Configure -des -Dusedevel';
201              
202             $data{make_test_prep_command} = $args->{make_test_prep_command}
203             ? delete $args->{make_test_prep_command}
204 10 100       40 : 'make test_prep';
205             $data{make_test_harness_command} = $args->{make_test_harness_command}
206             ? delete $args->{make_test_harness_command}
207 10 100       32 : 'make test_harness';
208              
209 10 100 66     53 if ($args->{make_minitest_prep_command} or $args->{make_minitest_command}) {
210 3         7 delete $data{make_test_prep_command};
211 3         9 delete $data{make_test_harness_command};
212             $data{make_minitest_prep_command} = $args->{make_minitest_prep_command}
213             ? delete $args->{make_minitest_prep_command}
214 3 50       11 : 'make minitest_prep';
215             $data{make_minitest_command} = $args->{make_minitest_command}
216             ? delete $args->{make_minitest_command}
217 3 50       10 : 'make minitest';
218             }
219              
220             $data{skip_test_harness} = defined $args->{skip_test_harness}
221             ? delete $args->{skip_test_harness}
222 10 100       31 : '';
223             $data{verbose} = defined $args->{verbose}
224             ? delete $args->{verbose}
225 10 100       28 : '';
226              
227             # Double-check that every parameter ultimately gets into the object with
228             # some assignment.
229 10 0       16 map { ! exists $data{$_} ? $data{$_} = $args->{$_} : '' } keys %{$args};
  0         0  
  10         30  
230 10         55 return bless \%data, $class;
231             }
232              
233             =head2 C
234              
235             =over 4
236              
237             =item * Purpose
238              
239             Prepare the C directory for F operations, I terminates
240             any bisection in process, cleans the directory, fetches from origing, checks
241             out blead, then checks out any non-blead branch indicated in the C
242             argument to C.
243              
244             =item * Arguments
245              
246             None.
247              
248             my $rv = $self->prepare_repostory();
249              
250             =item * Return Value
251              
252             Returns true value upon success.
253              
254             =back
255              
256             =cut
257              
258             sub prepare_repository {
259 0     0 1 0 my $self = shift;
260              
261 0 0       0 chdir $self->{workdir} or croak "Unable to change to $self->{workdir}";
262              
263 0 0       0 my $grv = system(qq|
264             git bisect reset && \
265             git clean -dfxq && \
266             git remote prune origin && \
267             git fetch origin && \
268             git checkout blead && \
269             git rebase origin/blead
270             |) and croak "Unable to prepare $self->{workdir} for git activity";
271              
272 0 0       0 if ($self->{branch} ne 'blead') {
273 0 0       0 system(qq|git checkout $self->{branch}|)
274             and croak "Unable to checkout branch '$self->{branch}'";
275             }
276 0         0 return 1;
277             }
278              
279             =head2 C
280              
281             =over 4
282              
283             =item * Purpose
284              
285             Display most important configuration choices.
286              
287             =item * Arguments
288              
289             $self->display_plan();
290              
291             =item * Return Value
292              
293             Implicitly returns true value upon success.
294              
295             =item * Comment
296              
297             The output will look like this:
298              
299             branch: blead
300             configure_command: sh ./Configure -des -Dusedevel 1>/dev/null
301             make_test_prep_command: make test_prep 1>/dev/null
302             make_test_harness_command: make_test_harness 1>/dev/null
303              
304             Or like this, if you are doing F-level testing on a debugging build:
305              
306             branch: blead
307             configure_command: sh ./Configure -des -Dusedevel -DDEBUGGING 1>/dev/null
308             make_minitest_prep_command: make minitest_prep 1>/dev/null
309             make_minitest_command: make minitest 1>/dev/null
310              
311             =back
312              
313             =cut
314              
315             sub display_plan {
316 4     4 1 14350 my $self = shift;
317 4         260 say "branch: $self->{branch}";
318 4         115 say "configure_command: $self->{configure_command}";
319 4 100       19 if ($self->{make_minitest_prep_command}) {
320 2         31 say "make_minitest_prep_command: $self->{make_minitest_prep_command}";
321 2         26 say "make_minitest_command: $self->{make_minitest_command}";
322             }
323             else {
324 2         57 say "make_test_prep_command: $self->{make_test_prep_command}";
325 2 100       19 if ($self->{skip_test_harness}) {
326 1         18 say "Skipping 'make test_harness'";
327             }
328             else {
329 1         17 say "make_test_harness_command: $self->{make_test_harness_command}";
330             }
331             }
332 4         28 return 1;
333             }
334              
335             =head2 C
336              
337             =over 4
338              
339             =item * Purpose
340              
341             Get a list of SHAs of all commits being tested.
342              
343             =item * Arguments
344              
345             my $lines = $self->get_commits();
346              
347             =item * Return Value
348              
349             Reference to an array holding list of all commits being tested.
350              
351             =item * Comment
352              
353             An exception will be thrown if the array holding the list of all commits being
354             tested is empty. This can occur, for example, if you mistakenly interchange
355             the values for the C<--start> and C<--end> commits.
356              
357             =back
358              
359             =cut
360              
361             sub get_commits {
362 0     0 1   my $self = shift;
363 0           my $origin_commit = $self->{start} . '^';
364 0           my $end_commit = $self->{end};
365 0           my @commits = `git rev-list --reverse ${origin_commit}..${end_commit}`;
366 0           chomp @commits;
367 0 0         if (! scalar(@commits)) {
368 0           croak "No commits found in range; check values for --start and --end";
369             }
370             else {
371 0           return \@commits;
372             }
373             }
374              
375             =head2 C
376              
377             =over 4
378              
379             =item * Purpose
380              
381             Display a list of SHAs of all commits being tested.
382              
383             =item * Arguments
384              
385             $self->display_commits();
386              
387             =item * Return Value
388              
389             Implicitly returns true value upon success.
390              
391             =item * Comment
392              
393             The output will look like this:
394              
395             c9cd2e0cf4ad570adf68114c001a827190cb2ee9
396             79b32d926ef5961b4946ebe761a7058cb235f797
397             0dfa8ac113680e6acdef0751168ab231b9bf842c
398              
399             =back
400              
401             =cut
402              
403             sub display_commits {
404 0     0 1   my $self = shift;
405 0           say $_ for @{$self->get_commits()};
  0            
406 0           return 1;
407             }
408              
409              
410             =head2 C
411              
412             =over 4
413              
414             =item * Purpose
415              
416             Iterate over all commits in the selected range, configuring, building and --
417             assuming we have not elected to C -- testing each commit.
418              
419             =item * Arguments
420              
421             $self->examine_all_commits();
422              
423             =item * Return Value
424              
425             For possible future chaining, returns the Perl5::TestEachCommit object, which
426             now includes the results of the examination of each commit in the selected
427             range.
428              
429             =back
430              
431             =cut
432              
433             sub examine_all_commits {
434 0     0 1   my $self = shift;
435 0           $self->{results} = [];
436 0           $self->{commits_count} = scalar @{ $self->get_commits };
  0            
437 0           for (my $i = 0; $i < $self->{commits_count}; $i++) {
438             say STDERR "\nExamining commit ", $i+1, " of $self->{commits_count} commits"
439 0 0         if $self->{verbose};
440 0           $self->examine_one_commit($self->get_commits->[$i]);
441             }
442 0           return $self;
443             }
444              
445             =head2 C
446              
447             =over 4
448              
449             =item * Purpose
450              
451             Get a list of the SHA and score for each commit.
452              
453             =item * Arguments
454              
455             my $results_ref = $self->get_results();
456              
457             =item * Return Value
458              
459             Reference to an array holding a hashref for each commit. Each such hashref
460             has two elements: C and C. (See C.)
461              
462             =back
463              
464             =cut
465              
466             sub get_results {
467 0     0 1   my $self = shift;
468 0           return $self->{results}; # aref
469             }
470              
471             =head2 C
472              
473             =over 4
474              
475             =item * Purpose
476              
477             Pretty-print to C the results obtained via C.
478              
479             =item * Arguments
480              
481             $self->display_results();
482              
483             =item * Return Value
484              
485             Implicitly returns a true value upon success.
486              
487             =item * Comment
488              
489             The output will look like this:
490              
491             commit score
492             ------------------------------------------------
493             c9cd2e0cf4ad570adf68114c001a827190cb2ee9 | 2
494             79b32d926ef5961b4946ebe761a7058cb235f797 | 1
495             0dfa8ac113680e6acdef0751168ab231b9bf842c | 2
496              
497             =back
498              
499             =cut
500              
501             sub display_results {
502 0     0 1   my $self = shift;
503 0           say ' ' x 17, 'commit', ' ' x 17, ' ' x 3, 'score';
504 0           say '-' x 48;
505 0           for my $el (@{$self->{results}}) {
  0            
506 0           say $el->{commit}, ' | ', $el->{score};
507             }
508 0           return 1;
509             }
510              
511             =head2 C
512              
513             =over 4
514              
515             =item * Purpose
516              
517             Configure, build and test one commit in the selected range.
518              
519             =item * Arguments
520              
521             my $score_ref = $self->examine_one_commit($this_SHA);
522              
523             =item * Return Value
524              
525             Returns the Perl5::TestEachCommit object, how holding a list of results.
526              
527             =over 4
528              
529             =item * C: the commit's SHA.
530              
531             =item * C: A numeral between 0 and 3 indicating how many stages the
532             commit completed successfully:
533              
534             =over 4
535              
536             =item 0 Unable to configure.
537              
538             =item 1 Completed configuration only.
539              
540             =item 2 Completed configuration and build only.
541              
542             =item 3 Completed all of configuration, build and testing.
543              
544             =back
545              
546             =back
547              
548             =item * Comment
549              
550             Called internally within C.
551              
552             =back
553              
554             =cut
555              
556             sub examine_one_commit {
557 0     0 1   my ($self, $c, $i) = @_;
558 0 0         chdir $self->{workdir} or croak "Unable to change to $self->{workdir}";
559             # So that ./Configure, make test_prep and make_test_harness all behave
560             # as they typically do in a git checkout.
561 0           local $ENV{PERL_CORE} = 1;
562              
563 0 0         my $rv = system(qq|git clean -dfxq|) and croak "Unable to git-clean";
564 0 0         $rv = system(qq|git checkout $c|) and croak "Unable to git-checkout $c";
565 0           undef $rv;
566 0           my $commit_score = 0;
567              
568 0 0         say STDERR "Configuring at $c" if $self->{verbose};
569              
570             # first stage: configuration
571 0           $rv = system($self->{configure_command});
572 0 0         if ($rv) {
573 0           carp "Unable to configure at $c";
574 0           push @{$self->{results}}, { commit => $c, score => $commit_score };
  0            
575 0           return;
576             }
577             else {
578 0           $commit_score++;
579              
580             # miniperl-level testing
581             # second and third levels are 'make minitest_prep' and 'make minitest'
582 0 0         if ($self->{make_minitest_prep_command}) {
583              
584 0 0         say STDERR "Building miniperl at $c" if $self->{verbose};
585 0           $rv = system($self->{make_minitest_prep_command});
586 0 0         if ($rv) {
587 0           carp "Unable to make_minitest_prep at $c";
588 0           push @{$self->{results}}, { commit => $c, score => $commit_score };
  0            
589 0           return;
590             }
591             else {
592 0           $commit_score++;
593              
594 0 0         say STDERR "Running minitest at $c" if $self->{verbose};
595 0           $rv = system($self->{make_minitest_command});
596 0 0         if ($rv) {
597 0           carp "Unable to make_minitest at $c";
598             }
599             else {
600 0           $commit_score++;
601             }
602 0           push @{$self->{results}}, { commit => $c, score => $commit_score };
  0            
603             }
604             }
605             # regular testing
606             # second and third levels are 'make test_prep' and 'make test_harness'
607             # option to skip 'make test_harness'
608             else {
609              
610 0 0         say STDERR "Building $c" if $self->{verbose};
611 0           $rv = system($self->{make_test_prep_command});
612 0 0         if ($rv) {
613 0           carp "Unable to make_test_prep at $c";
614 0           push @{$self->{results}}, { commit => $c, score => $commit_score };
  0            
615 0           return;
616             }
617             else {
618 0           $commit_score++;
619              
620 0 0         if ($self->{skip_test_harness}) {
621 0 0         say STDERR "Skipping 'make test_harness'" if $self->{verbose};
622             }
623             else {
624 0 0         say STDERR "Testing $c" if $self->{verbose};
625 0           $rv = system($self->{make_test_harness_command});
626 0 0         if ($rv) {
627 0           carp "Unable to make_test_harness at $c";
628             }
629             else {
630 0           $commit_score++;
631             }
632             }
633 0           push @{$self->{results}}, { commit => $c, score => $commit_score };
  0            
634             }
635             }
636             }
637             }
638              
639             =head2 C
640              
641             =over 4
642              
643             =item * Purpose
644              
645             Clean up the repository in the directory designated by C.
646              
647             =item * Arguments
648              
649             $self->cleanup_respository();
650              
651             =item * Return Value
652              
653             Implicitly returns a true value upon success.
654              
655             =item * Comment
656              
657             Performs a F and F but does not do any fetching
658             from origin or updating of C.
659              
660             =back
661              
662             =cut
663              
664             sub cleanup_repository {
665 0     0 1   my $self = shift;
666              
667 0 0         chdir $self->{workdir} or croak "Unable to change to $self->{workdir}";
668              
669 0 0         my $grv = system(qq|
670             git bisect reset && \
671             git clean -dfxq && \
672             git checkout blead
673             |) and croak "Unable to clean $self->{workdir} after git activity";
674              
675 0           return 1;
676             }
677              
678             =head1 BUGS
679              
680             None reported so far.
681              
682             =head1 SUPPORT
683              
684             Contact the author.
685              
686             =head1 AUTHOR
687              
688             James E Keenan
689             CPAN ID: JKEENAN
690             jkeenan@cpan.org
691             https://thenceforward.net/perl/modules/Perl5-TestEachCommit
692              
693             =head1 COPYRIGHT
694              
695             Copyright 2025 James E Keenan
696              
697             This program is free software; you can redistribute
698             it and/or modify it under the same terms as Perl itself.
699              
700             The full text of the license can be found in the
701             LICENSE file included with this module.
702              
703             =head1 SEE ALSO
704              
705             perl(1).
706              
707             =cut
708              
709             1;
710             # The preceding line will help the module return a true value
711