The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
# 01-func.t
# Test the public BSD::Process routines
#
# Copyright (C) 2006-2011 David Landgren

use strict;
use Test::More;

use BSD::Process;
plan tests => 152 + scalar(BSD::Process::attr());

use Config;

my $Unchanged = 'The scalar remains the same';
$_ = $Unchanged;

my $RUNNING_ON_FREEBSD_4 = $Config{osvers} =~ /^4/;
my $RUNNING_ON_FREEBSD_5 = $Config{osvers} =~ /^5/;

my $info = BSD::Process::info();

# remove all attributes from object, should be none left over
ok( defined( delete $info->{pid} ), 'attribute pid');
ok( defined( delete $info->{ppid} ), 'attribute ppid');
ok( defined( delete $info->{pgid} ), 'attribute pgid');
ok( defined( delete $info->{tpgid} ), 'attribute tpgid');
ok( defined( delete $info->{sid} ), 'attribute sid');
ok( defined( delete $info->{jobc} ), 'attribute jobc');
ok( defined( delete $info->{rssize} ), 'attribute rssize');
ok( defined( delete $info->{swrss} ), 'attribute swrss');
ok( defined( delete $info->{tsize} ), 'attribute tsize');
ok( defined( delete $info->{xstat} ), 'attribute xstat');
ok( defined( delete $info->{acflag} ), 'attribute acflag');
ok( defined( delete $info->{pctcpu} ), 'attribute pctcpu');
ok( defined( delete $info->{estcpu} ), 'attribute estcpu');
ok( defined( delete $info->{slptime} ), 'attribute slptime');
ok( defined( delete $info->{swtime} ), 'attribute swtime');
ok( defined( delete $info->{runtime} ), 'attribute runtime');
ok( defined( delete $info->{flag} ), 'attribute flag');
ok( defined( delete $info->{nice} ), 'attribute nice');
ok( defined( delete $info->{lock} ), 'attribute lock');
ok( defined( delete $info->{rqindex} ), 'attribute rqindex');
ok( defined( delete $info->{oncpu} ), 'attribute oncpu');
ok( defined( delete $info->{lastcpu} ), 'attribute lastcpu');
ok( defined( delete $info->{wmesg} ), 'attribute wmesg');
ok( defined( delete $info->{login} ), 'attribute login');
ok( defined( delete $info->{comm} ), 'attribute comm');

