File Coverage

lib/UR/Namespace/Command/Test/Run.pm
Criterion Covered Total %
statement 71 459 15.4
branch 0 184 0.0
condition 0 40 0.0
subroutine 24 54 44.4
pod 1 5 20.0
total 96 742 12.9


line stmt bran cond sub pod time code
1             package UR::Namespace::Command::Test::Run;
2              
3             #
4             # single dash command line params go to perl
5             # double dash command line params go to the script
6             #
7              
8 1     1   22 use warnings;
  1         2  
  1         28  
9 1     1   4 use strict;
  1         2  
  1         16  
10 1     1   4 use File::Temp; # qw/tempdir/;
  1         1  
  1         106  
11 1     1   376 use Path::Class; # qw(file dir);
  1         11574  
  1         45  
12 1     1   6 use DBI;
  1         1  
  1         31  
13 1     1   4 use Cwd;
  1         3  
  1         34  
14 1     1   4 use UR;
  1         1  
  1         6  
15             our $VERSION = "0.46"; # UR $VERSION;
16 1     1   5 use File::Find;
  1         1  
  1         38  
17              
18 1     1   437 use TAP::Harness;
  1         3592  
  1         8  
19 1     1   404 use TAP::Formatter::Console;
  1         2004  
  1         9  
20 1     1   469 use TAP::Parser::Aggregator;
  1         4601  
  1         7  
