diff --git a/lib/Test2/Harness/Scheduler.pm b/lib/Test2/Harness/Scheduler.pm index 1a71cf031..a852fc44e 100644 --- a/lib/Test2/Harness/Scheduler.pm +++ b/lib/Test2/Harness/Scheduler.pm @@ -307,6 +307,9 @@ sub abort { CORE::kill('TERM', $pid); $job->{killed} = 1; } + + # Also terminate the scheduler itself, as BAIL_OUT means we should exit + $self->terminate(1); } sub kill { diff --git a/t/integration/bailout.t b/t/integration/bailout.t new file mode 100644 index 000000000..a63bbb17a --- /dev/null +++ b/t/integration/bailout.t @@ -0,0 +1,49 @@ +use Test2::V0; +# HARNESS-DURATION-LONG + +use App::Yath::Tester qw/yath/; +use Test2::Plugin::Immiscible(sub { $ENV{TEST2_HARNESS_ACTIVE} ? 1 : 0 }); + +my $dir = __FILE__; +$dir =~ s{\.t$}{}g; +$dir =~ s{^\./}{}; + +# Test that BAIL_OUT causes yath to exit (not hang) +# Regression test for https://github.com/Test-More/Test2-Harness/issues/287 +yath( + command => 'test', + args => [$dir, '--ext=tx'], + exit => T(), + test => sub { + my $out = shift; + ok($out->{exit}, "yath exits with non-zero when BAIL_OUT is encountered"); + like($out->{output}, qr/BAIL_OUT|bail|halt/i, "output mentions bail/halt"); + }, +); + +# Test that BAIL_OUT is ignored when --no-abort-on-bail is set +yath( + command => 'test', + args => [$dir, '--ext=tx', '--no-abort-on-bail'], + exit => T(), + test => sub { + my $out = shift; + # The test still fails (BAIL_OUT is a failure), but yath should + # not abort the entire suite — just the bailing test fails + ok($out->{exit}, "yath exits non-zero (test still fails)"); + }, +); + +# Test that when BAIL_OUT is disabled via env, everything passes +yath( + command => 'test', + args => [$dir, '--ext=tx'], + env => {BAILOUT_DO_PASS => 1}, + exit => 0, + test => sub { + my $out = shift; + ok(!$out->{exit}, "yath exits cleanly when BAIL_OUT is not triggered"); + }, +); + +done_testing; diff --git a/t/integration/bailout/bail.tx b/t/integration/bailout/bail.tx new file mode 100644 index 000000000..beff35fd6 --- /dev/null +++ b/t/integration/bailout/bail.tx @@ -0,0 +1,11 @@ +use Test2::Tools::Tiny; +use strict; +use warnings; + +ok(1, "a passing test before bail"); + +BAIL_OUT("Something went horribly wrong") unless $ENV{BAILOUT_DO_PASS}; + +ok(1, "should not reach here"); + +done_testing; diff --git a/t/integration/bailout/pass.tx b/t/integration/bailout/pass.tx new file mode 100644 index 000000000..fbfd38371 --- /dev/null +++ b/t/integration/bailout/pass.tx @@ -0,0 +1,7 @@ +use Test2::Tools::Tiny; +use strict; +use warnings; + +ok(1, "this test always passes"); + +done_testing; diff --git a/t/unit/Test2/Harness/Scheduler.t b/t/unit/Test2/Harness/Scheduler.t index bcaf33042..4be799752 100644 --- a/t/unit/Test2/Harness/Scheduler.t +++ b/t/unit/Test2/Harness/Scheduler.t @@ -1,5 +1,142 @@ use Test2::V0 -target => 'Test2::Harness::Scheduler'; -skip_all "write me"; +use File::Temp qw/tempdir/; +use Test2::Harness::Runner; +use Test2::Harness::TestSettings; + +my $dir = tempdir(CLEANUP => 1); + +sub make_scheduler { + my %params = @_; + + my $runner = Test2::Harness::Runner->new( + workdir => $dir, + test_settings => Test2::Harness::TestSettings->new(), + ); + + return $CLASS->new(runner => $runner, %params); +} + +subtest 'abort terminates the scheduler' => sub { + my $sched = make_scheduler(); + + ok(!$sched->terminated, "scheduler is not terminated initially"); + + $sched->abort(); + + ok($sched->terminated, "scheduler is terminated after abort()"); + is($sched->terminated, 1, "terminate reason is 1"); +}; + +subtest 'abort with no running jobs still terminates' => sub { + my $sched = make_scheduler(); + + ok(!$sched->terminated, "scheduler is not terminated initially"); + is($sched->{running}{jobs}, undef, "no running jobs"); + + $sched->abort(); + + ok($sched->terminated, "scheduler terminates even with no running jobs"); +}; + +subtest 'kill delegates to abort and terminates' => sub { + my $sched = make_scheduler(); + + ok(!$sched->terminated, "scheduler is not terminated initially"); + + $sched->kill(); + + ok($sched->terminated, "scheduler is terminated after kill()"); +}; + +subtest 'terminate is idempotent' => sub { + my $sched = make_scheduler(); + + my $reason1 = $sched->terminate('first'); + is($reason1, 'first', "first terminate returns the reason"); + + my $reason2 = $sched->terminate('second'); + is($reason2, 'first', "second terminate returns original reason"); +}; + +subtest 'abort marks runs as halted' => sub { + my $sched = make_scheduler(); + + # Create a mock run object + my $halt_value; + my $mock_run = mock {} => ( + add => [ + set_halt => sub { $halt_value = $_[1] }, + run_id => sub { 'run-1' }, + ], + ); + + $sched->{runs}{'run-1'} = $mock_run; + + $sched->abort(); + + is($halt_value, 'aborted', "run was marked as halted/aborted"); + ok($sched->terminated, "scheduler terminated after abort"); +}; + +subtest 'abort kills running job PIDs' => sub { + my $sched = make_scheduler(); + + my $halt_value; + my $mock_run = mock {} => ( + add => [ + set_halt => sub { $halt_value = $_[1] }, + run_id => sub { 'run-1' }, + ], + ); + + $sched->{runs}{'run-1'} = $mock_run; + + # Use a child process so TERM doesn't kill us + my $child = fork(); + if (!defined $child) { + skip_all "fork failed: $!"; + } + elsif ($child == 0) { + sleep 10; + exit 0; + } + + $sched->{running}{jobs}{'job-1'} = { + run => $mock_run, + pid => $child, + killed => 0, + }; + + $sched->abort(); + + ok($sched->{running}{jobs}{'job-1'}{killed}, "job was marked as killed"); + ok($sched->terminated, "scheduler terminated after abort with running jobs"); + + # Clean up child + kill('KILL', $child); + waitpid($child, 0); +}; + +subtest 'abort with specific run IDs only aborts those runs' => sub { + my $sched = make_scheduler(); + + my %halt_values; + for my $id ('run-1', 'run-2') { + my $run_id = $id; + $sched->{runs}{$id} = mock {} => ( + add => [ + set_halt => sub { $halt_values{$run_id} = $_[1] }, + run_id => sub { $run_id }, + ], + ); + } + + $sched->abort('run-1'); + + is($halt_values{'run-1'}, 'aborted', "run-1 was aborted"); + ok(!exists $halt_values{'run-2'}, "run-2 was not aborted"); + ok($sched->terminated, "scheduler still terminates"); +}; done_testing;