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 )