@@ -31,8 +31,8 @@ sub with_color {
3131 }
3232}
3333
34- sub run ($$$$$) {
35- my ($name , $input , $cmd , $options , $output ) = @_ ;
34+ sub run ($$$$$$ ) {
35+ my ($name , $input , $cmd , $options , $output , $timeout ) = @_ ;
3636 my $cmdline ;
3737 if (length ($input )) {
3838 $cmdline = " $cmd $options '$input ' >'$output ' 2>&1" ;
@@ -41,13 +41,58 @@ ($$$$$)
4141 }
4242
4343 print LOG " Running $cmdline \n " ;
44- # see https://github.com/git-for-windows/msys2-runtime/pull/11/files
45- system (" bash" , " -c" , " cd '$name ' ; MSYS_NO_PATHCONV=1 $cmdline " );
46- my $exit_value = $? >> 8;
47- my $signal_num = $? & 127;
48- my $dumped_core = $? & 128;
44+ my $exit_value ;
45+ my $signal_num ;
46+ my $dumped_core ;
47+ my $timed_out = 0;
48+
49+ # Fork so we can enforce a timeout via kill on the child process group.
50+ my $pid = fork ();
51+ die " fork failed: $! " unless defined $pid ;
52+
53+ if ($pid == 0) {
54+ # Child: start a new process group so we can kill the whole group on
55+ # timeout, then exec the test command.
56+ setpgrp (0, 0);
57+ # see https://github.com/git-for-windows/msys2-runtime/pull/11/files
58+ exec (" bash" , " -c" , " cd '$name ' ; MSYS_NO_PATHCONV=1 $cmdline " );
59+ die " exec failed: $! " ;
60+ }
61+
62+ # Parent: wait for the child, with an optional timeout.
63+ if ($timeout ) {
64+ eval {
65+ local $SIG {ALRM } = sub { die " alarm\n " };
66+ alarm($timeout );
67+ waitpid ($pid , 0);
68+ alarm(0);
69+ };
70+ if ($@ && $@ eq " alarm\n " ) {
71+ $timed_out = 1;
72+ # Kill the entire process group of the child.
73+ kill (9, -$pid );
74+ waitpid ($pid , 0);
75+ } elsif ($@ ) {
76+ die $@ ;
77+ }
78+ } else {
79+ waitpid ($pid , 0);
80+ }
81+
82+ $exit_value = $? >> 8;
83+ $signal_num = $? & 127;
84+ $dumped_core = $? & 128;
4985 my $failed = 0;
5086
87+ if ($timed_out ) {
88+ print LOG " Timed out after $timeout seconds\n " ;
89+ print " [TIMEOUT]" ;
90+ $failed = 1;
91+ # Write output so that pattern matching does not crash on missing file.
92+ system (" bash" , " -c" , " cd '$name ' ; echo '\n TIMEOUT=1\n EXIT=137\n SIGNAL=9\n ' >> '$output '" );
93+ return $failed ;
94+ }
95+
5196 print LOG " Exit: $exit_value \n " ;
5297 print LOG " Signal: $signal_num \n " ;
5398 print LOG " Core: $dumped_core \n " ;
84129 return @data ;
85130}
86131
87- sub test ($$$$$$$$$$$) {
88- my ($name , $test , $t_level , $cmd , $ign , $dry_run , $defines , $include_tags , $exclude_tags , $output_suffix , $exit_signal_checks ) = @_ ;
132+ sub test ($$$$$$$$$$$$ ) {
133+ my ($name , $test , $t_level , $cmd , $ign , $dry_run , $defines , $include_tags , $exclude_tags , $output_suffix , $exit_signal_checks , $timeout ) = @_ ;
89134 my ($level_and_tags , $input , $options , $grep_options , @results ) = load(" $test " , $exit_signal_checks );
90135 my @keys = keys %{$defines };
91136 foreach my $key (@keys ) {
@@ -179,7 +224,7 @@ ($$$$$$$$$$$)
179224 return 0;
180225 }
181226
182- $failed = run($name , $input , $cmd , $options , $output );
227+ $failed = run($name , $input , $cmd , $options , $output , $timeout );
183228
184229 if (!$failed ) {
185230 print LOG " Execution [OK]\n " ;
@@ -302,6 +347,8 @@ ($$$$)
302347 as runs with different suffixes will operate independently and keep
303348 independent logs.
304349 -f forward the test name to CMD
350+ -t <secs> timeout per test in seconds (kills test if exceeded)
351+ default is \$ TESTPL_TIMEOUT if set; overridden by -t <secs>
305352
306353 --[no]color enable/disable color output; enabled by default unless
307354 TESTPL_COLOR_OUTPUT is set to 0, in which case it is
@@ -341,7 +388,7 @@ ($$$$)
341388use Getopt::Long qw( :config pass_through bundling) ;
342389$main::VERSION = 0.1;
343390$Getopt::Std::STANDARD_HELP_VERSION = 1;
344- our ($opt_c , $opt_e , $opt_f , $opt_i , $opt_j , $opt_n , $opt_p , $opt_h , $opt_C , $opt_T , $opt_F , $opt_K , $opt_s , $opt_S , %defines , @include_tags , @exclude_tags ); # the variables for getopt
391+ our ($opt_c , $opt_e , $opt_f , $opt_i , $opt_j , $opt_n , $opt_p , $opt_h , $opt_C , $opt_T , $opt_F , $opt_K , $opt_s , $opt_S , $opt_t , %defines , @include_tags , @exclude_tags ); # the variables for getopt
345392
346393# this needs to come before GetOptions to ensure the
347394# default -> environment -> flag override priority
@@ -351,7 +398,7 @@ ($$$$)
351398}
352399
353400GetOptions(" D=s" => \%defines , " X=s" => \@exclude_tags , " I=s" => \@include_tags , ' color!' => \$color_output_enabled );
354- getopts(' c:efi:j:nphCTFKs:S:' ) or &main::HELP_MESSAGE(\*STDOUT , " " , $main::VERSION , " " );
401+ getopts(' c:efi:j:nphCTFKs:S:t: ' ) or &main::HELP_MESSAGE(\*STDOUT , " " , $main::VERSION , " " );
355402$opt_c or &main::HELP_MESSAGE(\*STDOUT , " " , $main::VERSION , " " );
356403$opt_j = $opt_j || $ENV {' TESTPL_JOBS' } || 0;
357404if ($opt_j && $opt_j != 1 && !$has_thread_pool ) {
@@ -369,6 +416,7 @@ ($$$$)
369416my $dry_run = $opt_n ;
370417my $log_suffix = $opt_s ;
371418my $exit_signal_checks = defined ($opt_e );
419+ my $timeout = $opt_t || $ENV {' TESTPL_TIMEOUT' } || 0;
372420
373421my $logfile_name = " tests" ;
374422if ($log_suffix ) {
403451 defined ($pool ) or print " Running $files [$_ ]" ;
404452 my $start_time = time ();
405453 $failed_skipped = test(
406- $test , $files [$_ ], $t_level , $opt_c , $opt_i , $dry_run , \%defines , \@include_tags , \@exclude_tags , $log_suffix , $exit_signal_checks );
454+ $test , $files [$_ ], $t_level , $opt_c , $opt_i , $dry_run , \%defines , \@include_tags , \@exclude_tags , $log_suffix , $exit_signal_checks , $timeout );
407455 my $runtime = time () - $start_time ;
408456
409457 lock($skips );
0 commit comments