Back to documentation
package Statocles::Test;
our $VERSION = '0.094';
# ABSTRACT: Common test routines for Statocles
use Statocles::Base;
use Statocles::Util qw( dircopy derp );
use base qw( Exporter );
our @EXPORT_OK = qw(
test_constructor test_pages build_test_site build_test_site_apps
build_temp_site
);
=sub build_test_site
my $site = build_test_site( %site_args )
Build a site for testing. The build and deploy will be set correctly to temporary
directories. C<%site_args> will be given to the L<Statocles::Site|Statocles::Site>
constructor.
You must provide a C<theme> (probably using the one in C<t/share/theme>).
=cut
sub build_test_site {
my ( %site_args ) = @_;
require Statocles::Site;
require Statocles::Store;
require Statocles::Deploy::File;
my $store = $site_args{build_store}
? Statocles::Store->new( delete $site_args{build_store} )
: Path::Tiny->tempdir
;
my $deploy = $site_args{deploy}
? Statocles::Deploy::File->new( delete $site_args{deploy} )
: Path::Tiny->tempdir
;
# Give a testable logger by default, but only if we haven't asked
# for some verbose logging from the environment
my $log = $site_args{log}
|| Mojo::Log->new(
level => 'warn',
max_history_size => 500,
);
return Statocles::Site->new(
title => 'Example Site',
build_store => $store,
deploy => $deploy,
log => $log,
%site_args,
);
}
=sub build_test_site_apps
my ( $site, $build_dir, $deploy_dir ) = build_test_site_apps( $share_dir, %site_args );
Build a site for testing, with some apps. Returns the site, the build dir, and the
deploy dir.
=cut
sub build_test_site_apps {
my ( $share_dir, %site_args ) = @_;
my $build_dir = Path::Tiny->tempdir;
my $deploy_dir = Path::Tiny->tempdir;
$site_args{build_store}{path} = $build_dir;
$site_args{deploy}{path} = $deploy_dir;
if ( !$site_args{apps} ) {
require Statocles::App::Blog;
my $blog = Statocles::App::Blog->new(
store => $share_dir->child( qw( app blog ) ),
url_root => '/blog',
page_size => 2,
);
require Statocles::App::Basic;
my $basic = Statocles::App::Basic->new(
store => $share_dir->child( qw( app basic ) ),
url_root => '/',
);
$site_args{apps} = {
blog => $blog,
basic => $basic,
};
}
return (
build_test_site(
theme => $share_dir->child( 'theme' ),
build_store => delete $site_args{build_store},
deploy => delete $site_args{deploy},
%site_args,
),
$build_dir,
$deploy_dir,
);
}
sub test_constructor {
my ( $class, %args ) = @_;
derp 'Statocles::Test::test_constructor is deprecated and will be removed in v1.000';
my %required = $args{required} ? ( %{ $args{required} } ) : ();
my %defaults = $args{default} ? ( %{ $args{default} } ) : ();
require Test::Builder;
local $Test::Builder::Level = $Test::Builder::Level + 1;
my $tb = Test::Builder->new();
$tb->subtest( $class . ' constructor' => sub {
my $got = $class->new( %required );
my $want = $class;
my $typeof = do {
!defined $got ? 'undefined'
: !ref $got ? 'scalar'
: !Scalar::Util::blessed($got) ? ref $got
: eval { $got->isa($want) } ? $want
: Scalar::Util::blessed($got);
};
$tb->is_eq($typeof, $class, 'constructor works with all required args');
if ( $args{required} ) {
$tb->subtest( 'required attributes' => sub {
for my $key ( keys %required ) {
require Test::Exception;
&Test::Exception::dies_ok(sub {
$class->new(
map {; $_ => $required{ $_ } } grep { $_ ne $key } keys %required,
);
}, $key . ' is required');
}
});
}
if ( $args{default} ) {
$tb->subtest( 'attribute defaults' => sub {
my $obj = $class->new( %required );
for my $key ( keys %defaults ) {
if ( ref $defaults{ $key } eq 'CODE' ) {
local $_ = $obj->$key;
$tb->subtest( "$key default value" => $defaults{ $key } );
}
else {
require Test::Deep;
Test::Deep::cmp_deeply( $obj->$key, $defaults{ $key }, "$key default value" );
}
}
});
}
});
}
sub test_pages {
my ( $site, $app ) = ( shift, shift );
derp 'Statocles::Test::test_pages is deprecated and will be removed in v1.000';
require Test::Builder;
my %opt;
if ( ref $_[0] eq 'HASH' ) {
%opt = %{ +shift };
}
my %page_tests = @_;
local $Test::Builder::Level = $Test::Builder::Level + 1;
my $tb = Test::Builder->new();
my @warnings;
local $SIG{__WARN__} = sub { push @warnings, $_[0] };
my @pages = $app->pages;
$tb->is_eq( scalar @pages, scalar keys %page_tests, 'correct number of pages' );
for my $page ( @pages ) {
$tb->ok( $page->DOES( 'Statocles::Page' ), 'must be a Statocles::Page' );
my $date = $page->date;
my $want = 'DateTime::Moonpig';
my $typeof = do {
!defined $date ? 'undefined'
: !ref $date ? 'scalar'
: !Scalar::Util::blessed($date) ? ref $date
: eval { $date->isa($want) } ? $want
: Scalar::Util::blessed($date);
};
$tb->is_eq( $typeof, $want, 'must set a date' );
if ( !$page_tests{ $page->path } ) {
$tb->ok( 0, "No tests found for page: " . $page->path );
next;
}
my $output;
if ( $page->has_dom ) {
$output = "".$page->dom;
}
else {
$output = $page->render;
# Handle filehandles from render
if ( ref $output eq 'GLOB' ) {
$output = do { local $/; <$output> };
}
# Handle Path::Tiny from render
elsif ( Scalar::Util::blessed( $output ) && $output->isa( 'Path::Tiny' ) ) {
$output = $output->slurp_raw;
}
}
if ( $page->path =~ /[.](?:html|rss|atom)$/ ) {
my $dom = Mojo::DOM->new( $output );
$tb->ok( 0, "Could not parse dom" ) unless $dom;
$tb->subtest( 'html content: ' . $page->path, $page_tests{ $page->path }, $output, $dom );
}
elsif ( $page_tests{ $page->path } ) {
$tb->subtest( 'text content: ' . $page->path, $page_tests{ $page->path }, $output );
}
else {
$tb->ok( 0, "Unknown page: " . $page->path );
}
}
$tb->ok( !@warnings, "no warnings!" ) or $tb->diag( join "\n", @warnings );
}
=sub build_temp_site
my ( $tmpdir, $config_fn, $config ) = build_temp_site( $share_dir );
Build a config file so we can test config loading and still use
temporary directories
=cut
sub build_temp_site {
my ( $share_dir ) = @_;
my $tmp = Path::Tiny->tempdir( CLEANUP => $ENV{STATOCLES_TEST_CLEANUP} // 1 );
dircopy $share_dir->child( qw( app blog ) ), $tmp->child( 'blog' );
dircopy $share_dir->child( 'theme' ), $tmp->child( 'theme' );
$tmp->child( 'build_site' )->mkpath;
$tmp->child( 'deploy_site' )->mkpath;
$tmp->child( 'build_foo' )->mkpath;
$tmp->child( 'deploy_foo' )->mkpath;
my $config = {
theme => {
class => 'Statocles::Theme',
args => {
store => $tmp->child( 'theme' ),
},
},
build => {
class => 'Statocles::Store',
args => {
path => $tmp->child( 'build_site' ),
},
},
deploy => {
class => 'Statocles::Deploy::File',
args => {
path => $tmp->child( 'deploy_site' ),
},
},
blog => {
'class' => 'Statocles::App::Blog',
'args' => {
store => {
'$class' => 'Statocles::Store',
'$args' => {
path => $tmp->child( 'blog' ),
},
},
url_root => '/blog',
},
},
plain => {
'class' => 'Statocles::App::Basic',
'args' => {
store => {
'$class' => 'Statocles::Store',
'$args' => {
path => "$tmp",
},
},
url_root => '/',
},
},
site => {
class => 'Statocles::Site',
args => {
title => 'Site Title',
index => '/blog',
build_store => { '$ref' => 'build' },
deploy => { '$ref' => 'deploy' },
theme => { '$ref' => 'theme' },
apps => {
blog => { '$ref' => 'blog' },
plain => { '$ref' => 'plain' },
},
},
},
build_foo => {
class => 'Statocles::Store',
args => {
path => $tmp->child( 'build_foo' ),
},
},
deploy_foo => {
class => 'Statocles::Deploy::File',
args => {
path => $tmp->child( 'deploy_foo' ),
},
},
site_foo => {
class => 'Statocles::Site',
args => {
title => 'Site Foo',
index => '/blog',
build_store => { '$ref' => 'build_foo' },
deploy => { '$ref' => 'deploy_foo' },
theme => '::default',
apps => {
blog => { '$ref' => 'blog' },
plain => { '$ref' => 'plain' },
},
},
},
};
my $config_fn = $tmp->child( 'site.yml' );
YAML::DumpFile( $config_fn, $config );
return ( $tmp, $config_fn, $config );
}
1;
__END__
=head1 DESCRIPTION
This module provides some common test routines for Statocles tests.