Developer-Dashboard
view release on metacpan or search on metacpan
t/103-collectorrunner-coverage.t view on Meta::CPAN
# environ, which reads back as a defined empty string and drives the
# empty-environ return path.
#
# This used to delegate to `env -i sleep 30` and poll /proc/<pid>/cmdline for
# /sleep/, but the intermediate env process already matches that pattern in its
# own cmdline, so the poll returned on its first iteration - before env had
# exec'd sleep and installed the empty environment. Measured 10 runs out of 10,
# the environ at that moment was still the inherited one (1564 and 1820 bytes in
# two samples), so whether the routine below saw an empty environ at all came
# down to env finishing its exec inside the gap between the poll exiting and the
# call being made. Nothing pinned that: is($marker, undef) is satisfied just as
# well by a populated environ that lacks the key, so the empty-environ leg could
# stop being exercised without a single assertion failing. Clearing the
# environment in this very fork removes the intermediate process entirely, and
# the environ assertion below turns the precondition into a real one.
SKIP: {
my ($sleep_bin) = grep { -x } qw(/bin/sleep /usr/bin/sleep);
skip 'no sleep binary available to hold an empty environment open', 2 if !defined $sleep_bin;
my $probe_title = 'dd-empty-environ-probe';
my $child = fork();
die "fork failed: $!" if !defined $child;
if ( !$child ) {
%ENV = ();
exec { $sleep_bin } $probe_title, '30' or CORE::exit(127);
}
# Poll on the exec'd argv[0], which only the post-exec process can show.
#
# The bound is 30 seconds, and it is measured rather than chosen (DD-482).
# The previous bound was 500 polls -- 5 seconds -- and the coverage gate
# failed on it while the plain suite passed. Instrumenting the loop showed
# the bound expiring with the child still ALIVE in state R, having not yet
# reached its exec: a Devel::Cover-instrumented child flushes its coverage
# database before exec'ing, which took 678 polls (~6.8s) against 2 polls
# plain. So the old bound sat just under what the gated run needs.
#
# Widening a poll loop is normally a smell, and in this repo it has been an
# actual defect (a loop once widened past a sleep that was silently zero).
# The difference is that the mechanism here was demonstrated before the
# number was changed, and the timeout below now reports what it saw so a
# future failure is diagnosable instead of a bare undef.
my $probe_environ;
my $probe_polls = 0;
for ( 1 .. 3000 ) {
$probe_polls = $_;
my $cmdline = '';
if ( open my $cf, '<', "/proc/$child/cmdline" ) { local $/; $cmdline = <$cf>; close $cf; }
if ( defined $cmdline && index( $cmdline, $probe_title ) == 0 ) {
if ( open my $ef, '<', "/proc/$child/environ" ) { local $/; $probe_environ = <$ef>; close $ef; }
diag("probe child did not expose a readable environ after $probe_polls polls: $!")
if !defined $probe_environ;
last;
}
select undef, undef, undef, 0.01;
}
# A bare "got undef" says nothing about which of the two very different
# failures happened: the child never reached its exec, or its environ could
# not be read. Note that undef never means "the environ was empty" -- a
# genuinely empty /proc/<pid>/environ reads back as '' with length 0.
if ( !defined $probe_environ ) {
my $alive = -e "/proc/$child" ? 'alive' : 'gone';
my $state = eval {
open my $st, '<', "/proc/$child/stat" or die;
my $line = <$st>;
close $st;
( split / /, $line )[2];
} // '?';
diag("probe gave up after $probe_polls polls; child is $alive in state $state and never exposed its exec'd argv[0]");
}
# Pin the precondition the coverage below depends on. Without this the
# marker assertion is satisfied by any environ that lacks the key, so the
# empty-environ leg can stop being exercised without a single test failing.
is( $probe_environ, '', 'the probe child exposes a readable zero-length environ' );
is( $runner->_read_process_env_marker( $child, 'ANY' ), undef, '_read_process_env_marker returns undef for an empty environ' );
kill 9, $child;
waitpid( $child, 0 );
}
# _read_process_title: proc cmdline path, then the ps fallback under a mocked
# capture so every exit-code and defined-title branch is exercised.
{
my $title = $runner->_read_process_title($$);
ok( defined $title && $title ne '', '_read_process_title reads the current process cmdline' );
}
{
no warnings 'redefine';
local *Developer::Dashboard::CollectorRunner::_read_proc_file = sub { return undef };
{
local *Developer::Dashboard::CollectorRunner::capture = sub { return ( 'ps-title ', '', 0 ) };
is( $runner->_read_process_title($$), 'ps-title', '_read_process_title trims a successful ps fallback title' );
}
{
local *Developer::Dashboard::CollectorRunner::capture = sub { return ( '', '', 1 ) };
is( $runner->_read_process_title($$), undef, '_read_process_title returns undef when the ps fallback exits non-zero' );
}
{
local *Developer::Dashboard::CollectorRunner::capture = sub { return ( undef, '', undef ) };
is( $runner->_read_process_title($$), undef, '_read_process_title returns undef when the ps fallback yields no title' );
}
}
# _read_process_state: procfs parse variants plus the ps fallback.
{
no warnings 'redefine';
{
local *Developer::Dashboard::CollectorRunner::_read_proc_file = sub { return '4242 (cmd) R 1 4242 4242' };
is( $runner->_read_process_state($$), 'R', '_read_process_state parses a running state from procfs stat' );
}
{
local *Developer::Dashboard::CollectorRunner::_read_proc_file = sub { return 'garbage-without-format' };
local *Developer::Dashboard::CollectorRunner::capture = sub { return ( ' Ss ', '', 0 ) };
is( $runner->_read_process_state($$), 'S', '_read_process_state falls back to ps when procfs stat is unparseable' );
}
{
local *Developer::Dashboard::CollectorRunner::_read_proc_file = sub { return '' };
local *Developer::Dashboard::CollectorRunner::capture = sub { return ( 'R', '', 0 ) };
is( $runner->_read_process_state($$), 'R', '_read_process_state falls back to ps when procfs stat is empty' );
}
{
local *Developer::Dashboard::CollectorRunner::_read_proc_file = sub { return undef };
local *Developer::Dashboard::CollectorRunner::capture = sub { return ( '', '', 1 ) };
is( $runner->_read_process_state($$), undef, '_read_process_state returns undef when the ps fallback exits non-zero' );
}
{
local *Developer::Dashboard::CollectorRunner::_read_proc_file = sub { return undef };
local *Developer::Dashboard::CollectorRunner::capture = sub { return ( undef, '', undef ) };
is( $runner->_read_process_state($$), undef, '_read_process_state returns undef when the ps fallback yields no state' );
}
t/103-collectorrunner-coverage.t view on Meta::CPAN
POSIX::_exit(0);
}
my $pidfile = $runner->_pidfile($name);
open my $fh, '>', $pidfile or die $!;
print {$fh} $child;
close $fh;
$runner->_write_loop_state( $name, { pid => $child, name => $name, process_name => $runner->_process_title($name), status => 'running' } );
{
no warnings 'redefine';
local *Developer::Dashboard::CollectorRunner::_read_process_env_marker = sub { return $name };
is( $runner->stop_loop($name), $child, 'stop_loop returns the pid of a managed loop it terminates' );
}
select undef, undef, undef, 0.2;
ok( !kill( 0, $child ), 'stop_loop terminates the managed loop process' );
waitpid( $child, 0 ) if kill 0, $child;
}
# Live managed loop under the Windows branch: no process-group kill.
{
my $name = 'stop.managed.windows';
my $child = fork();
die "fork failed: $!" if !defined $child;
if ( !$child ) {
$0 = $runner->_process_title($name);
$ENV{DEVELOPER_DASHBOARD_LOOP_NAME} = $name;
$SIG{TERM} = 'DEFAULT';
select undef, undef, undef, 30;
POSIX::_exit(0);
}
my $pidfile = $runner->_pidfile($name);
open my $fh, '>', $pidfile or die $!;
print {$fh} $child;
close $fh;
$runner->_write_loop_state( $name, { pid => $child, name => $name, process_name => $runner->_process_title($name), status => 'running' } );
{
no warnings 'redefine';
local *Developer::Dashboard::CollectorRunner::is_windows = sub { return 1 };
local *Developer::Dashboard::CollectorRunner::_read_process_env_marker = sub { return $name };
is( $runner->stop_loop($name), $child, 'stop_loop returns the managed loop pid on the Windows branch' );
}
select undef, undef, undef, 0.2;
kill 9, $child;
waitpid( $child, 0 ) if kill 0, $child;
}
# Already-reaped loop child: the reap short-circuits the kill path.
{
my $name = 'stop.reaped';
my $child = fork();
die "fork failed: $!" if !defined $child;
if ( !$child ) { POSIX::_exit(0); }
my $pidfile = $runner->_pidfile($name);
open my $fh, '>', $pidfile or die $!;
print {$fh} $child;
close $fh;
$runner->_write_loop_state( $name, { pid => $child, name => $name, process_name => $runner->_process_title($name), status => 'running' } );
select undef, undef, undef, 0.1;
is( $runner->stop_loop($name), $child, 'stop_loop returns the pid of an already-exited loop it reaps' );
}
# Foreign-namespace loop: pid is alive but reported in another namespace.
{
my $name = 'stop.foreign';
my $child = fork();
die "fork failed: $!" if !defined $child;
if ( !$child ) { $SIG{TERM} = 'DEFAULT'; select undef, undef, undef, 30; POSIX::_exit(0); }
my $pidfile = $runner->_pidfile($name);
open my $fh, '>', $pidfile or die $!;
print {$fh} $child;
close $fh;
{
no warnings 'redefine';
local *Developer::Dashboard::CollectorRunner::_same_pid_namespace = sub { return 0 };
is( $runner->stop_loop($name), $child, 'stop_loop returns a foreign-namespace pid without signalling it' );
}
ok( kill( 0, $child ), 'stop_loop leaves a foreign-namespace loop running' );
kill 9, $child;
waitpid( $child, 0 ) if kill 0, $child;
}
# Unrecognized live loop with recorded worker pids: else-branch worker sweep.
{
my $name = 'stop.unrecognized';
my $child = fork();
die "fork failed: $!" if !defined $child;
if ( !$child ) { $SIG{TERM} = 'DEFAULT'; select undef, undef, undef, 30; POSIX::_exit(0); }
my $pidfile = $runner->_pidfile($name);
open my $fh, '>', $pidfile or die $!;
print {$fh} $child;
close $fh;
$runner->_write_loop_state( $name, { pid => $child, name => $name, process_name => 'unrelated-title', status => 'running', active_worker_pids => [ 2000000001 ] } );
{
no warnings 'redefine';
local *Developer::Dashboard::CollectorRunner::_read_process_env_marker = sub { return undef };
local *Developer::Dashboard::CollectorRunner::_read_process_title = sub { return 'unrelated-title' };
local *Developer::Dashboard::CollectorRunner::_state_confirms_managed_loop = sub { return 0 };
local *Developer::Dashboard::CollectorRunner::_terminate_loop_workers = sub { return 1 };
is( $runner->stop_loop($name), $child, 'stop_loop sweeps recorded workers for an unrecognized live loop' );
}
kill 9, $child;
waitpid( $child, 0 ) if kill 0, $child;
}
# ===========================================================================
# running_loops: empty pidfile, reaped child, managed loop, foreign loop, and
# an unrecognized (swept) loop.
# ===========================================================================
{
# Use a fresh runtime so the collectors root holds only these pidfiles and
# every readdir branch/condition outcome is exercised deterministically.
my $rl_home = tempdir( CLEANUP => 1 );
my $rl_paths = Developer::Dashboard::PathRegistry->new( home => $rl_home, workspace_roots => [ File::Spec->catdir( $rl_home, 'workspace' ) ] );
my $rl_runner = Developer::Dashboard::CollectorRunner->new(
collectors => Developer::Dashboard::Collector->new( paths => $rl_paths ),
files => Developer::Dashboard::FileRegistry->new( paths => $rl_paths ),
indicators => Developer::Dashboard::IndicatorStore->new( paths => $rl_paths ),
paths => $rl_paths,
);
my $root = $rl_paths->collectors_root;
# Empty pidfile -> skipped on the falsy-pid guard (slurps to undef).
( run in 1.063 second using v1.01-cache-2.11-cpan-14f38c9f855 )