Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/Test2/Harness/Scheduler.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
49 changes: 49 additions & 0 deletions t/integration/bailout.t
Original file line number Diff line number Diff line change
@@ -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;
11 changes: 11 additions & 0 deletions t/integration/bailout/bail.tx
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 7 additions & 0 deletions t/integration/bailout/pass.tx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use Test2::Tools::Tiny;
use strict;
use warnings;

ok(1, "this test always passes");

done_testing;
139 changes: 138 additions & 1 deletion t/unit/Test2/Harness/Scheduler.t
Original file line number Diff line number Diff line change
@@ -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;
Loading