Test-Expander/lib/Test/Expander.pm

259 lines
6.6 KiB
Perl

## no critic (ProhibitStringyEval ProhibitSubroutinePrototypes RequireLocalizedPunctuationVars)
package Test::Expander;
our $VERSION = '1.0.0'; ## no critic (RequireUseStrict, RequireUseWarnings)
use v5.14;
use warnings
FATAL => qw(all),
NONFATAL => qw(deprecated exec internal malloc newline portable recursion);
no warnings qw(experimental);
use Const::Fast;
use File::chdir;
use File::Temp qw(tempdir tempfile);
use Importer;
use Path::Tiny qw(cwd path);
use Scalar::Readonly qw(readonly_on);
use Test::Files;
use Test::Output;
use Test::Warn;
use Test2::Tools::Explain;
use Test2::V0 ();
use Test::Expander::Constants qw(
$ANY_EXTENSION
$CLASS_HIERARCHY_LEVEL
$ERROR_WAS
$FALSE
$EXCEPTION_PREFIX
$INVALID_ENV_ENTRY
$INVALID_VALUE
$NEW_FAILED $NEW_SUCCEEDED
$REPLACEMENT
$REQUIRE_DESCRIPTION $REQUIRE_IMPLEMENTATION
$SEARCH_PATTERN
$TOP_DIR_IN_PATH
$TRUE
$UNKNOWN_OPTION
$USE_DESCRIPTION $USE_IMPLEMENTATION
$VERSION_NUMBER
);
readonly_on($VERSION);
our ($CLASS, $METHOD, $METHOD_REF, $TEMP_DIR, $TEMP_FILE);
our @EXPORT = (
@{Const::Fast::EXPORT},
@{Test::Files::EXPORT},
@{Test::Output::EXPORT},
@{Test::Warn::EXPORT},
@{Test2::Tools::Explain::EXPORT},
@{Test2::V0::EXPORT},
qw(tempdir tempfile),
qw(cwd path),
qw($CLASS $METHOD $METHOD_REF $TEMP_DIR $TEMP_FILE),
qw(BAIL_OUT dies_ok is_deeply lives_ok new_ok require_ok throws_ok use_ok),
);
*BAIL_OUT = \&bail_out; # Explicit "sub BAIL_OUT" would be untestable
sub dies_ok (&;$) {
my ($coderef, $description) = @_;
eval { $coderef->() };
return ok($@, $description);
}
sub import {
my ($class, @exports) = @_;
my %options;
while (my $optionName = shift(@exports)) {
given ($optionName) {
when ('-tempdir') {
my $optionValue = shift(@exports);
die(sprintf($INVALID_VALUE, $optionName, $optionValue)) if ref($optionValue) ne 'HASH';
$TEMP_DIR = tempdir(CLEANUP => 1, %$optionValue);
}
when ('-tempfile') {
my $optionValue = shift(@exports);
die(sprintf($INVALID_VALUE, $optionName, $optionValue)) if ref($optionValue) ne 'HASH';
my $fileHandle;
($fileHandle, $TEMP_FILE) = tempfile(UNLINK => 1, %$optionValue);
}
when (/^-\w/) {
$options{$optionName} = shift(@exports);
}
default {
die(sprintf($UNKNOWN_OPTION, $optionName, shift(@exports) // ''));
}
}
}
my $testFile = path((caller(2))[1]) =~ s{^/}{}r; ## no critic (ProhibitMagicNumbers)
my ($testRoot) = $testFile =~ $TOP_DIR_IN_PATH;
unless (exists($options{-target})) {
my $testee = path($testFile)->relative($testRoot)->parent;
$options{-target} = join('::', split(qr{/}, $testee))
if grep { path($_)->child($testee . '.pm')->is_file } @INC;
}
$METHOD = path($testFile)->basename($ANY_EXTENSION);
my $startDir = cwd();
_setEnv($METHOD, $options{-target}, $testFile);
Test2::V0->import(%options);
$METHOD_REF = '-target' ~~ %options ? $CLASS->can($METHOD) : undef;
$METHOD = undef unless($METHOD_REF);
readonly_on($CLASS) if $CLASS;
readonly_on($METHOD) if $METHOD;
readonly_on($METHOD_REF) if $METHOD_REF;
readonly_on($TEMP_DIR) if $TEMP_DIR;
readonly_on($TEMP_FILE) if $TEMP_FILE;
Importer->import_into($class, scalar(caller), ());
return;
}
sub is_deeply ($$;$@) {
my ($got, $expected, $title) = @_;
return is($got, $expected, $title);
}
sub lives_ok (&;$) {
my ($coderef, $description) = @_;
eval { $coderef->() };
return ok(!$@, $description);
}
sub new_ok {
my ($class, $args) = @_;
$args ||= [];
my $obj = eval { $class->new(@$args) };
ok(!$@, _newTestMessage($class));
return $obj;
}
sub require_ok {
my ($module) = @_;
my $package = caller;
my $requireResult = eval(sprintf($REQUIRE_IMPLEMENTATION, $package, $module));
ok($requireResult, sprintf($REQUIRE_DESCRIPTION, $module, _error()));
return $requireResult;
}
sub throws_ok (&$;$) {
my ($coderef, $expecting, $description) = @_;
eval { $coderef->() };
return like($@, qr/$expecting/, $description);
}
sub use_ok ($;@) {
my ($module, @imports) = @_;
my ($package, $filename, $line) = caller(0);
$filename =~ y/\n\r/_/; # taken over from Test::More
my $requireResult = eval(sprintf($USE_IMPLEMENTATION, $package, $module, _useImports(\@imports)));
ok(
$requireResult,
sprintf($USE_DESCRIPTION, $module, _error($SEARCH_PATTERN, sprintf($REPLACEMENT, $filename, $line)))
);
return $requireResult;
}
sub _error {
my ($searchString, $replacementString) = @_;
return '' if $@ eq '';
my $error = $ERROR_WAS . $@ =~ s/\n$//mr;
$error =~ s/$searchString/$replacementString/m if defined($searchString);
return $error;
}
sub _newTestMessage {
my ($class) = @_;
return $@ ? sprintf($NEW_FAILED, $class, _error()) : sprintf($NEW_SUCCEEDED, $class, $class);
}
sub _readEnvFile {
my ($envFile) = @_;
my @lines = path($envFile)->lines({ chomp => 1 });
my %env;
while (my ($index, $line) = each(@lines)) {
next unless $line =~ /^ (?<name> \w+) \s* = \s* (?<value> \S .*)/x;
$env{$+{name}} = eval($+{value});
die(sprintf($INVALID_ENV_ENTRY, $index, $envFile, $line, $@)) if $@;
}
return \%env;
}
sub _setEnv {
my ($method, $class, $testFile) = @_;
my $envFound = $FALSE;
my $newEnv = {};
{
local $CWD = $testFile =~ s{/.*}{}r; ## no critic (ProhibitLocalVars)
($envFound, $newEnv) = _setEnvHierarchically($class, $envFound, $newEnv);
}
my $envFile = $testFile =~ s/$ANY_EXTENSION/.env/r;
if (path($envFile)->is_file) {
$envFound = $TRUE unless $envFound;
my $methodEnv = _readEnvFile($envFile);
@$newEnv{keys(%$methodEnv)} = values(%$methodEnv)
}
%ENV = %$newEnv if $envFound;
return;
}
sub _setEnvHierarchically {
my ($class, $envFound, $newEnv) = @_;
return ($envFound, $newEnv) unless $class;
my $classTopLevel;
($classTopLevel, $class) = $class =~ $CLASS_HIERARCHY_LEVEL;
return ($FALSE, {}) unless path($classTopLevel)->is_dir;
my $envFile = $classTopLevel . '.env';
if (path($envFile)->is_file) {
$envFound = $TRUE unless $envFound;
$newEnv = { %$newEnv, %{ _readEnvFile($envFile) } };
}
local $CWD = $classTopLevel; ## no critic (ProhibitLocalVars)
return _setEnvHierarchically($class, $envFound, $newEnv);
}
sub _useImports {
my ($imports) = @_;
return @$imports == 1 && $imports->[0] =~ $VERSION_NUMBER ? ' ' . $imports->[0] : '';
}
1;