Developer-Dashboard
view release on metacpan or search on metacpan
t/144-collector-fixture-recognition-race.t view on Meta::CPAN
return $pid if $pid;
Time::HiRes::sleep($title_delay) if $title_delay;
$0 = $title if defined $title;
Time::HiRes::sleep(30);
POSIX::_exit(0);
}
# write_bare_pidfile($name, $pid)
# Records a collector pid the way the lifecycle fixtures do: a pidfile and
# nothing else, so no loop state exists to confirm the loop's identity.
# Input: collector name string and pid integer.
# Output: pidfile path string.
sub write_bare_pidfile {
my ( $name, $pid ) = @_;
my $pidfile = $runner->_pidfile($name);
open my $fh, '>', $pidfile or die "Unable to write $pidfile: $!";
print {$fh} "$pid\n";
close $fh or die "Unable to close $pidfile: $!";
return $pidfile;
}
# reap_fixture_child($pid)
# Removes a fixture child unconditionally so no stray collector stand-in
# survives the test file.
# Input: child pid integer.
# Output: true value once the child is gone.
sub reap_fixture_child {
my ($pid) = @_;
kill 9, $pid;
waitpid( $pid, 0 );
return 1;
}
# A hand-written pidfile carries no loop state, so the only thing that marks the
# child as a managed loop is the process title it adopts after the fork. Until
# then the runner correctly refuses to claim it - and that refusal, not a slow
# shutdown, is what a CPU-starved host turns into a test failure.
my $untitled = fork_fixture_child( 0, undef );
write_bare_pidfile( 'fixture.untitled', $untitled );
ok(
!$runner->_is_managed_loop( $untitled, 'fixture.untitled' ),
'a bare pidfile whose child has not adopted the managed process title is not recognized as a managed loop',
);
is(
wait_for_managed_loop( $runner, $untitled, 'fixture.untitled', timeout => 0.2 ),
0,
'wait_for_managed_loop gives up on its wall-clock budget instead of blocking for ever',
);
# The exact CI reading this file exists to explain. stop_loop takes its
# unmanaged branch, never signals the child, and returns the recorded pid, so
# waitpid reports 0 - "the child is still there" - where the fixture expects -1.
is( $runner->stop_loop('fixture.untitled'), $untitled, 'stop_loop returns the recorded pid even for a loop it does not recognize' );
is( waitpid( $untitled, WNOHANG ), 0, 'an unrecognized loop child is left running and unreaped, which is the "got 0, expected -1" failure' );
ok( kill( 0, $untitled ), 'the unrecognized loop child really is still alive rather than exited-but-unreaped' );
reap_fixture_child($untitled);
# running_loops is worse than a missed probe: it deletes the pidfile of any
# same-namespace pid it cannot recognize. A fixture that polls running_loops
# before its child is recognizable therefore destroys its own fixture on the
# first iteration, and no number of further iterations can recover it.
my $swept = fork_fixture_child( 0, undef );
my $swept_pidfile = write_bare_pidfile( 'fixture.swept', $swept );
my @swept_rows = $runner->running_loops;
is( scalar( grep { $_->{name} eq 'fixture.swept' } @swept_rows ), 0, 'running_loops does not list a loop whose child has not adopted the managed title' );
ok( !-e $swept_pidfile, 'running_loops deletes the unrecognized pidfile, so a later poll iteration can never see the loop' );
reap_fixture_child($swept);
# Waiting on the runner's own predicate first makes both behaviours
# deterministic without weakening either assertion: the title path is still the
# one being exercised, and the shutdown still has to reap the child.
my $titled = fork_fixture_child( 0, $runner->_process_title('fixture.titled') );
write_bare_pidfile( 'fixture.titled', $titled );
ok( wait_for_managed_loop( $runner, $titled, 'fixture.titled' ), 'wait_for_managed_loop reports success once the runner recognizes the child' );
is_deeply(
[ map { $_->{name} } grep { $_->{name} eq 'fixture.titled' } $runner->running_loops ],
['fixture.titled'],
'running_loops lists the loop once recognition has actually happened',
);
is( $runner->stop_loop('fixture.titled'), $titled, 'stop_loop terminates a recognized managed loop' );
is( waitpid( $titled, WNOHANG ), -1, 'stop_loop reaps a recognized managed loop child' );
# The helper has to wait rather than probe once, which is what makes it a fix
# for a starved host instead of a restatement of the race. The child cannot be
# recognized before it adopts its title, so the elapsed time proves the wait.
my $late_delay = 3;
my $late = fork_fixture_child( $late_delay, $runner->_process_title('fixture.late') );
write_bare_pidfile( 'fixture.late', $late );
my $started = Time::HiRes::time();
ok( wait_for_managed_loop( $runner, $late, 'fixture.late' ), 'wait_for_managed_loop recognizes a child that adopts its title late' );
cmp_ok(
Time::HiRes::time() - $started,
'>=',
$late_delay - 0.5,
'the wait elapsed alongside the late title instead of succeeding on its first probe',
);
reap_fixture_child($late);
# Structural guard, and it is only structural: no behavioural test can inspect
# the ordering inside another suite file. Each fixture below hand-writes a
# collector pidfile and depends on title recognition, so each must wait before
# it probes. Every anchor is a method CALL rather than a bare method name,
# because a name also matches the prose in a comment explaining the rule - which
# is precisely what the first draft of this guard measured.
my @guards = (
{
file => File::Spec->catfile( $repo_root, 't', '07-core-units.t' ),
label => q{t/07's manual collector shutdown fixture},
between => qr/'manual\.pid'(.*?)\$runner->stop_loop\('manual'\)/s,
},
{
file => File::Spec->catfile( $repo_root, 't', '14-coverage-closure-extra.t' ),
label => q{t/14's managed-loop listing fixture},
between => qr/my \$loop_name = 'coverage\.loop';(.*?)\$runner->running_loops/s,
},
{
file => File::Spec->catfile( $repo_root, 't', '14-coverage-closure-extra.t' ),
label => q{t/14's loop-name sorting fixture},
between => qr/coverage\.sort-b coverage\.sort-a\)(.*?)\$runner->running_loops/s,
},
);
( run in 1.330 second using v1.01-cache-2.11-cpan-364913b4093 )