Sponsoring The Perl Toolchain Summit 2025: Help make this important event another success Learn more

#
# (c) Jan Gehring <jan.gehring@gmail.com>
#
# vim: set ts=2 sw=2 tw=0:
# vim: set expandtab:
=head1 NAME
Rex::Test::Base - Basic Test Module
=head1 DESCRIPTION
This is a basic test module to test your code with the help of local VMs. You can place your tests in the "t" directory.
=head1 EXAMPLE
use Rex::Test::Base;
use Data::Dumper;
use Rex -base;
test {
my $t = shift;
$t->name("ubuntu test");
$t->vm_auth(user => "root", password => "box");
$t->run_task("setup");
$t->has_package("vim");
$t->has_package("ntp");
$t->has_package("unzip");
$t->has_file("/etc/ntp.conf");
$t->has_service_running("ntp");
$t->has_content("/etc/passwd", qr{root:x:0:}ms);
run "ls -l";
$t->ok($? == 0, "ls -l returns success.");
$t->finish;
};
1; # last line
=head1 METHODS
=cut
use strict;
our $VERSION = '1.7.0'; # VERSION
require Rex::Commands;
use Carp;
require Exporter;
use base qw(Exporter);
use vars qw(@EXPORT);
@EXPORT = qw(test);
=head2 new(name => $test_name)
Constructor if used in OO mode.
my $test = Rex::Test::Base->new(name => "test_name");
=cut
sub new {
my $that = shift;
my $proto = ref($that) || $that;
my $self = {@_};
bless( $self, $proto );
my ( $pkg, $file ) = caller(0);
$self->{name} ||= $file;
$self->{redirect_port} = 2222;
$self->{memory} ||= 512; # default, in MB
$self->{cpu} ||= 1; # default
return $self;
}
=head2 name($name)
The name of the test. A VM called $name will be created for each test. If the VM already exists, Rex will try to reuse it.
=cut
sub name {
my ( $self, $name ) = @_;
$self->{name} = $name;
}
=head2 memory($amount)
The amount of memory the VM should use, in Megabytes.
=cut
sub memory {
my ( $self, $memory ) = @_;
$self->{memory} = $memory;
}
=head2 cpus($number)
The number of CPUs the VM should use.
=cut
sub cpus {
my ( $self, $cpus ) = @_;
$self->{cpus} = $cpus;
}
=head2 vm_auth(%auth)
Authentication options for the VM. It accepts the same parameters as C<Rex::Box::Base-E<gt>auth()>.
=cut
sub vm_auth {
my ( $self, %auth ) = @_;
$self->{auth} = \%auth;
}
=head2 base_vm($vm)
The URL to a base image to be used for the test VM.
=cut
sub base_vm {
my ( $self, $vm ) = @_;
$self->{vm} = $vm;
}
sub test(&) { ## no critic ProhibitSubroutinePrototypes
my $code = shift;
my $test = __PACKAGE__->new;
$code->($test);
}
=head2 redirect_port($port)
Redirect local $port to the VM's SSH port (default: 2222).
=cut
sub redirect_port {
my ( $self, $port ) = @_;
$self->{redirect_port} = $port;
}
=head2 run_task($task)
The task to run on the test VM. You can run multiple tasks by passing an array reference.
=cut
sub run_task {
my ( $self, $task ) = @_;
# allow multiple calls to run_task() without setting up new box
if ( $self->{box} ) {
$self->{box}->provision_vm($task);
return;
}
my $box;
box {
$box = shift;
$box->name( $self->{name} );
$box->url( $self->{vm} );
$box->memory( $self->{memory} );
$box->cpus( $self->{cpus} );
$box->network(
1 => {
type => "nat",
}
);
$box->forward_port( ssh => [ $self->{redirect_port}, 22 ] );
$box->auth( %{ $self->{auth} } );
if ( ref $task eq 'ARRAY' ) {
$box->setup(@$task);
}
else {
$box->setup($task);
}
};
$self->{box} = $box;
# connect to the machine
Rex::connect(
server => $box->ip,
%{ $self->{auth} },
);
}
sub ok() {
my ( $self, $test, $msg ) = @_;
my $tb = Rex::Test::Base->builder;
$tb->ok( $test, $msg );
}
sub like {
my ( $self, $thing, $want, $name ) = @_;
my $tb = Rex::Test::Base->builder;
$tb->like( $thing, $want, $name );
}
sub diag {
my ( $self, $msg ) = @_;
my $tb = Rex::Test::Base->builder;
$tb->diag($msg);
}
sub finish {
my $tb = Rex::Test::Base->builder;
$tb->done_testing();
$tb->is_passing()
? print "PASS\n"
: print "FAIL\n";
if ( !$tb->is_passing() ) {
Rex::Test::push_exit("FAIL");
}
$tb->reset();
Rex::pop_connection();
}
=head1 TEST METHODS
=head2 has_content($file, $regexp)
Test if the content of $file matches against $regexp.
=head2 has_dir($path)
Test if $path is present and is a directory.
=head2 has_file($file)
Test if $file is present.
=head2 has_package($package, $version)
Test if $package is installed, optionally at $version.
=head2 has_service_running($service)
Test if $service is running.
=head2 has_service_stopped($service)
Test if $service is stopped.
=head2 has_stat($file, $stat)
Test if $file has properties described in hash reference $stat. List of supported checks:
=over 4
=item group
=item owner
=back
=cut
our $AUTOLOAD;
sub AUTOLOAD {
my $self = shift or return;
( my $method = $AUTOLOAD ) =~ s{.*::}{};
if ( $method eq "DESTROY" ) {
return;
}
my $real_method = $method;
my $is_not = 0;
if ( $real_method =~ m/^has_not_/ ) {
$real_method =~ s/^has_not_/has_/;
$is_not = 1;
}
my $pkg = __PACKAGE__ . "::$real_method";
eval "use $pkg";
if ($@) {
confess "Error loading $pkg. No such test method.";
}
my $p = $pkg->new;
if ($is_not) {
$p->run_not_test(@_);
}
else {
$p->run_test(@_);
}
}
1;