ok( defined( delete $info->{args} ), 'attribute args');
ok( defined( delete $info->{tsid} ), 'attribute tsid');
ok( defined( delete $info->{uid} ), 'attribute uid');
ok( defined( delete $info->{ruid} ), 'attribute ruid');
ok( defined( delete $info->{svuid} ), 'attribute svuid');
ok( defined( delete $info->{rgid} ), 'attribute rgid');
ok( defined( delete $info->{svgid} ), 'attribute svgid');
ok( defined( delete $info->{size} ), 'attribute size');
ok( defined( delete $info->{dsize} ), 'attribute dsize');
ok( defined( delete $info->{ssize} ), 'attribute ssize');
ok( defined( delete $info->{start} ), 'attribute start');
ok( defined( delete $info->{childtime} ), 'attribute childtime');
ok( defined( delete $info->{advlock} ), 'attribute advlock');
ok( defined( delete $info->{controlt} ), 'attribute controlt');
ok( defined( delete $info->{kthread} ), 'attribute kthread');
ok( defined( delete $info->{noload} ), 'attribute noload');
ok( defined( delete $info->{ppwait} ), 'attribute ppwait');
ok( defined( delete $info->{profil} ), 'attribute profil');
ok( defined( delete $info->{stopprof} ), 'attribute stopprof');
ok( defined( delete $info->{sugid} ), 'attribute sugid');
ok( defined( delete $info->{system} ), 'attribute system');
ok( defined( delete $info->{single_exit} ), 'attribute single_exit');
ok( defined( delete $info->{traced} ), 'attribute traced');
ok( defined( delete $info->{waited} ), 'attribute waited');
ok( defined( delete $info->{wexit} ), 'attribute wexit');
ok( defined( delete $info->{exec} ), 'attribute exec');
ok( defined( delete $info->{kiflag} ), 'attribute kiflag');
ok( defined( delete $info->{locked} ), 'attribute locked');
ok( defined( delete $info->{isctty} ), 'attribute isctty');
ok( defined( delete $info->{issleader} ), 'attribute issleader');
ok( defined( delete $info->{stat} ), 'attribute stat');
ok( defined( delete $info->{stat_1} ), 'attribute stat_1');
ok( defined( delete $info->{stat_2} ), 'attribute stat_2');
ok( defined( delete $info->{stat_3} ), 'attribute stat_3');
ok( defined( delete $info->{stat_4} ), 'attribute stat_4');
ok( defined( delete $info->{stat_5} ), 'attribute stat_5');
ok( defined( delete $info->{stat_6} ), 'attribute stat_6');
ok( defined( delete $info->{stat_7} ), 'attribute stat_7');
ok( defined( delete $info->{ocomm} ), 'attribute ocomm');
ok( defined( delete $info->{lockname} ), 'attribute lockname');
ok( defined( delete $info->{pri_class} ), 'attribute pri_class');
ok( defined( delete $info->{pri_level} ), 'attribute pri_level');
ok( defined( delete $info->{pri_native} ), 'attribute pri_native');
ok( defined( delete $info->{pri_user} ), 'attribute pri_user');
ok( defined( delete $info->{utime} ), 'attribute utime');
ok( defined( delete $info->{stime} ), 'attribute stime');
ok( defined( delete $info->{time} ), 'attribute time (utime+stime)');
ok( defined( delete $info->{maxrss} ), 'attribute maxrss');
ok( defined( delete $info->{ixrss} ), 'attribute ixrss');
ok( defined( delete $info->{idrss} ), 'attribute idrss');
ok( defined( delete $info->{isrss} ), 'attribute isrss');
ok( defined( delete $info->{minflt} ), 'attribute minflt');
ok( defined( delete $info->{majflt} ), 'attribute majflt');
ok( defined( delete $info->{nswap} ), 'attribute nswap');
ok( defined( delete $info->{inblock} ), 'attribute inblock');
ok( defined( delete $info->{oublock} ), 'attribute oublock');
ok( defined( delete $info->{msgsnd} ), 'attribute msgsnd');
ok( defined( delete $info->{msgrcv} ), 'attribute msgrcv');
ok( defined( delete $info->{nsignals} ), 'attribute nsignals');
ok( defined( delete $info->{nvcsw} ), 'attribute nvcsw');
ok( defined( delete $info->{nivcsw} ), 'attribute nivcsw');

ok( defined( delete $info->{hadthreads} ), 'attribute hadthreads');
ok( defined( delete $info->{emul} ), 'attribute emul');
ok( defined( delete $info->{jid} ), 'attribute jid');
ok( defined( delete $info->{numthreads} ), 'attribute numthreads');
ok( defined( delete $info->{utime_ch} ), 'attribute utime_ch');
ok( defined( delete $info->{stime_ch} ), 'attribute stime_ch');
ok( defined( delete $info->{time_ch} ), 'attribute time_ch (utime_ch+stime_ch)');
ok( defined( delete $info->{maxrss_ch} ), 'attribute maxrss_ch');
ok( defined( delete $info->{ixrss_ch} ), 'attribute ixrss_ch');
ok( defined( delete $info->{idrss_ch} ), 'attribute idrss_ch');
ok( defined( delete $info->{isrss_ch} ), 'attribute isrss_ch');
ok( defined( delete $info->{minflt_ch} ), 'attribute minflt_ch');
ok( defined( delete $info->{majflt_ch} ), 'attribute majflt_ch');
ok( defined( delete $info->{nswap_ch} ), 'attribute nswap_ch');
ok( defined( delete $info->{inblock_ch} ), 'attribute inblock_ch');
ok( defined( delete $info->{oublock_ch} ), 'attribute oublock_ch');
ok( defined( delete $info->{msgsnd_ch} ), 'attribute msgsnd_ch');
ok( defined( delete $info->{msgrcv_ch} ), 'attribute msgrcv_ch');
ok( defined( delete $info->{nsignals_ch} ), 'attribute nsignals_ch');
ok( defined( delete $info->{nvcsw_ch} ), 'attribute nvcsw_ch');
ok( defined( delete $info->{nivcsw_ch} ), 'attribute nivcsw_ch');

my $ngroups;
ok( defined( $ngroups = delete $info->{ngroups} ), 'attribute ngroups');

# attribute returning non-scalars

