File Coverage

blib/lib/Test/DependentModules.pm
Criterion Covered Total %
statement 85 274 31.0
branch 10 78 12.8
condition 1 21 4.7
subroutine 27 54 50.0
pod 3 3 100.0
total 126 430 29.3


line stmt bran cond sub pod time code
1             package Test::DependentModules;
2              
3 1     1   176101 use strict;
  1         7  
  1         30  
4 1     1   5 use warnings;
  1         3  
  1         24  
5 1     1   526 use autodie;
  1         14463  
  1         4  
6              
7             our $VERSION = '0.27';
8              
9             # CPAN::Reporter spits out random output we don't want, and we don't want to
10             # report these tests anyway.
11             BEGIN {
12             ## no critic (Variables::RequireLocalizedPunctuationVars)
13 1     1   6819 $INC{'CPAN/Reporter.pm'} = 0;
14             }
15              
16 1     1   575 use Capture::Tiny qw( capture );
  1         24810  
  1         62  
17 1     1   8 use Cwd qw( abs_path );
  1         2  
  1         42  
18 1     1   5 use Exporter qw( import );
  1         4  
  1         25  
19 1     1   5 use File::Path qw( rmtree );
  1         2  
  1         35  
20 1     1   5 use File::Spec;
  1         2  
  1         19  
21 1     1   5 use File::Temp qw( tempdir );
  1         1  
  1         33  
22 1     1   6 use File::chdir;
  1         2  
  1         93  
23 1     1   582 use IO::Handle::Util qw( io_from_write_cb );
  1         15961  
  1         7  
24 1     1   816 use IPC::Run3 qw( run3 );
  1         4941  
  1         77  
25 1     1   509 use Log::Dispatch;
  1         226734  
  1         41  
26 1     1   493 use MetaCPAN::Client;
  1         323624  
  1         36  
27 1     1   19 use Test::Builder;
  1         3  
  1         25  
28 1     1   5 use Try::Tiny;
  1         3  
  1         2760  