21              
22             UR::Object::Type->define(
23             class_name => __PACKAGE__,
24             is => "UR::Namespace::Command::Base",
25             has => [
26             bare_args => { is_optional => 1, is_many => 1, shell_args_position => 1, is_input => 1 },
27             recurse => { is => 'Boolean',
28             doc => 'Run all .t files in the current directory, and in recursive subdirectories.' },
29             list => { is => 'Boolean',
30             doc => 'List the tests, but do not actually run them.' },
31             noisy => { is => 'Boolean',
32             doc => "doesn't redirect stdout",is_optional => 1 },
33             perl_opts => { is => 'String',
34             doc => 'Override options to the Perl interpreter when running the tests (-d:Profile, etc.)', is_optional => 1,
35             default_value => '' },
36             lsf => { is => 'Boolean',
37             doc => 'If true, tests will be submitted as jobs via bsub' },
38             color => { is => 'Boolean',
39             doc => 'Use TAP::Harness::Color to generate color output',
40             default_value => 0 },
41             junit => { is => 'Boolean',
42             doc => 'Run all tests with junit style XML output. (requires TAP::Formatter::JUnit)' },
43             ],
44             has_optional => [
45             'time' => { is => 'String',
46             doc => 'Write timelog sum to specified file', },
47             long => { is => 'Boolean',
48             doc => 'Run tests including those flagged as long', },
49             cover => { is => 'List',
50             doc => 'Cover only this(these) modules', },
51             cover_svn_changes => { is => 'Boolean',
52             doc => 'Cover modules modified in svn status', },
53             cover_svk_changes => { is => 'Boolean',
54             doc => 'Cover modules modified in svk status', },
55             cover_cvs_changes => { is => 'Boolean',
56             doc => 'Cover modules modified in cvs status', },
57             cover_git_changes => { is => 'Boolean',
58             doc => 'Cover modules modified in git status', },
59             coverage => { is => 'Boolean',
60             doc => 'Invoke Devel::Cover', },
61             script_opts => { is => 'String',
62             doc => 'Override options to the test case when running the tests (--dump-sql --no-commit)',
63             default_value => '' },
64             callcount => { is => 'Boolean',
65             doc => 'Count the number of calls to each subroutine/method', },
66             jobs => { is => 'Number',
67             doc => 'How many tests to run in parallel',
68             default_value => 1, },
69             lsf_params => { is => 'String',
70             doc => 'Params passed to bsub while submitting jobs to lsf',
71             default_value => '-q short -R select[type==LINUX64]' },
72             run_as_lsf_helper => { is => 'String',
73             doc => 'Used internally by the test harness', },
74             inc => { is => 'String',
75             doc => 'Additional paths for @INC, alias for -I',
76             is_many => 1, },
77             ],
78             );
79              
80 0     0 0 0 sub help_brief { "Run the test suite against the source tree." }
81              
82             sub help_synopsis {
83             return <<'EOS'
84             cd MyNamespace
85             ur test run --recurse # run all tests in the namespace or under the current directory
86             ur test run # runs all tests in the t/ directory under pwd
87             ur test run t/mytest1.t My/Class.t # run specific tests
88             ur test run -v -t --cover-svk-changes # run tests to cover latest svk updates
89             ur test run -I ../some/path/ # Adds ../some/path to perl's @INC through -I
90             ur test run --junit # writes test output in junit's xml format (consumable by Hudson integration system)
91             EOS
92 0     0 0 0 }
93              
94             sub help_detail {
95             return <
96             This command is like "prove" or "make test", running the test suite for the
97             current namespace.
98              
99             The default behavior is to search for tests by finding directories named 't'
100             under the current directory, and then find files matching *.t under those
101             directories. If the --recurse option is used, then it will search for *.t
102             files anywhere under the current directory.
103              
104             It uses many of the TAP:: family of modules, and so the underlying behavior
105             can be influenced by changing the environment variables they use such
106             as PERL_TEST_HARNESS_DUMP_TAP and ALLOW_PASSING_TODOS. These modules include
107             TAP::Harness, TAP::Formatter::Console, TAP::Formatter::Junit, TAP::Parser
108             and others.
109             EOS
110 0     0 0 0 }
111              
112              
113             # We're overriding create() so it'll run in a Namespace directory or
114             # not. If run within a namespace dir, then it'll run all the tests under
115             # the namespace. If not, it'll run all the tests in the current dir
116             sub create {
117 0     0 1 0 my $class = shift;
118              
119 0         0 my $bx = $class->define_boolexpr(@_);
120 0 0       0 unless ($bx->specifies_value_for('namespace_name')) {
121 0         0 my $namespace_name = $class->resolve_namespace_name_from_cwd();
122 0   0     0 $namespace_name ||= 'UR'; # Pretend we're running in the UR namespace
123 0         0 $bx = $bx->add_filter(namespace_name => $namespace_name);
124             }
125 0         0 return $class->SUPER::create($bx);
126             }
127              
128              
129             # Override so we'll allow '-I' on the command line
130             sub _shell_args_getopt_specification {
131 0     0   0 my $self = shift;
132              
133 0         0 my($params_hash, @spec) = $self->SUPER::_shell_args_getopt_specification();
134              
135 0         0 foreach (@spec) {
136 0 0       0 if ($_ eq 'inc=s@') {
137 0         0 $_ = 'inc|I=s@';
138 0         0 last;
139             }
140             }
141 0         0 return($params_hash, @spec);
142             }
143              
144             sub execute {
145 0     0   0 my $self = shift;
146              
147             #$DB::single = 1;
148              
149 0         0 my $working_path;
150 0 0       0 if ($self->namespace_name ne 'UR') {
151 0         0 $self->status_message("Running tests within namespace ".$self->namespace_name);
152 0         0 $working_path = $self->namespace_path;
153             } else {
154 0         0 $self->status_message("Running tests under the current directory");
155 0         0 $working_path = '.';
156             }
157              
158 0 0       0 if ($self->run_as_lsf_helper) {
159 0         0 $self->_lsf_test_worker($self->run_as_lsf_helper);
160 0         0 exit(0);
161             }
162              
163             # nasty parsing of command line args
164             # this may no longer be needed..
165 0         0 my @tests = $self->bare_args;
166              
167 0 0       0 if ($self->recurse) {
    0          
168 0 0       0 if (@tests) {
169 0         0 $self->error_message("Cannot currently combine the recurse option with a specific test list.");
170 0         0 return;
171             }
172 0         0 @tests = $self->_find_t_files_under_directory($working_path);
173             }
174             elsif (not @tests) {
175 0         0 my @dirs;
176             File::Find::find(sub {
177 0 0 0 0   0 if ($_ eq 't' and -d $_) {
178 0         0 push @dirs, $File::Find::name;
179             }
180             },
181 0         0 $working_path);
182              
183 0 0       0 if (@dirs == 0) {
184 0         0 $self->error_message("No 't' directories found. Write some tests.");
185 0         0 return;
186             }
187 0         0 chomp @dirs;
188 0         0 for my $dir (@dirs) {
189 0         0 push @tests, $self->_find_t_files_under_directory($dir);
190             }
191             }
192             else {
193             # rely on the @tests list from the cmdline
194             }
195              
196             # uniqify and sort them
197 0         0 my %tests = map { $_ => 1 } @tests;
  0         0  
198 0         0 @tests = sort keys %tests;
199              
200 0 0       0 if ($self->list) {
201 0         0 $self->status_message("Tests:");
202 0         0 for my $test (@tests) {
203 0         0 $self->status_message($test);
204             }
205 0         0 return 1;
206             }
207              
208 0 0       0 if (not @tests) {
209 0         0 $self->error_message("No tests found under $working_path");
210 0         0 return;
211             }
212              
213 0         0 my $results = $self->_run_tests(@tests);
214              
215 0         0 return $results;
216             }
217              
218              
219             sub _find_t_files_under_directory {
220 0     0   0 my($self,$path) = @_;
221              
222 0         0 my @tests;
223             File::Find::find(sub {
224 0 0 0 0   0 if (m/\.t$/ and not -d $_) {
225 0         0 push @tests, $File::Find::name;
226             }
227 0         0 }, $path);
228 0         0 chomp @tests;
229 0         0 return @tests;
230             }
231              
232             # Run by the test harness when test are scheduled out via LSF
233             # $master_spec is a string like "host:port"
234             sub _lsf_test_worker {
235 0     0   0 my($self,$master_spec) = @_;
236              
237 0         0 require IO::Socket;
238              
239 0 0       0 open my $saved_stdout, ">&STDOUT" or die "Can't dup STDOUT: $!";
240 0 0       0 open my $saved_stderr, ">&STDERR" or die "Can't dup STDERR: $!";
241              
242 0         0 while(1) {
243 0 0       0 open STDOUT, ">&", $saved_stdout or die "Can't restore stdout \$saved_stdout: $!";
244 0 0       0 open STDERR, ">&", $saved_stderr or die "Can't restore stderr \$saved_stderr: $!";
245              
246 0         0 my $socket = IO::Socket::INET->new( PeerAddr => $master_spec,
247             Proto => 'tcp');
248 0 0       0 unless ($socket) {
249 0         0 die "Can't connect to test master: $!";
250             }
251              
252 0         0 $socket->autoflush(1);
253              
254 0         0 my $line = <$socket>;
255 0         0 chomp($line);
256              
257 0 0 0     0 if ($line eq '' or $line eq 'EXIT TESTS') {
258             # print STDERR "Closing\n";
259 0         0 $socket->close();
260 0         0 exit(0);
261             }
262              
263             # print "Running >>$line<<\n";
264              
265 0 0       0 open STDOUT, ">&", $socket or die "Can't redirect stdout: $!";
266 0 0       0 open STDERR, ">&", $socket or die "Can't redirect stderr: $!";
267              
268 0         0 system($line);
269              
270 0         0 $socket->close();
271             }
272             }
273              
274              
275             sub _run_tests {
276 0     0   0 my $self = shift;
277 0         0 my @tests = @_;
278              
279             # this ensures that we don't see warnings
280             # and error statuses when doing the bulk test
281 1     1   791 no warnings;
  1         2  
  1         54  
282 0         0 local $ENV{UR_TEST_QUIET} = $ENV{UR_TEST_QUIET};
283 0 0       0 unless (defined $ENV{UR_TEST_QUIET}) {
284 0         0 $ENV{UR_TEST_QUIET} = 1;
285             }
286 1     1   4 use warnings;
  1         5  
  1         305  
287              
288 0         0 local $ENV{UR_DBI_NO_COMMIT} = 1;
289              
290 0 0       0 if($self->long) {
291             # Make sure long tests run
292 0         0 $ENV{UR_RUN_LONG_TESTS}=1;
293             }
294              
295 0         0 my @cover_specific_modules;
296              
297 0 0       0 if (my $cover = $self->cover) {
298 0         0 push @cover_specific_modules, @$cover;
299             }
300              
301 0 0       0 if ($self->cover_svn_changes) {
    0          
    0          
    0          
302 0         0 push @cover_specific_modules, get_status_file_list('svn');
303             }
304             elsif ($self->cover_svk_changes) {
305 0         0 push @cover_specific_modules, get_status_file_list('svk');
306             }
307             elsif ($self->cover_git_changes) {
308 0         0 push @cover_specific_modules, get_status_file_list('git');
309             }
310             elsif ($self->cover_cvs_changes) {
311 0         0 push @cover_specific_modules, get_status_file_list('cvs');
312             }
313              
314 0 0       0 if (@cover_specific_modules) {
315 0         0 my $dbh = DBI->connect("dbi:SQLite:/gsc/var/cache/testsuite/coverage_metrics.sqlitedb","","");
316 0         0 $dbh->{PrintError} = 0;
317 0         0 $dbh->{RaiseError} = 1;
318 0         0 my %tests_covering_specified_modules;
319 0         0 for my $module_name (@cover_specific_modules) {
320 0         0 my $module_test_names = $dbh->selectcol_arrayref(
321             "select test_name from test_module_use where module_name = ?",undef,$module_name
322             );
323 0         0 for my $test_name (@$module_test_names) {
324 0   0     0 $tests_covering_specified_modules{$test_name} ||= [];
325 0         0 push @{ $tests_covering_specified_modules{$test_name} }, $module_name;
  0         0  
326             }
327             }
328              
329 0 0       0 if (@tests) {
330             # specific tests were listed: only run the intersection of that set and the covering set
331 0         0 my @filtered_tests;
332 0         0 for my $test_name (sort keys %tests_covering_specified_modules) {
333 0         0 my $specified_modules_coverted = $tests_covering_specified_modules{$test_name};
334 0         0 $test_name =~ s/^(.*?)(\/t\/.*)$/$2/g;
335 0 0       0 if (my @matches = grep { $test_name =~ $_ } @tests) {
  0         0  
336 0 0       0 if (@matches > 1) {
    0          
337 0         0 Carp::confess("test $test_name matches multiple items in the tests on the filesystem: @matches");
338             }
339             elsif (@matches == 0) {
340 0         0 Carp::confess("test $test_name matches nothing in the tests on the filesystem!");
341             }
342             else {
343 0         0 print STDERR "Running $matches[0] for modules @$specified_modules_coverted.\n";
344 0         0 push @filtered_tests, $matches[0];
345             }
346             }
347             }
348 0         0 @tests = @filtered_tests;
349             }
350             else {
351             # no tests explicitly specified on the command line: run exactly those which cover the listed modules
352 0         0 @tests = sort keys %tests_covering_specified_modules;
353             }
354 0         0 print "Running the " . scalar(@tests) . " tests which load the specified modules.\n";
355             }
356             else {
357             }
358              
359 1     1   5 use Cwd;
  1         1  
  1         400  
360 0         0 my $cwd = cwd();
361 0         0 for (@tests) {
362 0         0 s/^$cwd\///;
363             }
364              
365 0         0 my $perl_opts = $self->perl_opts;
366 0 0       0 if ($self->coverage()) {
367 0         0 $perl_opts .= ' -MDevel::Cover';
368             }
369 0 0       0 if ($self->callcount()) {
370 0         0 $perl_opts .= ' -d:callcount';
371             }
372              
373 0 0       0 if (UR::Util::used_libs()) {
374 0         0 $ENV{'PERL5LIB'} = UR::Util::used_libs_perl5lib_prefix() . $ENV{'PERL5LIB'};
375             }
376              
377 0         0 my %harness_args;
378             my $formatter;
379 0 0       0 if ($self->junit) {
380 0         0 eval "use TAP::Formatter::JUnit;";
381 0 0       0 if ($@) {
382 0         0 Carp::croak("Couldn't use TAP::Formatter::JUnit for junit output: $@");
383             }
384 0         0 %harness_args = ( formatter_class => 'TAP::Formatter::JUnit',
385             merge => 1,
386             timer => 1,
387             );
388             } else {
389 0         0 $formatter = TAP::Formatter::Console->new( {
390             jobs => $self->jobs,
391             show_count => 1,
392             color => $self->color,
393             } );
394 0         0 $formatter->quiet();
395 0         0 %harness_args = ( formatter => $formatter );
396             }
397              
398 0 0       0 $harness_args{'jobs'} = $self->jobs if ($self->jobs > 1);
399 0 0       0 if ($self->script_opts) {
400 0         0 my @opts = split(/\s+/, $self->script_opts);
401 0         0 $harness_args{'test_args'} = \@opts;
402             }
403 0         0 $harness_args{'multiplexer_class'} = 'My::TAP::Parser::Multiplexer';
404 0         0 $harness_args{'scheduler_class'} = 'My::TAP::Parser::Scheduler';
405            
406 0 0 0     0 if ($self->perl_opts || $self->inc) {
407             $harness_args{'switches'} = [ split(' ', $self->perl_opts),
408 0         0 map { '-I' . Path::Class::Dir->new($_)->absolute } $self->inc];
  0         0  
409             }
410              
411 0         0 my $timelog_sum = $self->time();
412 0         0 my $timelog_dir;
413 0 0       0 if ($timelog_sum) {
414 0         0 $harness_args{'parser_class'} = 'My::TAP::Parser::Timer';
415 0         0 $timelog_sum = Path::Class::file($timelog_sum);
416 0         0 $timelog_dir = Path::Class::dir(File::Temp::tempdir('.timelog.XXXXXX', DIR => '.', CLEANUP => 1));
417 0         0 My::TAP::Parser::Timer->set_timer_info($timelog_dir,\@tests);
418             }
419              
420 0         0 my $harness = TAP::Harness->new( \%harness_args);
421              
422 0 0       0 if ($self->lsf) {
423             # There doesn't seem to be a clean way (either by configuring the harness,
424             # subclassing the harness or parser, or hooking to a callback) to pass
425             # down the user's requested lsf params from here. So, looks like we
426             # need to hack it through here. This means that multiple 'ur test' commands
427             # running concurrently and using lsf will always use the last object's lsf_params.
428             # though I doubt anyone would ever really need to do that...
429 0         0 My::TAP::Parser::IteratorFactory::LSF->lsf_params($self->lsf_params);
430 0         0 My::TAP::Parser::IteratorFactory::LSF->max_jobs($self->jobs);
431              
432             $harness->callback('parser_args',
433             sub {
434 0     0   0 my($args, $job_as_arrayref) = @_;
435 0         0 $args->{'iterator_factory_class'} = 'My::TAP::Parser::IteratorFactory::LSF';
436 0         0 });
437              
438              
439             }
440              
441 0         0 my $aggregator = TAP::Parser::Aggregator->new();
442            
443 0         0 $aggregator->start();
444              
445 0         0 my $old_stderr;
446 0 0       0 unless ($self->noisy) {
447 0 0       0 open $old_stderr ,">&STDERR" or die "Failed to save STDERR";
448 0 0       0 open(STDERR,">/dev/null") or die "Failed to redirect STDERR";
449             }
450              
451 0         0 eval {
452 1     1   5 no warnings;
  1         2  
  1         588  
453 0         0 local %SIG = %SIG;
454 0         0 delete $SIG{__DIE__};
455 0         0 $ENV{UR_DBI_NO_COMMIT} = 1;
456             #$DB::single = 1;
457              
458             $SIG{'INT'} = sub {
459 0     0   0 print "\n\nInterrupt.\nWaiting for running tests to finish...\n\n";
460            
461 0         0 $My::TAP::Parser::Iterator::Process::LSF::SHOULD_EXIT = 1;
462 0         0 $SIG{'INT'} = 'DEFAULT';
463             #My::TAP::Parser::IteratorFactory::LSF->_kill_running_jobs();
464             #sleep(1);
465             #$aggregator->stop();
466             #$formatter->summary($aggregator);
467             #exit(0);
468 0         0 };
469            
470             #runtests(@tests);
471 0         0 $harness->aggregate_tests( $aggregator, @tests );
472             };
473              
474 0 0       0 unless ($self->noisy) {
475 0 0       0 open(STDERR,">&", $old_stderr) or die "Failed to restore STDERR";
476             }
477              
478 0         0 $aggregator->stop();
479 0 0       0 if ($@) {
480 0         0 $self->error_message($@);
481 0         0 return;
482             }
483             else {
484 0 0       0 if ($self->coverage()) {
485             # FIXME - is this GSC-specific?
486 0         0 system("chmod -R g+rwx cover_db");
487 0         0 system("/gsc/bin/cover | tee > coverage.txt");
488             }
489 0 0       0 $formatter->summary($aggregator) if ($formatter);
490             }
491              
492 0 0       0 if ($timelog_sum) {
493             $timelog_sum->openw->print(
494             sort
495 0         0 map { $_->openr->getlines }
  0         0  
496             $timelog_dir->children
497             );
498 0 0       0 if (-z $timelog_sum) {
499 0         0 unlink $timelog_sum;
500 0         0 warn "Error producing time summary file!";
501             }
502 0         0 $timelog_dir->rmtree;
503             }
504              
505 0         0 return !$aggregator->has_problems;
506             }
507              
508              
509             sub get_status_file_list {
510 0     0 0 0 my $tool = shift;
511              
512 0         0 my @status_data = eval {
513              
514 0         0 my $orig_cwd = cwd();
515 0         0 my @words = grep { length($_) } split("/",$orig_cwd);
  0         0  
516 0   0     0 while (@words and ($words[-1] ne "GSC")) {
517 0         0 pop @words;
518             }
519 0 0 0     0 unless (@words and $words[-1] eq "GSC") {
520 0         0 die "Cannot find 'GSC' directory above the cwd. Cannot auto-run $tool status.\n";
521             }
522 0         0 pop @words;
523 0         0 my $vcs_dir = "/" . join("/", @words);
524              
525 0 0       0 unless (chdir($vcs_dir)) {
526 0         0 die "Failed to change directories to $vcs_dir!";
527             }
528              
529 0         0 my @lines;
530 0 0 0     0 if ($tool eq "svn" or $tool eq "svk") {
    0          
    0          
531 0         0 @lines = IO::File->new("$tool status |")->getlines;
532             }
533             elsif ($tool eq "cvs") {
534 0         0 @lines = IO::File->new("cvs -q up |")->getlines;
535             }
536             elsif ($tool eq "git") {
537 0         0 @lines = IO::File->new("git diff --name-status |")->getlines;
538             }
539             else {
540 0         0 die "Unknown tool $tool. Try svn, svk, cvs or git.\n";
541             }
542             # All these tools have flags or other data with the filename as the last column
543 0         0 @lines = map { (split(/\s+/))[-1] } @lines;
  0         0  
544              
545 0 0       0 unless (chdir($orig_cwd)) {
546 0         0 die "Error changing directory back to the original cwd after checking file status with $tool.";
547             }
548              
549 0         0 return @lines;
550             };
551              
552 0 0       0 if ($@) {
553 0         0 die "Error checking version control status for $tool:\n$@";
554             }
555              
556 0         0 my @modules;
557 0         0 for my $line (@status_data) {
558 0         0 my ($status,$file) = ($line =~ /^(.).\s*(\S+)/);
559 0 0 0     0 next if $status eq "?" or $status eq "!";
560 0         0 print "covering $file\n";
561 0         0 push @modules, $file;
562             }
563              
564 0 0       0 unless (@modules) {
565 0         0 die "Failed to find modified modules via $tool.\n";
566             }
567              
568 0         0 return @modules;
569             }
570              
571             package My::TAP::Parser::Multiplexer;
572 1     1   5 use base 'TAP::Parser::Multiplexer';
  1         1  
  1         11  