my $grouplist = delete $info->{groups};
ok( defined($grouplist), 'attribute groups' );
is( ref($grouplist), 'ARRAY', q{... it's a list} );
if ($RUNNING_ON_FREEBSD_4) {
    pass("... of the expected size (unknowable on FreeBSD 4.x)");
}
else {
    is( scalar(@$grouplist), $ngroups, "... of the expected size" )
        or diag("grouplist = (@$grouplist)");
}

# check for typos in hv_store calls in Process.xs
is( scalar(keys %$info), 0, 'all attributes have been accounted for' )
    or diag( 'leftover: ' . join( ',', keys %$info ));

my @attribute = BSD::Process::attr;
my $max_len = 0;
my $proc    = BSD::Process::info();
my $exists  = 0;
for my $attr (@attribute) {
    if ($max_len < length($attr)) {
        $max_len = length($attr);
    }
    if (exists $proc->{$attr}) {
        pass("lookup $attr");
        delete $proc->{$attr};
        ++$exists;
    }
    else {
        fail("lookup $attr");
    }
}
is($max_len, BSD::Process::attr_len, 'length of longest attribute');
is($exists, scalar(@attribute), "all lookups exist");
is(scalar(grep {!/^_/} keys %$proc), 0, 'nothing left to look up');

is(scalar(@attribute), scalar(BSD::Process::attr_alias), 'attributes and aliases');

my @all = BSD::Process::list();
my $all_procs = @all;
cmp_ok( scalar(@all), '>', 10, "list of all processes ($all_procs)" )
    or diag("proclist: (@all)");

# processes owned by a uid
SKIP: {
    skip( "not supported on FreeBSD 4.x", 4 )
        if $RUNNING_ON_FREEBSD_4;
    # count the processes owned by each uid
    my %uid;
    for my $pid (@all) {
        my $proc = BSD::Process->new($pid);
        if ($proc) {
            diag( "proc $proc->{_pid} is a zombie" )
                if exists $proc->{_pid} and not exists $proc->{pid};
            $uid{$proc->{uid}}++;
        }
        else {
            diag( "new() failed for pid $pid" );
        }
    }

    # now find the uids that own the most processes
    my ($biggest, $bigger) = (sort {$uid{$b} <=> $uid{$a} || $a <=> $b} keys %uid )[0,1];

    my @proc = BSD::Process::list( uid => $biggest );
    cmp_ok( scalar(@proc), '<', $all_procs, "uid $biggest smaller than count of all processes" );

    my $biggest_uid = @proc;
    @proc = BSD::Process::list( effective_user_id => $bigger );
    cmp_ok( scalar(@proc), '<',  $all_procs, "uid $bigger smaller than count of all processes" );
    cmp_ok( scalar(@proc), '<=', $biggest_uid, "uid $bigger smaller or equal to uid $biggest" );

    my $all_uid = BSD::Process::all( uid => $biggest );
    my $total = scalar(keys %$all_uid);
    my $same_uid = 0;
    for my $proc (keys %$all_uid) {
        ++$same_uid if $all_uid->{$proc}{uid} == $biggest;
    }
    is ($total, $same_uid, "same number of processes for uid $biggest" )
        or do {
            diag( "pid: $_ uid: all_uid->{$_}{uid}" )
                for keys %$all_uid;
        };
}

# processes owned by a ruid
SKIP: {
    skip( "not supported on FreeBSD 4.x", 5 )
        if $RUNNING_ON_FREEBSD_4;
    # count the processes owned by each real uid
    my %ruid;
    for my $pid (@all) {
        my $proc = BSD::Process->new($pid);
        $ruid{$proc->{ruid}}++ if defined $proc->{ruid};
    }

    # now find the uids that own the most processes
    my ($biggest, $bigger) = (sort {$ruid{$b} <=> $ruid{$a} || $a <=> $b} keys %ruid )[0,1];

    my @proc = BSD::Process::list( ruid => $biggest );
    cmp_ok( scalar(@proc), '<', $all_procs, "ruid $biggest smaller than count of all processes" );

    my $biggest_ruid = @proc;
    @proc = BSD::Process::list( real_user_id => $bigger );
    cmp_ok( scalar(@proc), '<',  $all_procs, "ruid $bigger smaller than count of all processes" );
    cmp_ok( scalar(@proc), '<=', $biggest_ruid, "ruid $bigger smaller or equal to ruid $biggest" );

    my $all_ruid = BSD::Process::all( resolve => 1, ruid => $bigger );
    my $total = keys %$all_ruid;
    my $same_uid = 0;
    my $blessed  = 0;
    for my $proc (keys %$all_ruid) {
        ++$same_uid if scalar(getpwnam($all_ruid->{$proc}{ruid})) == $bigger;
        ++$blessed if ref($all_ruid->{$proc}) eq 'BSD::Process';
    }
    is ($total, $same_uid, "same number of processes for ruid $bigger" )
        or do {
            diag( "pid: $_ uid: $all_ruid->{$_}{uid}" )
                for keys %$all_ruid;
        };

    is ($total, $blessed, "... and all blessed BSD::Process objects" );
}

SKIP: {
    # processes owned by an effective gid
    skip( "not supported on FreeBSD 4.x or 5.x", 6 )
        if $RUNNING_ON_FREEBSD_4 or $RUNNING_ON_FREEBSD_5;
    # count the processes owned by each effective gid
    # kinfo_proc lacks a gid field, so we'll punt with a real gid
    my %gid;
    for my $pid (@all) {
        my $proc = BSD::Process->new($pid);
        $gid{$proc->{rgid}}++ if defined $proc->{rgid};
    }

    # now find the gids that own the most processes
    my ($biggest, $bigger) = (sort {$gid{$b} <=> $gid{$a} || $a <=> $b} keys %gid )[0,1];

    my @proc = BSD::Process::list( gid => $biggest );
    cmp_ok( scalar(@proc), '<', $all_procs, "gid $biggest smaller than count of all processes" );

    my $biggest_gid = @proc;
    @proc = BSD::Process::list( effective_group_id => $bigger );
    cmp_ok( scalar(@proc), '<',  $all_procs, "gid $bigger smaller than count of all processes" );
    cmp_ok( scalar(@proc), '<=', $biggest_gid, "gid $bigger smaller or equal to gid $biggest" );

    # processes owned by a rgid
    my %rgid;
    for my $pid (@all) {
        my $proc = BSD::Process->new($pid);
        $rgid{$proc->{rgid}}++ if defined $proc->{rgid};
    }

    # now find the gids that own the most processes
    ($biggest, $bigger) = (sort {$rgid{$b} <=> $rgid{$a} || $a <=> $b} keys %rgid )[0,1];

    @proc = BSD::Process::list( rgid => $biggest );
    cmp_ok( scalar(@proc), '<', $all_procs, "rgid $biggest smaller than count of all processes" );

    my $biggest_rgid = @proc;
    @proc = BSD::Process::list( real_group_id => $bigger );
    cmp_ok( scalar(@proc), '<',  $all_procs, "rgid $bigger smaller than count of all processes" );
    cmp_ok( scalar(@proc), '<=', $biggest_rgid, "rgid $bigger smaller or equal to rgid $biggest" );
}

# process groups
SKIP: {
    skip( "not supported on FreeBSD 4.x", 6 )
        if $RUNNING_ON_FREEBSD_4;
    # count the processes in each process group
    my %pgid;
    for my $pid (@all) {
        my $proc = BSD::Process->new($pid);
        $pgid{$proc->{pgid}}++ if defined $proc->{pgid};
    }

    # now find the process groups with the most members
    my ($biggest, $bigger) = (sort {$pgid{$b} <=> $pgid{$a} || $a <=> $b} keys %pgid )[0,1];

    my @proc = BSD::Process::list( pgid => $biggest );
    cmp_ok( scalar(@proc), '<', $all_procs, "pgid $biggest smaller than count of all processes" );

    my $biggest_pgid = @proc;
    @proc = BSD::Process::list( process_group_id => $bigger );
    cmp_ok( scalar(@proc), '<',  $all_procs, "pgid $bigger smaller than count of all processes" );
    cmp_ok( scalar(@proc), '<=', $biggest_pgid, "pgid $bigger smaller or equal to pgid $biggest" );

    # process sessions
    # count the processes in each process session
    my %sid;
    for my $pid (@all) {
        my $proc = BSD::Process->new($pid);
        $sid{$proc->{sid}}++ if defined $proc->{sid};
    }

    # now find the process groups with the most members
    ($biggest, $bigger) = (sort {$sid{$b} <=> $sid{$a} || $a <=> $b} keys %sid )[0,1];

    @proc = BSD::Process::list( sid => $biggest );
    cmp_ok( scalar(@proc), '<', $all_procs, "sid $biggest smaller than count of all processes" );

    my $biggest_sid = @proc;
    @proc = BSD::Process::list( process_session_id => $bigger );
    cmp_ok( scalar(@proc), '<',  $all_procs, "sid $bigger smaller than count of all processes" );
    cmp_ok( scalar(@proc), '<=', $biggest_sid, "sid $bigger smaller or equal to sid $biggest" );
}

$info = BSD::Process::info($$);
is( $info->{pid}, $$, "system says my pid is the same ($$)" );
isnt( $info->{pid}, $info->{ppid}, 'I am not my parent' );

my $parent = BSD::Process::info($info->{ppid});
is( $parent->{pid}, $info->{ppid}, 'my parent is indeed my parent' );
isnt( $info->{pid}, $parent->{ppid}, 'I am not my grandparent' );
isnt( $parent->{pid}, $parent->{ppid}, 'and my parent is not my grandparent' );

SKIP: {
    skip( "not supported on FreeBSD 4.x", 6 )
        if $RUNNING_ON_FREEBSD_4;

    my $resolved = BSD::Process::info({resolve => 1});
    is( $resolved->{uid}, scalar(getpwuid($info->{uid})), 'resolve implicit pid' );

    $resolved = BSD::Process::info($info->{pid}, {resolve => 1});
    is( $resolved->{uid}, scalar(getpwuid($info->{uid})), 'resolve explicit pid' );

    my $root = BSD::Process::all( uid => 'root' );
    my $uid_root_count = 0;
    $root->{$_}->uid == 0 and ++$uid_root_count for keys %$root;
    is( $uid_root_count, scalar(keys %$root), q{counted all uid root's processes} );

    $root = BSD::Process::all( effective_user_id => 'root' );
    $uid_root_count = 0;
    $root->{$_}->uid == 0 and ++$uid_root_count for keys %$root;
    is( $uid_root_count, scalar(keys %$root), q{counted all effective uid root's processes} );

    $root = BSD::Process::all( ruid => 'root' );
    $uid_root_count = 0;
    for (keys %$root) {
        if ($root->{$_}->uid == 0) {
            ++$uid_root_count;
        }
        elsif ($root->{$_}->ruid == 0) {
            ++$uid_root_count;
            $ENV{PERL_AUTHOR_TESTING}
                and diag("root proc $_ has uid " . $root->{$_}->uid . "/" . $root->{$_}->ruid  );
        }
    }
    is( $uid_root_count, scalar(keys %$root), q{counted all ruid root's processes} );

    $root = BSD::Process::all( real_user_id => 'root' );
    $uid_root_count = 0;
    $root->{$_}->ruid == 0 and ++$uid_root_count for keys %$root;
    is( $uid_root_count, scalar(keys %$root), q{counted all real_user_id root's processes} );
}

SKIP: {
    skip( "not supported on FreeBSD 4.x or 5.x", 2 )
        if $RUNNING_ON_FREEBSD_4 or $RUNNING_ON_FREEBSD_5;

    my $wheel_gid = getgrnam('wheel');
    {
        my $wheel = BSD::Process::all( gid => 'wheel' );
        my $gid_wheel_count = 0;
        for my $pid (keys %$wheel) {
            my $proc = $wheel->{$pid};
            if ($proc->rgid == $wheel_gid) {
                 ++$gid_wheel_count;
            }
            else {
                my $msg = "$proc->{comm}($proc->{pid}) has rgid $proc->{rgid} not $wheel_gid";
                if ($proc->{comm} eq 'sshd') {
                    # sshd uses process separation, which throws this off
                    ++$gid_wheel_count;
                    $msg .= " (pass)";
                }
                $ENV{PERL_AUTHOR_TESTING} and diag( $msg );
            }
        }
        is( $gid_wheel_count, scalar(keys %$wheel), q{counted all gid wheel's processes} );
    }

    {
        my $wheel = BSD::Process::all( effective_group_id => 'wheel' );
        my $gid_wheel_count = 0;
        for my $pid (keys %$wheel) {
            my $proc = $wheel->{$pid};
            if ($proc->rgid == $wheel_gid) {
                 ++$gid_wheel_count;
            }
            else {
                my $msg = "$proc->{comm}($proc->{pid}) has rgid $proc->{rgid} not $wheel_gid";
                if ($proc->{comm} eq 'sshd') {
                    # sshd uses process separation, which throws this off
                    ++$gid_wheel_count;
                    $msg .= " (pass)";
                }
                $ENV{PERL_AUTHOR_TESTING} and diag( $msg );
            }
        }

        is( $gid_wheel_count, scalar(keys %$wheel), q{counted all effective_group_id wheel's processes} );
    }
}

is($_, $Unchanged, $Unchanged);