Developer-Dashboard
view release on metacpan or search on metacpan
t/09-runtime-manager.t view on Meta::CPAN
my $payload = <$result_reader>;
close $result_reader;
waitpid( $pid, 0 );
chomp $payload if defined $payload;
is( $payload, '1:0', '_close_inherited_fds also closes inherited socketpair descriptors while preserving explicit keep handles' );
}
{
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::_open_file_descriptors = sub { return () };
ok(
$manager->_close_inherited_fds( keep => [ undef, 'bad-fd', 1 ] ),
'_close_inherited_fds accepts mixed keep values and still returns successfully when there are no inherited descriptors to close',
);
}
{
my $startup_payload = File::Spec->catfile( $home, 'startup-pipe-message.txt' );
open my $writer, '>', $startup_payload or die "Unable to write $startup_payload: $!";
ok( $manager->_write_startup_pipe_message( $writer, 'startup-ok' ), '_write_startup_pipe_message supports the explicit syswrite path for real file descriptors' );
open my $payload_fh, '<', $startup_payload or die "Unable to read $startup_payload: $!";
my $payload = do { local $/; <$payload_fh> };
close $payload_fh;
is( $payload, 'startup-ok', '_write_startup_pipe_message writes the complete payload through the explicit syswrite loop' );
}
{
my $startup_payload = File::Spec->catfile( $home, 'startup-pipe-bad-close.txt' );
open my $writer, '>', $startup_payload or die "Unable to write $startup_payload: $!";
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::_close_startup_pipe_writer = sub {
$! = 9;
return 0;
};
ok(
$manager->_write_startup_pipe_message( $writer, 'ok|bad-close' ),
'_write_startup_pipe_message tolerates a Bad file descriptor close after writing the payload',
);
open my $payload_fh, '<', $startup_payload or die "Unable to read $startup_payload: $!";
my $payload = do { local $/; <$payload_fh> };
close $payload_fh;
is( $payload, 'ok|bad-close', '_write_startup_pipe_message still writes the payload before ignoring the close failure' );
}
{
my $setsid_calls = 0;
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::is_windows = sub { return 0 };
local *Developer::Dashboard::RuntimeManager::setsid = sub { $setsid_calls++; return 1 };
is( $manager->_detach_web_process_session, 1, '_detach_web_process_session returns true on POSIX hosts after setsid succeeds' );
is( $setsid_calls, 1, '_detach_web_process_session calls setsid exactly once on POSIX hosts' );
}
{
my $state = $manager->_write_web_state();
is_deeply( $state, {}, '_write_web_state persists an empty hash when no state payload is provided' );
is_deeply( $manager->web_state, {}, 'web_state reads back the empty-state payload written by _write_web_state' );
$manager->_cleanup_web_files;
}
$files->write( 'dashboard_log', "starman line\nDancer2 line\n" );
is( $manager->web_log, "starman line\nDancer2 line\n", 'web_log reads the persisted dashboard web-service log output' );
is( $manager->_tail_text( "one\ntwo\nthree\n", 2 ), "two\nthree\n", '_tail_text keeps the requested trailing newline-terminated log lines' );
is( $manager->_tail_text( "one\ntwo\nthree", 2 ), "two\nthree", '_tail_text preserves non-terminated trailing log lines' );
is( $manager->web_log( lines => 1 ), "Dancer2 line\n", 'web_log can return only the last requested number of lines' );
$files->remove('dashboard_log');
is( $manager->web_log, '', 'web_log returns an empty string when the dashboard log file is missing' );
{
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::_follow_log_file = sub {
my ( $self, %args ) = @_;
is( $args{start_pos}, length("follow once\n"), 'web_log follow mode passes the original file byte length into the follow loop so appended lines are not skipped by a seek-to-end race' );
return 1;
};
$files->write( 'dashboard_log', "follow once\n" );
is( $manager->web_log( follow => 1, lines => 1 ), '', 'web_log follow mode returns an empty string after delegating to the follow loop' );
}
{
$files->write( 'dashboard_log', "alpha\nbeta\n" );
my $follow_capture = "$home/web-log-follow.txt";
my $follow_pid = fork();
die "fork failed: $!" if !defined $follow_pid;
if ( !$follow_pid ) {
open STDOUT, '>', $follow_capture or die $!;
$manager->web_log( follow => 1, lines => 1 );
POSIX::_exit(0);
}
my $follow_output = '';
for ( 1 .. 30 ) {
if ( -f $follow_capture ) {
open my $fh, '<', $follow_capture or die $!;
local $/;
$follow_output = <$fh>;
close $fh;
last if $follow_output =~ /beta\n/;
}
sleep 0.1;
}
like( $follow_output, qr/beta\n/, 'web_log follow mode starts from the tailed log output' );
$files->append( 'dashboard_log', "gamma\n" );
for ( 1 .. 30 ) {
open my $fh, '<', $follow_capture or die $!;
local $/;
$follow_output = <$fh>;
close $fh;
last if $follow_output =~ /gamma\n/;
sleep 0.1;
}
like( $follow_output, qr/gamma\n/, 'web_log follow mode streams appended log lines' );
kill 'TERM', $follow_pid;
waitpid( $follow_pid, 0 );
is( $? >> 8, 0, 'web_log follow mode exits cleanly on TERM' );
}
{
my $missing_follow = "$home/missing-follow.log";
my $missing_pid = fork();
die "fork failed: $!" if !defined $missing_pid;
if ( !$missing_pid ) {
$manager->_follow_log_file( file => $missing_follow, interval => 0.05 );
POSIX::_exit(0);
}
for ( 1 .. 30 ) {
t/09-runtime-manager.t view on Meta::CPAN
\@events,
[
[ 'start_collector:housekeeper', 'running', 'Start collector housekeeper' ],
[ 'start_collector:housekeeper', 'done', 'Start collector housekeeper' ],
[ 'start_collector:alpha.collector', 'running', 'Start collector alpha.collector' ],
[ 'start_collector:alpha.collector', 'done', 'Start collector alpha.collector' ],
[ 'start_collector:beta.collector', 'running', 'Start collector beta.collector' ],
[ 'start_collector:beta.collector', 'done', 'Start collector beta.collector' ],
[ 'start_collector:fleet-skill.health', 'running', 'Start collector fleet-skill.health' ],
[ 'start_collector:fleet-skill.health', 'done', 'Start collector fleet-skill.health' ],
],
'start_collectors emits progress events while starting each configured non-manual collector',
);
is_deeply(
[ map { $_->{name} } @started ],
[ 'housekeeper', 'alpha.collector', 'beta.collector', 'fleet-skill.health' ],
'start_collectors still returns the started collector metadata while progress is enabled',
);
}
{
my $manual_home = tempdir(CLEANUP => 1);
my $manual_paths = Developer::Dashboard::PathRegistry->new( home => $manual_home );
my $manual_files = Developer::Dashboard::FileRegistry->new( paths => $manual_paths );
my $manual_config = Developer::Dashboard::Config->new( files => $manual_files, paths => $manual_paths );
$manual_config->save_global(
{
collectors => [
{
name => 'manual.collector',
command => 'true',
cwd => 'home',
},
],
}
);
my $manual_runner = Local::RuntimeRunner->new;
my $manual_manager = Developer::Dashboard::RuntimeManager->new(
app_builder => sub { return Local::RuntimeServer->new( foreground_file => "$manual_home/manual.txt", host => '127.0.0.1', port => 7991 ) },
config => $manual_config,
files => $manual_files,
paths => $manual_paths,
runner => $manual_runner,
);
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::_collector_runtime_ready = sub { return 1 };
my $started = $manual_manager->start_named_collector( name => 'manual.collector' );
is( $started->{pid}, 1001, 'start_named_collector returns the started loop pid for a manual collector' );
is( $manual_runner->{started_jobs}[0]{schedule}, 'interval', 'start_named_collector converts manual collectors into interval loops for on-demand starts' );
is( $manual_runner->{started_jobs}[0]{interval}, 30, 'start_named_collector applies the default interval for an on-demand manual collector loop' );
my $restarted = $manual_manager->restart_target( scope => 'collector', name => 'manual.collector' );
is( $restarted->{collectors}[0]{name}, 'manual.collector', 'restart_target reports the named manual collector in scoped collector mode' );
is( $restarted->{collectors}[0]{status}, 'restarted', 'restart_target marks the named manual collector as restarted' );
ok( grep { $_ eq 'manual.collector' } @{ $manual_runner->{stopped} }, 'restart_target stops an already running named manual collector before restarting it' );
is( $manual_runner->{started_jobs}[1]{schedule}, 'interval', 'restart_target also converts manual collectors into interval loops for restarts' );
}
{
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::capture = sub (&) {
return ( "State Recv-Q Send-Q Local Address:Port Peer Address:Port Process\nLISTEN 0 1024 127.0.0.1:7906 0.0.0.0:* users:((\"starman worker \",pid=123,fd=4),(\"starman master \",pid=456,fd=4))\n", '', 0 );
};
local *Developer::Dashboard::RuntimeManager::_is_managed_web = sub {
my ( undef, $pid ) = @_;
return $pid == 456 ? 1 : 0;
};
is_deeply(
[ $manager->_managed_listener_pids_for_port(7906) ],
[456],
'_managed_listener_pids_for_port filters ss listener pids down to managed dashboard processes',
);
}
{
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::capture = sub (&) { return ( '', 'ss: not found', 127 ) };
local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port_via_proc = sub {
my ( undef, $port ) = @_;
return $port == 7909 ? (321, 654) : ();
};
is_deeply(
[ $manager->_listener_pids_for_port(7909) ],
[321, 654],
'_listener_pids_for_port falls back to /proc listener discovery when ss is unavailable',
);
}
{
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::capture = sub (&) { return ( '', 'ss command not found', 1 ) };
local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port_via_proc = sub {
my ( undef, $port ) = @_;
return $port == 7917 ? (987) : ();
};
is_deeply(
[ $manager->_listener_pids_for_port(7917) ],
[987],
'_listener_pids_for_port also falls back to /proc when ss reports not found through stderr without exit 127',
);
}
{
local $Developer::Dashboard::Platform::OS_NAME = 'MSWin32';
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::capture = sub (&) {
return (
"2372\n5868\n2372\n",
'',
0,
);
};
is_deeply(
[ $manager->_listener_pids_for_port(7890) ],
[2372, 5868],
'_listener_pids_for_port discovers unique Windows listener pids through Get-NetTCPConnection output',
);
}
{
local $Developer::Dashboard::Platform::OS_NAME = 'MSWin32';
no warnings 'redefine';
t/09-runtime-manager.t view on Meta::CPAN
{
my $polls = 0;
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::sleep = sub { return 0 };
local *Developer::Dashboard::RuntimeManager::running_web = sub {
return {
pid => 8807,
port => 7919,
};
};
local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub {
my ( undef, $port ) = @_;
return () if $port != 7919;
$polls++;
return $polls < 3 ? () : (8807);
};
ok(
$manager->_web_runtime_ready( 8807, 7919 ),
'_web_runtime_ready waits for the managed listener port to appear',
);
is( $polls, 5, '_web_runtime_ready returns after the listener becomes visible plus the short confirmation window instead of burning the whole startup budget' );
}
{
my $polls = 0;
my $written_pid;
my $written_state;
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::sleep = sub { return 0 };
local *Developer::Dashboard::RuntimeManager::running_web = sub {
return {
host => '127.0.0.1',
pid => 8812,
port => 7927,
process_name => 'dashboard web: 127.0.0.1:7927',
ssl => 0,
status => 'running',
workers => 2,
};
};
local *Developer::Dashboard::RuntimeManager::web_state = sub {
return {
host => '127.0.0.1',
pid => 8812,
port => 7927,
process_name => 'dashboard web: 127.0.0.1:7927',
ssl => 0,
status => 'running',
workers => 2,
};
};
local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub {
my ( undef, $port ) = @_;
return () if $port != 7927;
$polls++;
return $polls < 2 ? () : (9912);
};
local *Developer::Dashboard::RuntimeManager::_same_pid_namespace = sub { return 1 };
local *Developer::Dashboard::RuntimeManager::_read_process_title = sub {
my ( undef, $pid ) = @_;
return $pid == 9912 ? 'starman master' : 'dashboard web: 127.0.0.1:7927';
};
local *Developer::Dashboard::RuntimeManager::_write_web_state = sub {
my ( undef, $state ) = @_;
$written_state = { %{$state} };
return $state;
};
local *Developer::Dashboard::FileRegistry::write = sub {
my ( undef, $name, $content ) = @_;
$written_pid = $content if $name eq 'web_pid';
return 1;
};
ok(
$manager->_web_runtime_ready( 8812, 7927 ),
'_web_runtime_ready adopts the actual listener pid when Starman replaces the startup wrapper process',
);
is( $written_pid, "9912\n", '_web_runtime_ready persists the adopted listener pid for later stop and restart flows' );
is( $written_state->{pid}, 9912, '_web_runtime_ready writes the adopted listener pid into persisted web state' );
is( $written_state->{process_name}, 'starman master', '_web_runtime_ready refreshes the process title after adopting the listener pid' );
}
{
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::sleep = sub { return 0 };
local *Developer::Dashboard::RuntimeManager::running_web = sub {
return {
pid => 8808,
port => 7920,
};
};
local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub { return () };
ok(
!$manager->_web_runtime_ready( 8808, 7920 ),
'_web_runtime_ready fails when the web pid survives but no listener appears on the configured port',
);
}
{
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::sleep = sub { return 0 };
local *Developer::Dashboard::RuntimeManager::running_web = sub {
return {
pid => 8808,
};
};
ok(
!$manager->_web_runtime_ready( 8808, undef ),
'_web_runtime_ready fails cleanly when neither the requested port nor the recorded runtime port exists',
);
}
{
my $polls = 0;
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::sleep = sub { return 0 };
local *Developer::Dashboard::RuntimeManager::running_web = sub {
return {
pid => 8809,
port => 7921,
};
};
local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub { return () };
local *Developer::Dashboard::RuntimeManager::_port_accepting_connections = sub {
my ( undef, $port ) = @_;
return 0 if $port != 7921;
$polls++;
return $polls >= 2 ? 1 : 0;
};
ok(
$manager->_web_runtime_ready( 8809, 7921 ),
'_web_runtime_ready falls back to a local TCP probe when listener pid discovery has not populated yet',
);
is( $polls, 4, '_web_runtime_ready only keeps the short confirmation window once the local TCP probe succeeds' );
}
{
my $polls = 0;
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::sleep = sub { return 0 };
t/09-runtime-manager.t view on Meta::CPAN
return { pid => $$, host => '127.0.0.1', port => 7919, status => 'running' };
};
local *Developer::Dashboard::RuntimeManager::_is_managed_web = sub { return 0 };
local *Developer::Dashboard::RuntimeManager::_find_web_processes = sub { return () };
local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub { return ($listener) };
my $state = $manager->running_web;
is( $state->{pid}, $$, 'running_web trusts the recorded master pid when the managed port is still held by a separate listener worker pid' );
kill 'KILL', $listener;
waitpid( $listener, 0 );
}
{
no warnings 'redefine';
$files->write( 'web_pid', "$$\n" );
$manager->_write_web_state( { pid => $$, host => '127.0.0.1', port => 7920, status => 'running' } );
local *Developer::Dashboard::RuntimeManager::_is_managed_web = sub { return 0 };
local *Developer::Dashboard::RuntimeManager::_find_web_processes = sub { return () };
local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub { return () };
my $state = $manager->running_web;
is( $state->{pid}, $$, 'running_web trusts the recorded running pid even when listener discovery cannot identify the managed process shape' );
$manager->_cleanup_web_files;
}
{
my $listener = fork();
die "fork failed: $!" if !defined $listener;
if ( !$listener ) {
local $SIG{TERM} = 'IGNORE';
sleep 30;
POSIX::_exit(0);
}
my $calls = 0;
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::running_web = sub {
return $calls++ == 0 ? { pid => $listener, port => 7908 } : undef;
};
local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub { return ($listener) };
local *Developer::Dashboard::RuntimeManager::_find_legacy_web_processes = sub { return () };
local *Developer::Dashboard::RuntimeManager::_pkill_perl = sub { return 1 };
is( $manager->stop_web, $listener, 'stop_web returns the recorded pid while it also tracks listener pids on the bound port' );
waitpid( $listener, 0 );
ok( !kill( 0, $listener ), 'stop_web escalates listener-port pids to KILL when they remain alive after TERM' );
}
{
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::web_state = sub {
return {
pid => 111_111,
host => '127.0.0.1',
port => 7917,
status => 'running',
};
};
local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub {
my ( undef, $port ) = @_;
return $port == 7917 ? (333_333) : ();
};
local *Developer::Dashboard::RuntimeManager::_read_process_title = sub {
my ( undef, $pid ) = @_;
return $pid == 333_333 ? 'starman master' : undef;
};
local *Developer::Dashboard::RuntimeManager::_cleanup_web_files = sub { die "_cleanup_web_files should not run while a saved listener still exists\n" };
my $running = $manager->running_web;
is( $running->{pid}, 333_333, 'running_web falls back to the saved listener pid when the real listener no longer keeps the dashboard wrapper title' );
is( $running->{port}, 7917, 'running_web keeps the persisted port when it resolves the live listener from saved state' );
is( $running->{process_name}, 'starman master', 'running_web records the actual listener process title when using saved-state listener fallback' );
}
{
my $late_listener = fork();
die "fork failed: $!" if !defined $late_listener;
if ( !$late_listener ) {
local $SIG{TERM} = 'IGNORE';
sleep 30;
POSIX::_exit(0);
}
my $running_calls = 0;
my $listener_calls = 0;
my $wait_calls = 0;
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::running_web = sub {
return $running_calls++ == 0 ? { pid => $late_listener, port => 7918 } : undef;
};
local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub {
return $listener_calls++ == 0 ? () : ($late_listener);
};
local *Developer::Dashboard::RuntimeManager::_find_legacy_web_processes = sub { return () };
local *Developer::Dashboard::RuntimeManager::_pkill_perl = sub { return 1 };
local *Developer::Dashboard::RuntimeManager::_wait_for_port_release = sub {
return $wait_calls++ == 0 ? 0 : 1;
};
is( $manager->stop_web, $late_listener, 'stop_web returns the recorded pid when it has to re-probe the bound port for late listeners' );
waitpid( $late_listener, 0 );
ok( !kill( 0, $late_listener ), 'stop_web kills late-discovered listener pids after an initial port-release timeout' );
}
{
my $listener = fork();
die "fork failed: $!" if !defined $listener;
if ( !$listener ) {
local $SIG{TERM} = sub { exit 0 };
while (1) { sleep 0.1 }
}
sleep 0.2;
no warnings 'redefine';
local *Developer::Dashboard::RuntimeManager::running_web = sub { return };
local *Developer::Dashboard::RuntimeManager::web_state = sub {
return {
pid => 444_444,
host => '127.0.0.1',
port => 7919,
status => 'running',
};
};
local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub {
my ( undef, $port ) = @_;
return $port == 7919 ? ($listener) : ();
};
local *Developer::Dashboard::RuntimeManager::_find_legacy_web_processes = sub { return () };
local *Developer::Dashboard::RuntimeManager::_pkill_perl = sub { return 1 };
local *Developer::Dashboard::RuntimeManager::_wait_for_port_release = sub { return 1 };
my $stopped = $manager->stop_web;
is( $stopped, 444_444, 'stop_web preserves the saved managed pid even when it has to terminate a fallback listener pid from persisted state' );
waitpid( $listener, 0 );
ok( !kill( 0, $listener ), 'stop_web terminates the saved listener pid resolved from persisted web state' );
}
( run in 0.877 second using v1.01-cache-2.11-cpan-e93a5daba3e )