From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

# ABSTRACT: A class for testing a Pinto server
use Moose;
use MooseX::Types::Moose qw(Str Int ArrayRef);
use Carp;
use Path::Class qw(dir);
use Pinto::Types qw(File Uri);
use HTTP::Server::PSGI; # just to make sure we have it
#-------------------------------------------------------------------------------
# VERSION
#-------------------------------------------------------------------------------
extends 'Pinto::Tester';
#-------------------------------------------------------------------------------
=attr pintod_opts( \@args )
Passes additional C<@args> to the F<pintod> command line. Default is empty.
=cut
has pintod_opts => (
is => 'ro',
isa => ArrayRef,
default => sub { [] },
lazy => 1,
);
=attr server_port( $integer )
Sets the port that the server will listen on. If not specified during
construction, defaults to a randomly generated but open port.
=cut
has server_port => (
is => 'ro',
isa => Int,
default => sub { empty_port() },
lazy => 1,
);
=attr server_host( $hostname )
Sets the hostname that the server will bind to. Defaults to C<localhost>.
=cut
has server_host => (
is => 'ro',
isa => Str,
init_arg => undef,
default => 'localhost',
lazy => 1,
);
=attr server_pid
Returns the process id for the server (if it has been started). Read-only.
=cut
has server_pid => (
is => 'rw',
isa => Int,
init_arg => undef,
default => 0,
);
=attr server_url
Returns the full URL that the server will listen on. Read-only.
=cut
has server_url => (
is => 'ro',
isa => Uri,
init_arg => undef,
default => sub { URI->new( 'http://' . $_[0]->server_host . ':' . $_[0]->server_port ) },
);
=attr pintod_exe
Sets the path to the C<pintod> executable. If not specified, we will search
in F<./blib/script>, F<./bin>, C<PINTO_HOME>, and finally your C<PATH> An
exception is thrown if C<pintod> cannot be found.
=cut
has pintod_exe => (
is => 'ro',
isa => File,
builder => '_build_pintod_exe',
coerce => 1,
lazy => 1,
);
#-------------------------------------------------------------------------------
sub _build_pintod_exe {
my ($self) = @_;
# Look inside the dist directory
for my $dir ( [qw(blib script)], [qw(bin)] ) {
my $pintod = dir( @{$dir} )->file('pintod');
return $pintod if -e $pintod;
}
# Look at PINTO_HOME
return dir( $ENV{PINTO_HOME} )->file(qw(bin pintod))
if $ENV{PINTO_HOME};
# Look anywhere in PATH
return which('pintod')
|| croak 'Unable to find pintod anywhere';
}
#-------------------------------------------------------------------------------
=method start_server()
Starts the L<pintod> server. Emits a warning if the server is already started.
=cut
sub start_server {
my ($self) = @_;
carp 'Server already started' and return if $self->server_pid;
local $ENV{PLACK_ENV} = 'testing'; # Suppresses startup message
local $ENV{PLACK_SERVER} = 'HTTP::Server::PSGI'; # Basic non-forking server
local $ENV{PINTO_LOCKFILE_TIMEOUT} = 2; # Don't make tests wait!
local $ENV{PINTO_STALE_LOCKFILE_TIMEOUT} = 0; # Don't expire stale locks
run_fork {
child {
my $xtra_lib = $self->_extra_lib;
my $xtra_opts = $self->pintod_opts;
my %opts = ( '--port' => $self->server_port, '--root' => $self->root );
my @cmd = ( $^X, $xtra_lib, $self->pintod_exe, %opts, @{$xtra_opts} );
$self->tb->note( sprintf 'exec(%s)', join ' ', @cmd );
exec @cmd;
}
parent {
my $server_pid = shift;
$self->server_pid($server_pid);
sleep 15; # Let the server warm up
}
error {
croak "Failed to fork: $!";
}
};
return $self;
}
#-------------------------------------------------------------------------------
=method stop_server()
Stops the L<pintod> server. Emits a warning if the server is not
currently running.
=cut
sub stop_server {
my ($self) = @_;
my $server_pid = $self->server_pid;
carp 'Server was never started' and return if not $server_pid;
carp "Server $server_pid not running" and return if not kill 0, $server_pid;
# TODO: Consider using Proc::Terminator instead
kill 'TERM', $server_pid;
sleep 5 and waitpid $server_pid, 0;
return $self;
}
#-------------------------------------------------------------------------------
=method server_running_ok()
Asserts that the server is running.
=cut
sub server_running_ok {
my ($self) = @_;
my $server_pid = $self->server_pid;
my $server_port = $self->server_port;
my $ok = kill 0, $server_pid; # Is this portable?
return $self->tb->ok( $ok, "Server $server_pid is running on port $server_port" );
}
#-------------------------------------------------------------------------------
=method server_not_running_ok
Asserts that the server is not running.
=cut
sub server_not_running_ok {
my ($self) = @_;
my $server_pid = $self->server_pid;
my $ok = not kill 0, $server_pid; # Is this portable?
return $self->tb->ok( $ok, "Server is not running with pid $server_pid" );
}
#-------------------------------------------------------------------------------
=method can_connect
Returns true if the server is able to receive and respond to a connection
request.
=cut
sub can_connect {
my ($self) = @_;
# LWP uses a 500 error when the connection is refused. I'm not
# sure if this will be portable to otehr user agents. Might be
# better to open a raw socket. See IO::Socket::INET for that.
return LWP::UserAgent->new->get($self->server_url)->code != 500;
}
#-------------------------------------------------------------------------------
sub to_string {
my $self = shift;
return $self->server_url->as_string;
}
#-------------------------------------------------------------------------------
sub _extra_lib {
my ($self) = @_;
my $blib = dir(qw(blib lib));
my $lib = dir(qw( lib));
return "-I$blib" if -e $blib;
return "-I$lib" if -e $lib;
return '';
}
#-------------------------------------------------------------------------------
sub DEMOLISH {
my ($self) = @_;
$self->stop_server if $self->server_pid;
return;
}
#-------------------------------------------------------------------------------
__PACKAGE__->meta->make_immutable;
#-------------------------------------------------------------------------------
1;
__END__
=pod
=for stopwords responder
=for Pod::Coverage DEMOLISH
=cut