29              
30             our @EXPORT_OK = qw( test_all_dependents test_module test_modules );
31              
32             ## no critic (Variables::RequireLocalizedPunctuationVars)
33             $ENV{PERL5LIB} = join q{:}, ( $ENV{PERL5LIB} || q{} ),
34             File::Spec->catdir( _temp_lib_dir(), 'lib', 'perl5' );
35             $ENV{PERL_AUTOINSTALL} = '--defaultdeps';
36             $ENV{PERL_MM_USE_DEFAULT} = 1;
37             ## use critic
38              
39             my $Test = Test::Builder->new;
40              
41             sub test_all_dependents {
42 0     0 1 0 my $module = shift;
43 0         0 my $params = shift;
44              
45 0         0 _load_cpan();
46 0         0 _make_logs();
47              
48 0         0 my @deps = _get_deps( $module, $params );
49 0 0       0 unless (@deps) {
50 0         0 $Test->plan(
51             skip_all => "Could not find any distros that depend on $module" );
52 0         0 return 0;
53             }
54              
55 0         0 $Test->plan( tests => scalar @deps );
56              
57 0         0 local $Test::Builder::Level = $Test::Builder::Level + 1;
58 0         0 test_modules(@deps);
59             }
60              
61             sub _get_deps {
62 0     0   0 my $module = shift;
63 0         0 my $params = shift;
64              
65 0         0 $module =~ s/::/-/g;
66              
67 0         0 my $rev_deps = MetaCPAN::Client->new->rev_deps($module);
68              
69             my $allow
70             = $params->{filter} ? $params->{filter}
71 0     0   0 : $params->{exclude} ? sub { $_[0] !~ /$params->{exclude}/ }
72 0 0   0   0 : sub {1};
  0 0       0  
73              
74 0         0 my @deps;
75 0         0 while ( my $dep = $rev_deps->next ) {
76 0         0 my $dist = $dep->distribution;
77              
78 0 0       0 next unless $allow->($dist);
79 0 0       0 next if $dist =~ /^(?:Task|Bundle)/;
80              
81 0         0 push @deps => $dist;
82             }
83              
84             ## no critic (Subroutines::ProhibitReturnSort)
85 0         0 return sort { lc $a cmp lc $b } @deps;
  0         0  
86             }
87              
88             sub test_modules {
89 0     0 1 0 _load_cpan();
90 0         0 _make_logs();
91              
92 0         0 my $parallel = 0;
93 0 0 0     0 if ( $ENV{PERL_TEST_DM_PROCESSES}
94             && $ENV{PERL_TEST_DM_PROCESSES} > 1 ) {
95              
96 0 0       0 if ( eval { require Parallel::ForkManager; 1; } ) {
  0         0  
  0         0  
97 0         0 $parallel = 1;
98             }
99             else {
100 0         0 warn
101             'Cannot run multiple processes without the Parallel::ForkManager module.';
102             }
103             }
104              
105 0 0       0 if ($parallel) {
106 0         0 _test_in_parallel(@_);
107             }
108             else {
109 0         0 local $Test::Builder::Level = $Test::Builder::Level + 1;
110 0         0 for my $module (@_) {
111 0         0 test_module($module);
112             }
113             }
114             }
115              
116             sub _test_in_parallel {
117 0     0   0 my @modules = @_;
118              
119 0         0 my $pm = Parallel::ForkManager->new( $ENV{PERL_TEST_DM_PROCESSES} );
120              
121             $pm->run_on_finish(
122             sub {
123 0     0   0 shift; # pid
124 0         0 shift; # program exit code
125 0         0 shift; # ident
126 0         0 shift; # exit signal
127 0         0 shift; # core dump
128 0         0 my $results = shift;
129              
130 0         0 local $Test::Builder::Level = $Test::Builder::Level + 1;
131 0         0 _test_report($results);
132             }
133 0         0 );
134              
135 0         0 for my $module (@_) {
136 0 0       0 $pm->start and next;
137              
138 0         0 local $Test::Builder::Level = $Test::Builder::Level + 1;
139 0         0 test_module( $module, $pm );
140             }
141              
142 0         0 $pm->wait_all_children;
143             }
144              
145             sub test_module {
146 0     0 1 0 my $name = shift;
147 0         0 my $pm = shift;
148              
149 0         0 _load_cpan();
150 0         0 _make_logs();
151              
152 0         0 $name =~ s/-/::/g;
153              
154 0         0 my $dist = _get_distro($name);
155 0 0       0 unless ($dist) {
156 0         0 _finish_test(
157             $pm,
158             {
159             name => $name,
160             skipped => qq{Could't find a distro for $name},
161             }
162             );
163 0         0 return;
164             }
165              
166 0         0 $Test->diag( 'Testing ' . $dist->base_id );
167              
168 0 0       0 unless ($dist) {
169 0         0 $name =~ s/::/-/g;
170 0 0       0 my $todo
171             = defined( $Test->todo )
172             ? ' (TODO: ' . $Test->todo . ')'
173             : q{};
174 0         0 my $summary = "FAIL${todo}: $name - ??? - ???";
175 0         0 my $output = "Could not find $name on CPAN\n";
176              
177 0         0 _finish_test(
178             $pm, {
179             name => $name,
180             passed => 0,
181             summary => $summary,
182             output => $output,
183             stderr => $output,
184             }
185             );
186 0         0 return;
187             }
188              
189 0         0 $name = $dist->base_id;
190              
191             my $success = try {
192 0     0   0 capture { _install_prereqs($dist) };
  0         0  
193 0         0 1;
194             }
195             catch {
196 0     0   0 local $Test::Builder::Level = $Test::Builder::Level + 1;
197 0         0 my $msg = "Installing prereqs for $name failed: $_";
198 0         0 $msg =~ s/\s*$//;
199 0         0 $msg =~ s/\n/\t/g;
200              
201 0         0 _finish_test(
202             $pm,
203             , {
204             name => $name,
205             skipped => $msg,
206             }
207             );
208 0         0 return;
209 0         0 };
210              
211 0 0       0 return unless $success;
212              
213 0         0 my ( $passed, $output, $stderr ) = _run_tests_for_dir( $dist->dir );
214              
215             # A lot of modules seem to have cargo-culted a diag() that looks like this
216             # ...
217             #
218             # Testing Foo::Bar 0.01, Perl 5.00801, /usr/bin/perl
219 0 0 0     0 $stderr = q{}
220             if defined $stderr && $stderr =~ /\A\# Testing [\w:]+ [^\n]+\Z/;
221              
222 0 0 0     0 my $status = $passed && $stderr ? 'WARN' : $passed ? 'PASS' : 'FAIL';
    0          
223 0 0       0 if ( my $reason = $Test->todo ) {
224 0         0 $status .= " (TODO: $reason)";
225             }
226              
227 0         0 my $summary
228             = "$status: $name - " . $dist->base_id . ' - ' . $dist->author->id;
229              
230 0         0 _finish_test(
231             $pm,
232             {
233             name => $name,
234             passed => $passed,
235             summary => $summary,
236             output => $output,
237             stderr => $stderr,
238             }
239             );
240             }
241              
242             sub _finish_test {
243 0     0   0 my $pm = shift;
244 0         0 my $results = shift;
245              
246 0 0       0 if ($pm) {
247 0         0 $pm->finish( 0, $results );
248             }
249             else {
250 0         0 local $Test::Builder::Level = $Test::Builder::Level + 2;
251 0         0 _test_report($results);
252             }
253             }
254              
255             ## no critic (Subroutines::ProhibitManyArgs)
256             sub _test_report {
257 0     0   0 my $results = shift;
258              
259 0 0       0 if ( $results->{skipped} ) {
260 0         0 _status_log("UNKNOWN: $results->{name} ($results->{skipped})\n");
261 0         0 _error_log("UNKNOWN: $results->{name} ($results->{skipped})\n");
262              
263 0         0 $Test->diag("Skipping $results->{name}: $results->{skipped}");
264 0         0 $Test->skip( $results->{skipped} );
265             }
266             else {
267 0         0 _status_log("$results->{summary}\n");
268 0         0 _error_log("$results->{summary}\n");
269              
270 0         0 $Test->ok( $results->{passed}, "$results->{name} passed all tests" );
271             }
272              
273 0 0 0     0 if ( $results->{passed} || $results->{skipped} ) {
274 0         0 _error_log("\n");
275             }
276             else {
277 0         0 _error_log( q{-} x 50 );
278 0         0 _error_log("\n");
279 0 0       0 _error_log("$results->{output}\n") if defined $results->{output};
280 0 0       0 _error_log("$results->{stderr}\n") if defined $results->{stderr};
281             }
282             }
283              
284             {
285             my %logs;
286              
287             sub _make_logs {
288 0 0   0   0 return if %logs;
289              
290             my $file_class = $ENV{PERL_TEST_DM_PROCESSES}
291 0 0 0     0 && $ENV{PERL_TEST_DM_PROCESSES} > 1 ? 'File::Locked' : 'File';
292              
293 0         0 for my $type (qw( status error prereq )) {
294 0         0 $logs{$type} = Log::Dispatch->new(
295             outputs => [
296             [
297             $file_class,
298             min_level => 'debug',
299             filename => _log_filename($type),
300             mode => 'append',
301             ],
302             ],
303             );
304             }
305             }
306              
307             sub _status_log {
308 0     0   0 $logs{status}->info(@_);
309             }
310              
311             sub _error_log {
312 0     0   0 $logs{error}->info(@_);
313             }
314              
315             sub _prereq_log {
316 0     0   0 $logs{prereq}->info(@_);
317             }
318             }
319              
320             sub _log_filename {
321 0     0   0 my $type = shift;
322              
323             return File::Spec->devnull
324 0 0       0 unless $ENV{PERL_TEST_DM_LOG_DIR};
325              
326             return File::Spec->catfile(
327             $ENV{PERL_TEST_DM_LOG_DIR},
328 0         0 'test-mydeps-' . $$ . q{-} . $type . '.log'
329             );
330             }
331              
332             sub _get_distro {
333 0     0   0 my $name = shift;
334              
335 0         0 my @mods = CPAN::Shell->expand( 'Module', $name );
336              
337 0 0       0 return unless @mods == 1;
338              
339 0         0 my $dist = $mods[0]->distribution;
340              
341 0 0       0 return unless $dist;
342              
343 0         0 $dist->get;
344              
345 0         0 return $dist;
346             }
347              
348             sub _install_prereqs {
349 0     0   0 my $dist = shift;
350 0   0     0 my $root_dist = shift || $dist->base_id;
351              
352 0         0 my $install_dir = _temp_lib_dir();
353              
354             ## no critic (Variables::RequireInitializationForLocalVars, Variables::ProhibitPackageVars)
355 0         0 local $CPAN::Config->{makepl_arg} .= " INSTALL_BASE=$install_dir";
356             local $CPAN::Config->{mbuild_install_arg}
357 0         0 .= " --install_base $install_dir";
358             ## use critic
359              
360 0         0 my $for_dist = $dist->base_id;
361              
362 0         0 for my $prereq ( $dist->unsat_prereq('configure_requires_later') ) {
363 0         0 _install_prereq( $prereq->[0], $for_dist, $root_dist );
364             }
365              
366 0         0 $dist->undelay;
367 0         0 $dist->make;
368              
369 0         0 for my $prereq ( $dist->unsat_prereq('later') ) {
370 0         0 _install_prereq( $prereq->[0], $for_dist, $root_dist );
371             }
372              
373 0         0 $dist->undelay;
374             }
375              
376             sub _install_prereq {
377 0     0   0 my $prereq = shift;
378 0         0 my $for_dist = shift;
379 0         0 my $root_dist = shift;
380              
381 0 0       0 return if $prereq eq 'perl';
382              
383 0         0 my $for = "for $for_dist";
384 0 0       0 if ( $for_dist ne $root_dist ) {
385 0         0 $for .= " (started with $root_dist)";
386             }
387              
388 0         0 my $dist = _get_distro($prereq);
389 0 0       0 if ( !$dist ) {
390 0         0 _prereq_log("Couldn't find $prereq $for\n");
391 0         0 next;
392             }
393              
394 0         0 _install_prereqs( $dist, $root_dist );
395              
396 0         0 my $installing = $dist->base_id;
397              
398 0         0 _prereq_log("Installing $installing $for\n");
399              
400             try {
401 0     0   0 $dist->notest;
402 0         0 $dist->install;
403             }
404             catch {
405 0     0   0 die "Installing $installing for $for_dist failed: $_";
406 0         0 };
407             }
408              
409             {
410             my $Dir;
411 1     1   15 BEGIN { $Dir = tempdir( CLEANUP => 1 ); }
412              
413             sub _temp_lib_dir {
414 1     1   23 return $Dir;
415             }
416             }
417              
418             sub _run_tests_for_dir {
419 7     7   1512730 my $dir = shift;
420              
421 7         397 local $CWD = $dir;
422              
423 7 100       1321 if ( -e 'Build.PL' ) {
424             return
425 4 50       104 unless _run_commands(
426             ['./Build'],
427             );
428             }
429             else {
430             return
431 3 50       94 unless _run_commands(
432             ['make'],
433             );
434             }
435              
436 7         214 return _run_tests();
437             }
438              
439             sub _run_commands {
440 7     7   87 for my $cmd (@_) {
441 7         42 my $output;
442              
443             my $success = try {
444 7     7   1606 run3( $cmd, \undef, \$output, \$output );
445             }
446             catch {
447 0     0   0 $output .= "Couldn't run @$cmd: $_";
448 0         0 return;
449 7         534 };
450              
451 7 50       1984094 return ( 0, $output )
452             unless $success;
453             }
454              
455 7         190 return 1;
456             }
457              
458             sub _run_tests {
459 7     7   61 my $output = q{};
460 7         28 my $error = q{};
461              
462             my $stderr = sub {
463 6     6   782546 my $line = shift;
464              
465 6         35 $output .= $line;
466 6         137 $error .= $line;
467 7         135 };
468              
469 7         71 my $cmd;
470 7 100       370 if ( -e 'Build' ) {
    50          
471 4         46 $cmd = [qw( ./Build test )];
472             }
473             elsif ( -e 'Makefile' ) {
474 3         51 $cmd = [qw( make test )];
475             }
476             else {
477 0         0 return ( 0, "Cannot find a Build or Makefile file in $CWD" );
478             }
479              
480 7         69 my $passed;
481             try {
482 7     7   1196 run3( $cmd, undef, \$output, $stderr );
483 7 100       2818496 if ( $? == 0 ) {
484 6   33     497 $passed = $output eq q{}
485             || $output =~ /Result: (?:PASS|NOTESTS)|No tests defined/;
486             }
487             }
488             catch {
489 0     0   0 $output .= "Couldn't run @$cmd: $_";
490 0         0 $error .= "Couldn't run @$cmd: $_";
491 7         399 };
492              
493 7         1799 return ( $passed, $output, $error );
494             }
495              
496             {
497             my $LOADED_CPAN = 0;
498              
499             sub _load_cpan {
500             ## no critic (TestingAndDebugging::ProhibitNoWarnings)
501 1     1   1310 no warnings 'once';
  1         3  
  1         107  
502 0 0   0     return if $LOADED_CPAN;
503              
504 0           require CPAN;
505 0           require CPAN::Shell;
506              
507             ## no critic (InputOutput::RequireBriefOpen)
508 0           open my $fh, '>', File::Spec->devnull;
509              
510             {
511 1     1   7 no warnings 'redefine';
  1         2  
  1         396  
  0            
512 0     0     *CPAN::Shell::report_fh = sub {$fh};
  0            
513             }
514              
515             ## no critic (Variables::ProhibitPackageVars)
516 0           $CPAN::Be_Silent = 1;
517              
518 0           CPAN::HandleConfig->load;
519 0           CPAN::Shell::setup_output();
520 0           CPAN::Index->reload('force');
521              
522 0           $CPAN::Config->{test_report} = 0;
523 0           $CPAN::Config->{mbuildpl_arg} .= ' --quiet';
524 0           $CPAN::Config->{prerequisites_policy} = 'follow';
525 0           $CPAN::Config->{make_install_make_command} =~ s/^sudo //;
526 0           $CPAN::Config->{mbuild_install_build_command} =~ s/^sudo //;
527 0           $CPAN::Config->{make_install_arg} =~ s/UNINST=1//;
528 0           $CPAN::Config->{mbuild_install_arg} =~ s/--uninst\s+1//;
529              
530 0 0         if ( $ENV{PERL_TEST_DM_CPAN_VERBOSE} ) {
531 0     0     $fh = io_from_write_cb( sub { $Test->diag( $_[0] ) } );
  0            
532             }
533              
534 0           $LOADED_CPAN = 1;
535              
536 0           return;
537             }
538             }
539              
540             1;
541              
542             # ABSTRACT: Test all modules which depend on your module
543              
544             __END__
545              
546             =pod
547              
548             =encoding UTF-8
549              
550             =head1 NAME
551              
552             Test::DependentModules - Test all modules which depend on your module
553              
554             =head1 VERSION
555              
556             version 0.27
557              
558             =head1 SYNOPSIS
559              
560             use Test::DependentModules qw( test_all_dependents );
561              
562             test_all_dependents('My::Module');
563              
564             # or ...
565              
566             use Test::DependentModules qw( test_modules );
567             use Test::More tests => 3;
568              
569             test_modules( 'Exception::Class', 'DateTime', 'Log::Dispatch' );
570              
571             =head1 DESCRIPTION
572              
573             B<WARNING>: The tests this module does should B<never> be run as part of a
574             normal CPAN install!
575              
576             This module is intended as a tool for module authors who would like to easily
577             test that a module release will not break dependencies. This is particularly
578             useful for module authors (like myself) who have modules which are a
579             dependency of many other modules.
580              
581             =head2 How It Works
582              
583             Internally, this module will download dependencies from CPAN and run their
584             tests. If those dependencies in turn have unsatisfied dependencies, they are
585             installed into a temporary directory. These second-level (and third-, etc)
586             dependencies are I<not> tested.
587              
588             In order to avoid prompting, this module sets C<$ENV{PERL_AUTOINSTALL}> to
589             C<--defaultdeps> and sets C<$ENV{PERL_MM_USE_DEFAULT}> to a true value.
590              
591             Nonetheless, some ill-behaved modules will I<still> wait for a
592             prompt. Unfortunately, because of the way this module attempts to keep output
593             to a minimum, you won't see these prompts. Patches are welcome.
594              
595             =head2 Running Tests in Parallel
596              
597             If you're testing a lot of modules, you might benefit from running tests in
598             parallel. You'll need to have L<Parallel::ForkManager> installed for this to
599             work.
600              
601             Set the C<$ENV{PERL_TEST_DM_PROCESSES}> env var to a value greater than 1 to
602             enable parallel testing.
603              
604             =head1 FUNCTIONS
605              
606             This module optionally exports three functions:
607              
608             =head2 test_all_dependents( $module, { filter => sub { ... } } )
609              
610             Given a module or distro name, this function uses L<MetaCPAN::Client> to find
611             all its dependencies and test them. It will set a test plan for you.
612              
613             If you provide a C<filter> sub, it will be called with a single argument, the
614             I<distribution name>, which will be something like "Test-DependentModules"
615             (note the lack of colons). The filter should return a true or false value to
616             indicate whether or not to test that distribution.
617              
618             If you don't provide a filter, you can provide a regex to use by passing an
619             C<exclude> key in the hashref. Anything that matches the regex is excluded.
620              
621             Additionally, any distribution name starting with "Task" or "Bundle" is always
622             excluded.
623              
624             =head2 test_modules(@names)
625              
626             Given a list of module names, this function will test them all. You can use
627             this if you'd prefer to hard code a list of modules to test.
628              
629             In this case, you will have to handle your own test planning.
630              
631             =head2 test_module($name)
632              
633             B<DEPRECATED>. Use the C<test_modules()> sub instead, so you can optionally
634             run tests in parallel.
635              
636             Given a module name, this function will test it. You can use this if you'd
637             prefer to hard code a list of modules to test.
638              
639             In this case, you will have to handle your own test planning.
640              
641             =head1 PERL5LIB FOR DEPENDENCIES
642              
643             If you want to include a module-to-be-released in the path seen by
644             dependencies, you must make sure that the correct path ends up in
645             C<$ENV{PERL5LIB}>. If you use C<prove -l> or C<prove -b> to run tests, then
646             that will happen automatically.
647              
648             =head1 WARNINGS, LOGGING AND VERBOSITY
649              
650             By default, this module attempts to quiet down CPAN and the module building
651             toolchain as much as possible. However, when there are test failures in a
652             dependency it's nice to see the output.
653              
654             In addition, if the tests spit out warnings but still pass, this will just be
655             treated as a pass.
656              
657             If you enable logging, this module log all successes, warnings, and failures,
658             along with the full output of the test suite for each dependency. In addition,
659             it logs what prereqs it installs, since you may want to install some of them
660             permanently to speed up future tests.
661              
662             To enable logging, you must provide a directory to which log files will be
663             written. The log file names are of the form C<test-my-deps-$$-$type.log>,
664             where C<$type> is one of "status", "error", or "prereq".
665              
666             The directory should be provided in C<$ENV{PERL_TEST_DM_LOG_DIR}>. The
667             directory must already exist.
668              
669             You also can enable verbose output from the L<CPAN> package by setting the
670             C<$ENV{PERL_TEST_DM_CPAN_VERBOSE}> variable to a true value.
671              
672             =head1 SUPPORT
673              
674             Bugs may be submitted at L<http://rt.cpan.org/Public/Dist/Display.html?Name=Test-DependentModules> or via email to L<bug-test-dependentmodules@rt.cpan.org|mailto:bug-test-dependentmodules@rt.cpan.org>.
675              
676             I am also usually active on IRC as 'autarch' on C<irc://irc.perl.org>.
677              
678             =head1 SOURCE
679              
680             The source code repository for Test-DependentModules can be found at L<https://github.com/houseabsolute/Test-DependentModules>.
681              
682             =head1 DONATIONS
683              
684             If you'd like to thank me for the work I've done on this module, please
685             consider making a "donation" to me via PayPal. I spend a lot of free time
686             creating free software, and would appreciate any support you'd care to offer.
687              
688             Please note that B<I am not suggesting that you must do this> in order for me
689             to continue working on this particular software. I will continue to do so,
690             inasmuch as I have in the past, for as long as it interests me.
691              
692             Similarly, a donation made in this way will probably not make me work on this
693             software much more, unless I get so many donations that I can consider working
694             on free software full time (let's all have a chuckle at that together).
695              
696             To donate, log into PayPal and send money to autarch@urth.org, or use the
697             button at L<http://www.urth.org/~autarch/fs-donation.html>.
698              
699             =head1 AUTHOR
700              
701             Dave Rolsky <autarch@urth.org>
702              
703             =head1 CONTRIBUTORS
704              
705             =for stopwords Graham Knop Jesse Luehrs mickey Olaf Alders Sawyer X
706              
707             =over 4
708              
709             =item *
710              
711             Graham Knop <haarg@haarg.org>
712              
713             =item *
714              
715             Jesse Luehrs <doy@tozt.net>
716              
717             =item *
718              
719             mickey <mickey75@gmail.com>
720              
721             =item *
722              
723             Olaf Alders <olaf@wundersolutions.com>
724              
725             =item *
726              
727             Sawyer X <xsawyerx@cpan.org>
728              
729             =back
730              
731             =head1 COPYRIGHT AND LICENSE
732              
733             This software is Copyright (c) 2019 by Dave Rolsky.
734              
735             This is free software, licensed under:
736              
737             The Artistic License 2.0 (GPL Compatible)
738              
739             The full text of the license can be found in the
740             F<LICENSE> file included with this distribution.
741              
742             =cut