573              
574             sub _iter {
575 0     0   0 my $self = shift;
576              
577 0         0 my $original_iter = $self->SUPER::_iter(@_);
578             return sub {
579 0     0   0 for(1) {
580             # This is a hack...
581             # the closure _iter returns does a select() on the subprocess' output handle
582             # which returns immediately after you hit control-C with no results, and the
583             # existing code in there expects real results from select(). This way, we catch
584             # the exception that happens when you do that, and give it a chance to try again
585 0         0 my @retval = eval { &$original_iter };
  0         0  
586 0 0       0 if (index($@, q(Can't use an undefined value as an ARRAY reference))>= 0) {
    0          
587 0         0 redo;
588             } elsif ($@) {
589 0         0 die $@;
590             }
591 0         0 return @retval;
592             }
593 0         0 };
594             }
595              
596             package My::TAP::Parser::IteratorFactory::LSF;
597              
598 1     1   1096 use IO::Socket;
  1         9139  
  1         5  
599 1     1   354 use IO::Select;
  1         2  
  1         28  
600              
601 1     1   4 use base 'TAP::Parser::IteratorFactory';
  1         1  
  1         13  
602              
603             # Besides being the factory for parser iterators, we're also the factory for
604             # LSF jobs
605              
606             # In the TAP::* code, they mention that the iterator factory is never instantiated,
607             # but may be in the future. When that happens, move this state info into the
608             # object that gets created/initialized
609             my $state = { 'listen' => undef, # The listening socket
610             'select' => undef, # select object for the listen socket
611             idle_jobs => [], # holds a list of file handles of connected workers
612             # running_jobs => [], # we're not tracking workers that are working for now...
613             lsf_jobids => [], # jobIDs of the worker processes
614             lsf_params => '', # params when running bsub
615             max_jobs => 0, # Max number of jobs
616             };
617              
618             sub _kill_running_jobs {
619             # The worker processes should notice when the master goes away,
620             # but just in case, we'll kill them off
621 1     1   2 foreach my $jobid ( @{$state->{'lsf_jobids'}} ) {
  1         5  
622 0           print "bkilling LSF jobid $jobid\n";
623 0           `bkill $jobid`;
624             }
625             }
626              
627             END {
628 1     1   9846 my $exit_code = $?;
629 1         4 &_kill_running_jobs();
630 1         6 $? = $exit_code; # restore the exit code, since the bkill commands set a different exit code
631             }
632              
633              
634             sub lsf_params {
635 0     0     my $proto = shift;
636              
637 0 0         if (@_) {
638 0           $state->{'lsf_params'} = shift;
639             }
640 0           return $state->{'lsf_params'};
641             }
642              
643             sub max_jobs {
644 0     0     my $proto = shift;
645              
646 0 0         if (@_) {
647 0           $state->{'max_jobs'} = shift;
648             }
649 0           return $state->{'max_jobs'};
650             }
651              
652              
653             sub make_process_iterator {
654 0     0     my $proto = shift;
655              
656 0           My::TAP::Parser::Iterator::Process::LSF->new(@_);
657             }
658              
659             sub next_idle_worker {
660 0     0     my $proto = shift;
661              
662 0           $proto->process_events();
663              
664 0           while(! @{$state->{'idle_jobs'}} ) {
  0            
665              
666 0           my $did_create_new_worker = 0;
667 0 0         if (@{$state->{'lsf_jobids'}} < $state->{'max_jobs'}) {
  0            
668 0           $proto->create_new_worker();
669 0           $did_create_new_worker = 1;
670             }
671              
672 0           sleep(1);
673              
674 0 0         my $count = $proto->process_events($did_create_new_worker ? 10 : 0);
675 0 0 0       if (! $did_create_new_worker and ! $count) {
676 0 0         unless ($proto->_verify_lsf_jobs_are_still_alive()) {
677 0           print "\n*** The LSF worker jobs are having trouble starting up... Exiting\n";
678 0           kill 'INT', $$;
679 0           sleep 2;
680 0           kill 'INT', $$;
681             }
682             }
683             }
684              
685 0           my $worker = shift @{$state->{'idle_jobs'}};
  0            
686 0           return $worker;
687             }
688              
689             sub _verify_lsf_jobs_are_still_alive {
690 0     0     my $alive = 0;
691 0           foreach my $jobid ( @{$state->{'lsf_jobids'}} ) {
  0            
692 0           my @output = `bjobs $jobid`;
693 0 0         next unless $output[1]; # expired jobs only have 1 line of output: Job is not found
694 0           my @stat = split(/\s+/, $output[1]);
695 0 0 0       $alive++ if ($stat[2] eq 'RUN' or $stat[2] eq 'PEND');
696             }
697 0           return $alive;
698             }
699            
700              
701             #sub worker_is_now_idle {
702             # my($proto, $worker) = @_;
703             #
704             # for (my $i = 0; $i < @{$state->{'running_jobs'}}; $i++) {
705             # if ($state->{'running_jobs'}->[$i] eq $worker) {
706             # splice(@{$state->{'running_jobs'}}, $i, 1);
707             # last;
708             # }
709             # }
710             #
711             # push @{$state->{'idle_workers'}}, $worker;
712             #}
713              
714             sub create_new_worker {
715 0     0     my $proto = shift;
716              
717 0           my $port = $state->{'listen'}->sockport;
718              
719 0           my $host = $state->{'listen'}->sockhost;
720 0 0         if ($host eq '0.0.0.0') {
721 0           $host = $ENV{'HOST'};
722             }
723 0           $host .= ":$port";
724            
725 0   0       my $lsf_params = $state->{'lsf_params'} || '';
726 0           my $line = `bsub $lsf_params ur test run --run-as-lsf-helper $host`;
727 0           my ($jobid) = $line =~ m/Job \<(\d+)\>/;
728 0 0         unless ($jobid) {
729 0           Carp::croak("Couldn't parse jobid out of the line: $line");
730             }
731 0           push @{$state->{'lsf_jobids'}}, $jobid;
  0            
732             }
733              
734             sub process_events {
735 0     0     my $proto = shift;
736 0   0       my $timeout = shift || 0;
737              
738 0           my $listen = $state->{'listen'};
739 0 0         unless ($listen) {
740 0           $listen = $state->{'listen'} = IO::Socket::INET->new(Listen => 5,
741             Proto => 'tcp');
742 0 0         unless ($listen) {
743 0           Carp::croak("Unable to create listen socket: $!");
744             }
745             }
746              
747 0           my $select = $state->{'select'};
748 0 0         unless ($select) {
749 0           $select = $state->{'select'} = IO::Select->new($listen);
750             }
751              
752 0           my $processed_events = 0;
753 0           while(1) {
754 0           my @ready = $select->can_read($timeout);
755 0 0         last unless (@ready);
756              
757 0           foreach my $handle ( @ready ) {
758 0           $processed_events++;
759 0 0         if ($handle eq $listen) {
760 0           my $socket = $listen->accept();
761 0 0         unless ($socket) {
762 0           Carp::croak("accept: $!");
763             }
764 0           $socket->autoflush(1);
765 0           push @{$state->{'idle_jobs'}}, $socket;
  0            
766            
767             } else {
768             # shoulnd't get here...
769             }
770 0           $timeout = 0; # just do a poll() next time around
771             }
772             }
773 0           return $processed_events;
774             }
775              
776              
777             package My::TAP::Parser::Timer;
778              
779 1     1   2136 use base 'TAP::Parser';
  1         1  
  1         11  
780              
781             our $timelog_dir;
782             our $test_list;
783              
784             sub set_timer_info {
785 0     0     my($class,$time_dir,$testlist) = @_;
786              
787 0           $timelog_dir = $time_dir;
788 0           $test_list = $testlist;
789             }
790              
791            
792             sub make_iterator {
793 0     0     my $self = shift;
794              
795 0           my $args = $_[0];
796 0 0         if (ref($args) eq 'HASH') {
797             # It's about to make a process iterator. Prepend the stuff to
798             # run the timer, too
799              
800 0 0         unless (-d $timelog_dir) {
801 0           File::Path::mkpath("$timelog_dir");
802             }
803              
804 0           my $timelog_file = $self->_timelog_file_for_command_list($args->{'command'});
805              
806 0           my $format = q('%C %e %U %S %I %K %P'); # yes, that's single quotes inside q()
807 0           unshift @{$args->{'command'}},
  0            
808             '/usr/bin/time', '-o', $timelog_file, '-a', '-f', $format;
809             }
810            
811 0           $self->SUPER::make_iterator(@_);
812             }
813              
814             sub _timelog_file_for_command_list {
815 0     0     my($self,$command_list) = @_;
816              
817 0           foreach my $test_file ( @$test_list ) {
818 0           foreach my $cmd_part ( reverse @$command_list ) {
819 0 0         if ($test_file eq $cmd_part) {
820 0           my $log_file = Path::Class::file($cmd_part)->basename;
821 0           $log_file =~ s/\.t$//;
822 0           $log_file .= sprintf('.%d.%d.time', time(), $$); # Try to make the name unique
823 0           $log_file = $timelog_dir->file($log_file);
824 0           $log_file->openw->close();
825              
826 0           return $log_file;
827             }
828             }
829             }
830 0           Carp::croak("Can't determine time log file for command line: ",join(' ',@$command_list));
831             }
832              
833             package My::TAP::Parser::Scheduler;
834              
835 1     1   3374 use base 'TAP::Parser::Scheduler';
  1         1  
  1         12  
836              
837             sub get_job {
838 0     0     my $self = shift;
839              
840 0 0         if ($My::TAP::Parser::Iterator::Process::LSF::SHOULD_EXIT) {
841 0           our $already_printed;
842              
843 0 0         unless ($already_printed) {
844 0           print "\n\n ",$self->{'count'}," Tests not yet run before interrupt\n";
845 0           print "------------------------------------------\n";
846 0           foreach my $job ( $self->get_all ) {
847 0           print $job->{'description'},"\n";
848             }
849 0           print "------------------------------------------\n";
850 0           $already_printed = 1;
851             }
852 0           return;
853             }
854              
855 0           $self->SUPER::get_job(@_);
856             }
857              
858              
859             package My::TAP::Parser::Iterator::Process::LSF;
860              
861             our $SHOULD_EXIT = 0;
862              
863 1     1   2939 use base 'TAP::Parser::Iterator::Process';
  1         2  
  1         11  
864              
865             sub _initialize {
866 0     0     my($self, $args) = @_;
867              
868 0 0         my @command = @{ delete $args->{command} || [] }
  0 0          
869             or die "Must supply a command to execute";
870              
871             # From TAP::Parser::Iterator::Process
872 0   0       my $chunk_size = delete $args->{_chunk_size} || 65536;
873              
874 0 0         if ( my $setup = delete $args->{setup} ) {
875 0           $setup->(@command);
876             }
877              
878 0           my $handle = My::TAP::Parser::IteratorFactory::LSF->next_idle_worker();
879             # Tell the worker to run the command
880 0 0         unless($handle->print(join(' ', @command) . "\n")) {
881 0           print "Couldn't send command to worker on host ".$handle->peeraddr." port ".$handle->peerport.": $!\n";
882 0 0         print "Handle is " . ( $handle->connected ? '' : '_not_' ) . " connected\n";
883             }
884              
885 0           $self->{'out'} = $handle;
886 0           $self->{'err'} = '';
887 0           $self->{'sel'} = undef; #IO::Select->new($handle);
888 0           $self->{'pid'} = undef;
889 0           $self->{'chunk_size'} = $chunk_size;
890            
891 0 0         if ( my $teardown = delete $args->{teardown} ) {
892             $self->{teardown} = sub {
893 0     0     $teardown->(@command);
894 0           };
895             }
896              
897 0           return $self;
898             }
899              
900             sub next_raw {
901 0     0     my $self = shift;
902              
903 0           My::TAP::Parser::IteratorFactory::LSF->process_events();
904              
905 0 0         if ($SHOULD_EXIT) {
906             #$DB::single = 1;
907 0 0         if ($self->{'sel'}) {
908 0           foreach my $h ( $self->{'sel'}->handles ) {
909 0           $h->close;
910 0           $self->{'sel'}->remove($h);
911             }
912 0           return "1..0 # Skipped: Interrupted by user";
913             } else {
914 0           return;
915             }
916             }
917 0           $self->SUPER::next_raw(@_);
918             }
919              
920             #sub _finish {
921             # my $self = shift;
922             #
923             # $self->SUPER::_finish(@_);
924             #
925             # My::TAP::Parser::IteratorFactory::LSF->worker_is_now_idle($handle);
926             #}
927              
928            
929              
930             1;
931              
932             =pod
933              
934             =head1 NAME
935              
936             ur test run - run one or more test scripts
937              
938             =head1 SYNOPSIS
939              
940             # run everything in a given namespace
941             cd my_sandbox/TheNamespace
942             ur test run --recurse
943              
944             # run only selected tests
945             cd my_sandbox/TheNamespace
946             ur test run My/Module.t Another/Module.t t/foo.t t/bar.t
947              
948             # run only tests which load the TheNamespace::DNA module
949             cd my_sandbox/TheNamespace
950             ur test run --cover TheNamespace/DNA.pm
951              
952             # run only tests which cover the changes you have in Subversion
953             cd my_sandbox/TheNamespace
954             ur test run --cover-svn-changes
955              
956             # run 5 tests in parallel as jobs scheduled via LSF
957             cd my_sandbox/TheNamespace
958             ur test run --lsf --jobs 5
959              
960             =head1 DESCRIPTION
961              
962             Runs a test harness around automated test cases, like "make test" in a
963             make-oriented software distrbution, and similar to "prove" run in bulk.
964              
965             When run w/o parameters, it looks for "t" directory in the current working
966             directory, and runs ALL tests under that directory.
967              
968             =head1 OPTIONS
969              
970             =over 4
971              
972             =item --recurse
973              
974             Run all tests in the current directory, and in sub-directories. Without
975             --recurse, it will first recursively search for directories named 't' under
976             the current directory, and then recursively seatch for *.t files under those
977             directories.
978              
979             =item --long
980              
981             Include "long" tests, which are otherwise skipped in test harness execution
982              
983             =item -v
984              
985             Be verbose, meaning that individual cases will appear instead of just a full-script summary
986              
987             =item --cover My/Module.pm
988              
989             Looks in a special sqlite database which is updated by the cron which runs tests,
990             to find all tests which load My/Module.pm at some point before they exit. Only
991             these tests will be run.
992              
993             * you will still need the --long flag to run long tests.
994              
995             * if you specify tests on the command-line, only tests in both lists will run
996              
997             * this can be specified multiple times
998              
999             =item --cover-TOOL-changes
1000              
1001             TOOL can be svn, svk, or cvs.
1002              
1003             The script will run either "svn status", "svk status", or "cvs -q up" on a parent
1004             directory with "GSC" in it, and get all of the changes in your perl_modules trunk.
1005             It will behave as though those modules were listed as individual --cover options.
1006              
1007             =item --lsf
1008              
1009             Tests should not be run locally, instead they are submitted as jobs to the
1010             LSF cluster with bsub.
1011              
1012             =item --lsf-params
1013              
1014             Parameters given to bsub when sceduling jobs. The default is
1015             "-q short -R select[type==LINUX64]"
1016              
1017             =item --jobs
1018              
1019             This many tests should be run in parallel. If --lsf is also specified, then
1020             these parallel tests will be submitted as LSF jobs.
1021              
1022             =back
1023              
1024             =head1 PENDING FEATURES
1025              
1026             =over 4
1027              
1028             =item automatic remote execution for tests requiring a distinct hardware platform
1029              
1030             =item logging profiling and coverage metrics with each test
1031              
1032             =back
1033              
1034             =cut
1035              
1036