xref: /openbsd/gnu/usr.bin/perl/dist/Devel-PPPort/soak (revision 73471bf0)
1#!/usr/bin/perl -w
2################################################################################
3#
4#  soak -- Test Perl modules with multiple Perl releases.
5#
6#  Original Author: Paul Marquess
7#
8################################################################################
9#
10#  Version 3.x, Copyright (C) 2004-2013, Marcus Holland-Moritz.
11#  Version 2.x, Copyright (C) 2001, Paul Marquess.
12#  Version 1.x, Copyright (C) 1999, Kenneth Albanowski.
13#
14#  This program is free software; you can redistribute it and/or
15#  modify it under the same terms as Perl itself.
16#
17################################################################################
18
19require 5.006001;
20
21use strict;
22use warnings;
23use ExtUtils::MakeMaker;
24use Getopt::Long;
25use Pod::Usage;
26use File::Find;
27use List::Util qw(max);
28use Config;
29
30my $VERSION = '3.36';
31
32$| = 1;
33my %OPT = (
34  verbose => 0,
35  make    => $Config{make} || 'make',
36  min     => '5.000',
37  color   => 1,
38);
39
40GetOptions(\%OPT, qw(verbose make=s min=s mmargs=s@ color!)) or pod2usage(2);
41
42$OPT{mmargs} = [''] unless exists $OPT{mmargs};
43$OPT{min}    = parse_version($OPT{min}) - 1e-10;
44
45sub cs($;$$) { my $x = shift; my($s, $p) = @_ ? @_ : ('', 's'); ($x, $x == 1 ? $s : $p) }
46
47my @GoodPerls = map  { $_->[0] }
48                sort { $a->[1] <=> $b->[1] or $a->[0] cmp $b->[0] }
49                grep { $_->[1] >= $OPT{min} }
50                map  { [$_ => perl_version($_)] }
51                @ARGV ? SearchPerls(@ARGV) : FindPerls();
52
53unless (@GoodPerls) {
54  print "Sorry, got no Perl binaries for testing.\n\n";
55  exit 0;
56}
57
58my $maxlen = max(map length, @GoodPerls) + 3;
59my $mmalen = max(map length, @{$OPT{mmargs}});
60$maxlen += $mmalen+3 if $mmalen > 0;
61
62my $rep = Soak::Reporter->new( verbose => $OPT{verbose}
63                             , color   => $OPT{color}
64                             , width   => $maxlen
65                             );
66
67$SIG{__WARN__} = sub { $rep->warn(@_) };
68$SIG{__DIE__}  = sub { $rep->die(@_)  };
69
70# prime the pump, so the first "make realclean" will work.
71runit("$^X Makefile.PL") && runit("$OPT{make} realclean")
72    or $rep->die("Cannot run $^X Makefile.PL && $OPT{make} realclean\n");
73
74my $tot = @GoodPerls*@{$OPT{mmargs}};
75
76$rep->set(tests => $tot);
77
78$rep->status(sprintf("Testing %d version%s / %d configuration%s (%d combination%s)...\n",
79                     cs(@GoodPerls), cs(@{$OPT{mmargs}}), cs($tot)));
80
81for my $perl (@GoodPerls) {
82  for my $mm (@{$OPT{mmargs}}) {
83    $rep->set(perl => $perl, config => $mm);
84
85    $rep->test;
86
87    my @warn_mfpl;
88    my @warn_make;
89    my @warn_test;
90
91    my $ok = runit("$perl Makefile.PL $mm", \@warn_mfpl) &&
92             runit("$OPT{make}", \@warn_make) &&
93             runit("$OPT{make} test", \@warn_test);
94
95    $rep->warnings(['Makefile.PL' => \@warn_mfpl],
96                   ['make'        => \@warn_make],
97                   ['make test'   => \@warn_test]);
98
99    if ($ok) {
100      $rep->passed;
101    }
102    else {
103      $rep->failed;
104    }
105
106    runit("$OPT{make} realclean");
107  }
108}
109
110exit $rep->finish;
111
112sub runit
113{
114  # TODO -- portability alert!!
115
116  my($cmd, $warn) = @_;
117  $rep->vsay("\n    Running [$cmd]");
118  my $output = `$cmd 2>&1`;
119  $output = "\n" unless defined $output;
120  $output =~ s/^/    > /gm;
121  $rep->say("\n    Output:\n$output") if $OPT{verbose} || $?;
122  if ($?) {
123    $rep->warn("    Running '$cmd' failed: $?\n");
124    return 0;
125  }
126  push @$warn, $output =~ /(warning: .*)/ig;
127  return 1;
128}
129
130sub FindPerls
131{
132  # TODO -- need to decide how far back we go.
133  # TODO -- get list of user releases prior to 5.004
134  # TODO -- does not work on Windows (at least)
135
136  # find versions of Perl that are available
137  my @PerlBinaries = qw(
138    5.000
139    5.001
140    5.002
141    5.003
142    5.004 5.00401 5.00402 5.00403 5.00404 5.00405
143    5.005 5.00501 5.00502 5.00503 5.00504
144    5.6.0 5.6.1 5.6.2
145    5.7.0 5.7.1 5.7.2 5.7.3
146    5.8.0 5.8.1 5.8.2 5.8.3 5.8.4 5.8.5 5.8.6 5.8.7 5.8.8
147    5.9.0 5.9.1 5.9.2 5.9.3
148  );
149
150  print "Searching for Perl binaries...\n";
151
152  # find_perl will send a warning to STDOUT if it can't find
153  # the requested perl, so need to temporarily silence STDOUT.
154  tie *STDOUT, 'NoSTDOUT';
155
156  my $mm = MM->new( { NAME => 'dummy' });
157  my @path = $mm->path;
158  my @GoodPerls;
159
160  for my $perl (@PerlBinaries) {
161    if (my $abs = $mm->find_perl($perl, ["perl$perl"], \@path, 0)) {
162      push @GoodPerls, $abs;
163    }
164  }
165
166  untie *STDOUT;
167
168  print "\nFound:\n", (map "    $_\n", @GoodPerls), "\n";
169
170  return @GoodPerls;
171}
172
173sub SearchPerls
174{
175  my @args = @_;
176  my @perls;
177
178  for my $arg (@args) {
179    if (-d $arg) {
180      my @found;
181      print "Searching for Perl binaries in '$arg'...\n";
182      find({ wanted => sub {
183             $File::Find::name =~ m!perl5[\w._]+$!
184                 and -f $File::Find::name
185                 and -x $File::Find::name
186                 and perl_version($File::Find::name)
187                 and push @found, $File::Find::name;
188           }, follow => 1 }, $arg);
189      printf "Found %d Perl binar%s in '%s'.\n\n", cs(@found, 'y', 'ies'), $arg;
190      push @perls, @found;
191    }
192    else {
193      push @perls, $arg;
194    }
195  }
196
197  return @perls;
198}
199
200sub perl_version
201{
202  my $perl = shift;
203  my $ver = `$perl -e 'print \$]' 2>&1`;
204  return $? == 0 && $ver =~ /^\d+\.\d+/ && $ver >= 5 ? $ver : 0;
205}
206
207sub parse_version
208{
209  my $ver = shift;
210
211  if ($ver =~ /^(\d+)\.(\d+)\.(\d+)$/) {
212    return $1 + 1e-3*$2 + 1e-6*$3;
213  }
214  elsif ($ver =~ /^\d+\.[\d_]+$/) {
215    $ver =~ s/_//g;
216    return $ver;
217  }
218
219  die "cannot parse version '$ver'\n";
220}
221
222package NoSTDOUT;
223
224use Tie::Handle;
225our @ISA = qw(Tie::Handle);
226
227sub TIEHANDLE { bless \(my $s = ''), shift }
228sub PRINT {}
229sub WRITE {}
230
231package Soak::Reporter;
232
233use strict;
234
235sub cs($;$$) { my $x = shift; my($s, $p) = @_ ? @_ : ('', 's'); ($x, $x == 1 ? $s : $p) }
236
237sub new
238{
239  my $class = shift;
240  bless {
241    tests   => undef,
242    color   => 1,
243    verbose => 0,
244    @_,
245    _cur    => 0,
246    _atbol  => 1,
247    _total  => 0,
248    _good   => [],
249    _bad    => [],
250  }, $class;
251}
252
253sub colored
254{
255  my $self = shift;
256
257  if ($self->{color}) {
258    my $c = eval {
259      require Term::ANSIColor;
260      Term::ANSIColor::colored(@_);
261    };
262
263    if ($@) {
264      $self->{color} = 0;
265    }
266    else {
267      return $c;
268    }
269  }
270
271  return $_[0];
272}
273
274sub _config
275{
276  my $self = shift;
277  return $self->{config} =~ /\S+/ ? " ($self->{config})" : '';
278}
279
280sub _progress
281{
282  my $self = shift;
283  return '' unless defined $self->{tests};
284  my $tlen = length $self->{tests};
285  my $text = sprintf "[%${tlen}d/%${tlen}d] ", $self->{_cur}, $self->{tests};
286  return $self->colored($text, 'bold');
287}
288
289sub _test
290{
291  my $self = shift;
292  return $self->_progress . "Testing "
293         . $self->colored($self->{perl}, 'blue')
294         . $self->colored($self->_config, 'green');
295}
296
297sub _testlen
298{
299  my $self = shift;
300  return length("Testing " . $self->{perl} . $self->_config);
301}
302
303sub _dots
304{
305  my $self = shift;
306  return '.' x $self->_dotslen;
307}
308
309sub _dotslen
310{
311  my $self = shift;
312  return $self->{width} - length($self->{perl} . $self->_config);
313}
314
315sub _sep
316{
317  my $self = shift;
318  my $width = shift;
319  $self->print($self->colored('-'x$width, 'bold'), "\n");
320}
321
322sub _vsep
323{
324  goto &_sep if $_[0]->{verbose};
325}
326
327sub set
328{
329  my $self = shift;
330  while (@_) {
331    my($k, $v) = splice @_, 0, 2;
332    $self->{$k} = $v;
333  }
334}
335
336sub test
337{
338  my $self = shift;
339  $self->{_cur}++;
340  $self->_vsep($self->_testlen);
341  $self->print($self->_test, $self->{verbose} ? "\n" : ' ' . $self->_dots . ' ');
342  $self->_vsep($self->_testlen);
343}
344
345sub _warnings
346{
347  my($self, $mode) = @_;
348
349  my $warnings = 0;
350  my $differ   = 0;
351
352  for my $w (@{$self->{_warnings}}) {
353    if (@{$w->[1]}) {
354      $warnings += @{$w->[1]};
355      $differ++;
356    }
357  }
358
359  my $rv = '';
360
361  if ($warnings) {
362    if ($mode eq 'summary') {
363      $rv .= sprintf " (%d warning%s", cs($warnings);
364    }
365    else {
366      $rv .= "\n";
367    }
368
369    for my $w (@{$self->{_warnings}}) {
370      if (@{$w->[1]}) {
371        if ($mode eq 'detail') {
372          $rv .= "  Warnings during '$w->[0]':\n";
373          my $cnt = 1;
374          for my $msg (@{$w->[1]}) {
375            $rv .= sprintf "    [%d] %s", $cnt++, $msg;
376          }
377          $rv .= "\n";
378        }
379        else {
380          unless ($self->{verbose}) {
381            $rv .= $differ == 1 ? " during " . $w->[0]
382                                : sprintf(", %d during %s", scalar @{$w->[1]}, $w->[0]);
383          }
384        }
385      }
386    }
387
388    if ($mode eq 'summary') {
389      $rv .= ')';
390    }
391  }
392
393  return $rv;
394}
395
396sub _result
397{
398  my($self, $text, $color) = @_;
399  my $sum = $self->_warnings('summary');
400  my $len = $self->_testlen + $self->_dotslen + length($text) + length($sum) + 2;
401
402  $self->_vsep($len);
403  $self->print($self->_test, ' ', $self->_dots, ' ') if $self->{verbose} || $self->{_atbol};
404  $self->print($self->colored($text, $color));
405  $self->print($self->colored($sum, 'red'));
406  $self->print("\n");
407  $self->_vsep($len);
408  $self->print($self->_warnings('detail')) if $self->{verbose};
409  $self->{_total}++;
410}
411
412sub passed
413{
414  my $self = shift;
415  $self->_result(@_, 'ok', 'bold green');
416  push @{$self->{_good}}, [$self->{perl}, $self->{config}];
417}
418
419sub failed
420{
421  my $self = shift;
422  $self->_result(@_, 'not ok', 'bold red');
423  push @{$self->{_bad}}, [$self->{perl}, $self->{config}];
424}
425
426sub warnings
427{
428  my $self = shift;
429  $self->{_warnings} = \@_;
430}
431
432sub _tobol
433{
434  my $self = shift;
435  print "\n" unless $self->{_atbol};
436  $self->{_atbol} = 1;
437}
438
439sub print
440{
441  my $self = shift;
442  my $text = join '', @_;
443  print $text;
444  $self->{_atbol} = $text =~ /[\r\n]$/;
445}
446
447sub say
448{
449  my $self = shift;
450  $self->_tobol;
451  $self->print(@_, "\n");
452}
453
454sub vsay
455{
456  goto &say if $_[0]->{verbose};
457}
458
459sub warn
460{
461  my $self = shift;
462  $self->say($self->colored(join('', @_), 'red'));
463}
464
465sub die
466{
467  my $self = shift;
468  $self->say($self->colored(join('', 'FATAL: ', @_), 'bold red'));
469  exit -1;
470}
471
472sub status
473{
474  my($self, $text) = @_;
475  $self->_tobol;
476  $self->print($self->colored($text, 'bold'), "\n");
477}
478
479sub finish
480{
481  my $self = shift;
482
483  if (@{$self->{_bad}}) {
484    $self->status("\nFailed with:");
485    for my $fail (@{$self->{_bad}}) {
486      my($perl, $cfg) = @$fail;
487      $self->set(config => $cfg);
488      $self->say("    ", $self->colored($perl, 'blue'), $self->colored($self->_config, 'green'));
489    }
490  }
491
492  $self->status(sprintf("\nPassed with %d of %d combination%s.\n",
493                        scalar @{$self->{_good}}, cs($self->{_total})));
494
495  return scalar @{$self->{_bad}};
496}
497
498__END__
499
500=head1 NAME
501
502soak - Test Perl modules with multiple Perl releases
503
504=head1 SYNOPSIS
505
506  soak [options] [perl ...]
507
508  --make=program     override name of make program ($Config{make})
509  --min=version      use at least this version of perl
510  --mmargs=options   pass options to Makefile.PL (multiple --mmargs
511                     possible)
512  --verbose          be verbose
513  --nocolor          don't use colored output
514
515=head1 DESCRIPTION
516
517The F<soak> utility can be used to test Perl modules with
518multiple Perl releases or build options. It automates the
519task of running F<Makefile.PL> and the modules test suite.
520
521It is not primarily intended for cross-platform checking,
522so don't expect it to work on all platforms.
523
524=head1 EXAMPLES
525
526To test your favourite module, just change to its root
527directory (where the F<Makefile.PL> is located) and run:
528
529  soak
530
531This will automatically look for Perl binaries installed
532on your system.
533
534Alternatively, you can explicitly pass F<soak> a list of
535Perl binaries:
536
537  soak perl5.8.6 perl5.9.2
538
539Last but not least, you can pass it a list of directories
540to recursively search for Perl binaries, for example:
541
542  soak /tmp/perl/install /usr/bin
543
544All of the above examples will run
545
546  perl Makefile.PL
547  make
548  make test
549
550for your module and report success or failure.
551
552If your F<Makefile.PL> can take arguments, you may also
553want to test different configurations for your module.
554You can do so with the I<--mmargs> option:
555
556  soak --mmargs=' ' --mmargs='CCFLAGS=-Wextra' --mmargs='enable-debug'
557
558This will run
559
560  perl Makefile.PL
561  make
562  make test
563  perl Makefile.PL CCFLAGS=-Wextra
564  make
565  make test
566  perl Makefile.PL enable-debug
567  make
568  make test
569
570for each Perl binary.
571
572If you have a directory full of different Perl binaries,
573but your module isn't expected to work with ancient perls,
574you can use the I<--min> option to specify the minimum
575version a Perl binary must have to be chosen for testing:
576
577  soak --min=5.8.1
578
579Usually, the output of F<soak> is rather terse, to give
580you a good overview. If you'd like to see more of what's
581going on, use the I<--verbose> option:
582
583  soak --verbose
584
585=head1 COPYRIGHT
586
587Version 3.x, Copyright (c) 2004-2013, Marcus Holland-Moritz.
588
589Version 2.x, Copyright (C) 2001, Paul Marquess.
590
591Version 1.x, Copyright (C) 1999, Kenneth Albanowski.
592
593This program is free software; you can redistribute it and/or
594modify it under the same terms as Perl itself.
595
596=head1 SEE ALSO
597
598See L<Devel::PPPort>.
599
600=cut
601