commit 717e9581ea5005c942507273d15f373de3767d27 Author: Jurij Fajnberg Date: Mon Nov 1 17:16:06 2021 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c6ff92 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +Makefile +build diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..69ee37d --- /dev/null +++ b/Readme.md @@ -0,0 +1,196 @@ +# NAME + +**Test::Expander** - Expansion of test functionalities that appear to be frequently used while testing. + +# SYNOPSIS + +```perl + # Tries to determine both class and method to be tested automatically, + # does not create any temporary directory: + use Test::Expander; + + # Tries to determine both class and method to be tested automatically, + # does not create any temporary directory, + # passes the option '-srand' to Test::V0 changing the random seed to the current time in seconds: + use Test::Expander -srand => time; + + # Tries to determine only the method to be tested automatically, class is explicitly supplied, + # a temporary directory is created with name corresponing to the template supplied: + use Test::Expander -target => 'My::Class', -tempdir => { TEMPLATE => 'my_dir.XXXXXXXX' }; +``` + +# DESCRIPTION + +**Test::Expander** combines all advanced possibilities provided by [Test2::V0](https://metacpan.org/pod/Test2::V0) +with some specific functions available in the older module [Test::More](https://metacpan.org/pod/Test::More) only +(which allows a smooth migration from [Test::More](https://metacpan.org/pod/Test::More)-based tests to +[Test2::V0](https://metacpan.org/pod/Test2::V0)-based ones) and handy functions from some other modules +often used in test suites. + +Furthermore, this module provides a recognition of class to be tested (see variable **$CLASS** below) so that +in contrast to [Test2::V0](https://metacpan.org/pod/Test2::V0) you do not need to specify this explicitly +if the path to the test file is in accordance with the name of class to be tested. + +A similar recognition is provided in regard to the method / subroutine to be tested +(see variables **$METHOD** and **METHOD\_REF** below) if the base name (without extension) of test file is +identical with the name of this method / subroutine. + +Finally, a configurable setting of specific environment variables is provided so that +there is no need to hard-code this in the test itself. + +For the time being the following options are accepted by **Test::Expander**: + +- Options specific for this module only: + - **-target** - identical with the same-named option of [Test2::V0](https://metacpan.org/pod/Test2::V0) and + has the same purpose namely the explicit definition of class to be tested as a value of this option; + - **-tempdir** - activates creation of a temporary directory by the function **tempdir** provided by + [File::Temp::tempdir](https://metacpan.org/pod/File::Temp). + - **-tempfile** - activates creation of a temporary file by the method **tempfile** provided by + [File::Temp::tempfile](https://metacpan.org/pod/File::Temp). +- All other valid options (i.e. arguments starting with the dash sign **-**) are forwarded to +[Test2::V0](https://metacpan.org/pod/Test2::V0) along with their values. +- If an argument cannot be recognized as an option, an exception is raised. + +The proper application of **Test::Expander** implies that is is used as the very first in your unit test. + +The only exception currently known is the case, when some actions performed on the module level +(e.g. determination of constants) base on results of other actions (e.g. mocking of built-ins). + +To explain this let us assume that your unit test file should mock the built-in **close** +to verify if the testee properly reacts both on its success and failure. +For this purpose a reasonable implementation might look as follows: + +```perl + my $closeSuccess = 1; + BEGIN { + *CORE::GLOBAL::close = sub (*) { return $closeSuccess ? CORE::close($_[0]) : 0 }; + } + + use Test::Expander; +``` + +Furthermore, the automated recognition of name of class to be tested can only work properly +if the test file is located in the corresponding subdirectory of **t**, or **xt**, or any other folder +containing a bunch of test files. +For instance, if the class to be tested is _Foo::Bar::Baz_, then the folder with test files +related to this class should be **t/**_Foo_**/**_Bar_**/**_Baz_ or **xt/**_Foo_**/**_Bar_**/**_Baz_ +(the name of the top-level directory in this relative name - **t**, or **xt**, or **my\_test** is not important) - +otherwise the module name cannot be put into the exported variable **$CLASS** and, if you want to use this variable, +should be supplied as a value of the option **-target**: + +```perl + use Test::Expander -target => 'Foo::Bar::Baz'; +``` + +What is more, the automated recognition of name of method / subroutine to be tested can only work properly +if the base name of the test file without extension (usually **.t**) is equal to the method / subroutine +name. In other words, this recognition only works if the file containing the class mentioned above exists and +if this class has the method / subroutine with the same name as the test file base name without extension. +If this is the case, the exported variables **$METHOD** and **$METHOD\_REF** contain the name of method / subroutine +to be tested and its reference, correspondingly, otherwise both variables are undefined. + +Finally, **Test::Expander** supports testing inside of a clean environment containing only some clearly +specified environment variables required for the particular test. +Names and values of these environment variables should be configured in files, +which names are identical with pathes to single class levels or method to be tested, +and the extension is always **.env**. +For instance, if the test file name is **t/Foo/Bar/Baz/myMethod.t**, the following approach is applied: + +- if the file **t/Foo.env** exists, its content is used for the initialization of test environment, +- if the file **t/Foo/Bar.env** exists, its content is used either for extension of test environment +initialized in the previous step or for its initialization if the file **t/Foo.env** does not exist, +- if the file **t/Foo/Bar/Baz.env** exists, its content is used either for extension of test +environment initialized in one of the previous steps or for its initialization if neither the file **t/Foo.env** nor +the file **t/Foo/Bar.env** exists, +- if the file **t/Foo/Bar/Baz/myMethod.env** exists, its content will be used either for extension of test environment +initialized in one of the previous steps or for its initialization if no one of **.env** files mentioned above exists. + +If the **.env** files existing on different levels have identical names of environment variables, +the priority is the higher the later they have been detected. +I.e. **VAR = 'VALUE0'** in **t/Foo/Bar/Baz/myMethod.env** overwrites **VAR = 'VALUE1'** in **t/Foo/Bar/Baz.env**. + +If no one of these **.env** files exists, the environment will not be changed by **Test::Expander** +during the execution of **t/Foo/Bar/Baz/myMethod.t**. + +An environment configuration file (**.env** file) is a line-based text file, +which content is interpreted as follows: + +- if such files don't exist, the **%ENV** hash remains unchanged; +- otherwise, if at least one of such files exists, the **%ENV** gets emptied (without localization) and + - lines not matching the RegEx **/^\\w+\\s\\\*=\\s\\\*\\S/** (some alphanumeric characters representing a name of + environment variable, optional blanks, the equal sign, again optional blanks and at least one non-blank + character representing the first sign of environment variable value) are skipped; + - in all other lines the value of the environment variable is everything from the first non-blank + character after the equal sign until end of the line; + - the value of the environment variable is evaluated by the [string eval](https://perldoc.perl.org/functions/eval) + so that + - constant values must be quoted; + - variables and subroutines must not be quoted: + + NAME_CONST = 'VALUE' + NAME_VAR = $KNIB::App::MyApp::Constants::ABC + NAME_FUNC = join(' ', $KNIB::App::MyApp::Constants::DEF) + +Another feature frequently applied inside of test suites is creation of a temporary directory / file used as an +isolated container for some testing actions. +The module options **-tempdir** and **-tempfile** fully synactically compatible with +[File::Temp::tempdir](https://metacpan.org/pod/File::Temp#FUNCTIONS) / +[File::Temp::tempfile](https://metacpan.org/pod/File::Temp#FUNCTIONS) make sure that such temporary +directory / file are created after **use Test::Expander** and their names are stored in the variables +**$TEMP\_DIR** / **$TEMP\_FILE**, correspondingly. +Both temporary directory and file are removed by default after execution. + +All functions provided by this module are exported by default. These and the exported variables are: + +- all functions exported by default from [Test2::V0](https://metacpan.org/pod/Test2::V0), +- all functions exported by default from [Test::Files](https://metacpan.org/pod/Test::Files), +- all functions exported by default from [Test::Output](https://metacpan.org/pod/Test::Output), +- all functions exported by default from [Test::Warn](https://metacpan.org/pod/Test::Warn), +- some functions exported by default from [Test::More](https://metacpan.org/pod/Test::More) +and often used in older tests but not supported by [Test2::V0](https://metacpan.org/pod/Test2::V0): + - BAIL\_OUT, + - is\_deeply, + - new\_ok, + - require\_ok, + - use\_ok, +- some functions exported by default from [Test::Exception](https://metacpan.org/pod/Test::Exception) +and often used in older tests but not supported by [Test2::V0](https://metacpan.org/pod/Test2::V0): + - dies\_ok, + - explain, + - lives\_ok, + - throws\_ok, +- function exported by default from [Const::Fast](https://metacpan.org/pod/Const::Fast): + - const, +- some functions exported by request from [File::Temp](https://metacpan.org/pod/File::Temp): + - tempdir, + - tempfile, +- some functions exported by request from [Path::Tiny](https://metacpan.org/pod/Path::Tiny): + - cwd, + - path, +- variable **$CLASS** containing the name of class to be tested, +- variable **$METHOD** containing the name of method to be tested, +- variable **$METHOD\_REF** containing the reference to subroutine to be tested. +- variable **$TEMP\_DIR** containing the name of a temporary directory created at compile time +if the option **-tempdir** was supplied. +- variable **$TEMP\_FILE** containing the name of a temporary file created at compile time +if the option **-tempfile** was supplied. + +All variables mentioned above are read-only if they are defined after **use Test::Expander ...**. + +# AUTHOR + +Jurij Fajnberg, <fajnbergj at gmail.com> + +# BUGS + +Please report any bugs or feature requests through the web interface at +[https://github.com/jsf116/Test-Expander/issues](https://github.com/jsf116/Test-Expander/issues). + +# COPYRIGHT AND LICENSE + +## LICENSE AND COPYRIGHT + +Copyright (c) 2021 Jurij Fajnberg + +This program is free software; you can redistribute it and/or modify it under the same terms +as the Perl 5 programming language system itself. diff --git a/lib/.perlcriticrc b/lib/.perlcriticrc new file mode 100644 index 0000000..c45eac8 --- /dev/null +++ b/lib/.perlcriticrc @@ -0,0 +1,379 @@ +# You may disable specific policies appending the following annotation +# +# ## no critic (..., ...) +# +# to the corresponding code line. To direct perlcritic to ignore the +# "## no critic" annotations, use the --force option. + +# Policies shipped with Perl::Critic 1.125 were considered for the below +# defintion of the new policy theme "knib". + +severity = brutal +theme = knib +verbose = %f: %m at line %l, column %c. (Policy: %p)\n + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitLvalueSubstr] +add_themes = knib + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitStringyEval] +add_themes = knib +allow_includes = 1 + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitStringySplit] +add_themes = knib + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitUniversalCan] +add_themes = knib + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitUniversalIsa] +add_themes = knib + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitUselessTopic] +# KNOWN BUGS: This policy flags a false positive on reverse() called in list +# context, since reverse() in list context does not assume $_. +add_themes = knib + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitVoidGrep] +add_themes = knib + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitVoidMap] +add_themes = knib + +[Perl::Critic::Policy::BuiltinFunctions::RequireBlockGrep] +add_themes = knib + +[Perl::Critic::Policy::BuiltinFunctions::RequireBlockMap] +add_themes = knib + +# 14.01.2016 policy disabled after a discussion with the team +#[BuiltinFunctions::RequireGlobFunction] +#add_themes = knib + +[Perl::Critic::Policy::BuiltinFunctions::RequireSimpleSortBlock] +add_themes = knib + +[Perl::Critic::Policy::ClassHierarchies::ProhibitExplicitISA] +# Note: Some people prefer parent over base. +add_themes = knib + +[Perl::Critic::Policy::CodeLayout::ProhibitHardTabs] +add_themes = knib +allow_leading_tabs = 0 + +# 14.01.2016 policy disabled after a discussion with the team +#[Perl::Critic::Policy::CodeLayout::ProhibitQuotedWordLists] +#add_themes = knib +#min_elements = 1 +#strict = 1 + +[Perl::Critic::Policy::CodeLayout::ProhibitTrailingWhitespace] +add_themes = knib + +[Perl::Critic::Policy::CodeLayout::RequireConsistentNewlines] +add_themes = knib + +# 14.01.2016 policy disabled after a discussion with the team +#[Perl::Critic::Policy::CodeLayout::RequireTrailingCommas] +#add_themes = knib + +[Perl::Critic::Policy::ControlStructures::ProhibitCStyleForLoops] +add_themes = knib + +[Perl::Critic::Policy::ControlStructures::ProhibitCascadingIfElse] +add_themes = knib +max_elsif = 1 + +[Perl::Critic::Policy::ControlStructures::ProhibitDeepNests] +# Martin Fowler's book "Refactoring: Improving The Design of Existing Code". +add_themes = knib +max_nests = 5 + +[Perl::Critic::Policy::ControlStructures::ProhibitLabelsWithSpecialBlockNames] +add_themes = knib + +[Perl::Critic::Policy::ControlStructures::ProhibitMutatingListFunctions] +# Read the LIMITATIONS that this policy has. +add_themes = knib + +[Perl::Critic::Policy::ControlStructures::ProhibitNegativeExpressionsInUnlessAndUntilConditions] +add_themes = knib + +[Perl::Critic::Policy::ControlStructures::ProhibitUnreachableCode] +add_themes = knib + +# Available With Perl::Critic v1.126 +#[Perl::Critic::Policy::ControlStructures::ProhibitYadaOperator] +#add_themes = knib + +[Perl::Critic::Policy::Documentation::PodSpelling] +add_themes = +# "spell" is the spell checker avalable on our AIX system. The default spell +# checker "aspell" was not available. +spell_command = spell +#stop_words = ... +stop_words_file = PodSpelling_stop_words.txt + +[Perl::Critic::Policy::Documentation::RequirePackageMatchesPodName] +add_themes = knib + +#[Perl::Critic::Policy::Documentation::RequirePodSections] +#add_themes = knib + +# 14.01.2016 policy disabled after a discussion with the team +#[Perl::Critic::Policy::ErrorHandling::RequireCheckingReturnValueOfEval] +#add_themes = knib + +[Perl::Critic::Policy::InputOutput::ProhibitBacktickOperators] +add_themes = knib +# 14.01.2016 policy configuration changed after a discussion with the team +only_in_void_context = 1 + +[Perl::Critic::Policy::InputOutput::ProhibitBarewordFileHandles] +add_themes = knib + +[Perl::Critic::Policy::InputOutput::ProhibitExplicitStdin] +add_themes = knib + +[Perl::Critic::Policy::InputOutput::ProhibitInteractiveTest] +add_themes = knib + +[Perl::Critic::Policy::InputOutput::ProhibitJoinedReadline] +add_themes = knib + +[Perl::Critic::Policy::InputOutput::ProhibitReadlineInForLoop] +add_themes = knib + +[Perl::Critic::Policy::InputOutput::ProhibitTwoArgOpen] +add_themes = knib + +[Perl::Critic::Policy::InputOutput::RequireBriefOpen] +# http://www.perlmonks.org/?node_id=1134785 +add_themes = knib +lines = 9 + +[Perl::Critic::Policy::InputOutput::RequireCheckedSyscalls] +# Covers the policies +# Perl::Critic::Policy::InputOutput::RequireCheckedClose and +# Perl::Critic::Policy::InputOutput::RequireCheckedOpen +add_themes = knib +exclude_functions = print say +functions = :builtins + +[Perl::Critic::Policy::InputOutput::RequireEncodingWithUTF8Layer] +add_themes = knib + +[Perl::Critic::Policy::Miscellanea::ProhibitUnrestrictedNoCritic] +add_themes = knib + +[Perl::Critic::Policy::Miscellanea::ProhibitUselessNoCritic] +add_themes = knib + +[Perl::Critic::Policy::Modules::ProhibitAutomaticExportation] +add_themes = knib + +[Perl::Critic::Policy::Modules::ProhibitConditionalUseStatements] +add_themes = knib + +[Perl::Critic::Policy::Modules::ProhibitEvilModules] +add_themes = knib +modules = Class::ISA Error Pod::Plainer Shell Switch + +[Perl::Critic::Policy::Modules::ProhibitExcessMainComplexity] +# http://en.wikipedia.org/wiki/Cyclomatic_complexity +add_themes = knib +max_mccabe = 20 + +[Perl::Critic::Policy::Modules::ProhibitMultiplePackages] +add_themes = knib + +[Perl::Critic::Policy::Modules::RequireBarewordIncludes] +add_themes = knib + +[Perl::Critic::Policy::Modules::RequireEndWithOne] +add_themes = knib + +[Perl::Critic::Policy::Modules::RequireExplicitPackage] +add_themes = knib +allow_import_of = utf8 +exempt_scripts = 0 + +[Perl::Critic::Policy::Modules::RequireFilenameMatchesPackage] +add_themes = knib + +[Perl::Critic::Policy::Modules::RequireNoMatchVarsWithUseEnglish] +add_themes = knib + +[Perl::Critic::Policy::Modules::RequireVersionVar] +# Read the TO DO section of this policy and think about its implication. +add_themes = knib + +# Perl::Critic::Policy::NamingConventions::Capitalization +# It takes some time to configure this policy! + +[Perl::Critic::Policy::Objects::ProhibitIndirectSyntax] +add_themes = knib +# The new() subroutine is configured by default; any additional forbid values +# are in addition to new(). +forbid = create destroy + +[Perl::Critic::Policy::RegularExpressions::ProhibitFixedStringMatches] +add_themes = knib + +[Perl::Critic::Policy::RegularExpressions::ProhibitSingleCharAlternation] +add_themes = knib + +[Perl::Critic::Policy::RegularExpressions::ProhibitUnusedCapture] +add_themes = knib + +# 14.01.2016 policy still enabled after a discussion with the team +[Perl::Critic::Policy::RegularExpressions::ProhibitUnusualDelimiters] +add_themes = knib +allow_all_brackets = 0 + +[Perl::Critic::Policy::RegularExpressions::ProhibitUselessTopic] +add_themes = knib + +[Perl::Critic::Policy::RegularExpressions::RequireBracesForMultiline] +add_themes = knib +allow_all_brackets = 0 + +[Perl::Critic::Policy::Subroutines::ProhibitAmpersandSigils] +add_themes = knib + +[Perl::Critic::Policy::Subroutines::ProhibitBuiltinHomonyms] +# Read the CAVEATS. +add_themes = knib + +[Perl::Critic::Policy::Subroutines::ProhibitExcessComplexity] +# http://en.wikipedia.org/wiki/Cyclomatic_complexity +add_themes = knib +max_mccabe = 20 + +[Perl::Critic::Policy::Subroutines::ProhibitExplicitReturnUndef] +# http://perlmonks.org/index.pl?node_id=741847 +add_themes = knib + +[Perl::Critic::Policy::Subroutines::ProhibitManyArgs] +add_themes = knib +max_arguments = 5 + +[Perl::Critic::Policy::Subroutines::ProhibitNestedSubs] +add_themes = knib + +[Perl::Critic::Policy::Subroutines::ProhibitReturnSort] +# KNOWN BUGS: This Policy is not sensitive to the wantarray() function. +add_themes = knib + +[Perl::Critic::Policy::Subroutines::ProhibitSubroutinePrototypes] +add_themes = knib + +[Perl::Critic::Policy::Subroutines::ProhibitUnusedPrivateSubroutines] +add_themes = knib + +[Perl::Critic::Policy::Subroutines::ProtectPrivateSubs] +add_themes = knib + +#[Perl::Critic::Policy::Subroutines::RequireArgUnpacking] +#add_themes = knib +#allow_delegation_to = SUPER:: NEXT:: +#allow_subscripts = 0 +#short_subroutine_statements = 0 + +[Perl::Critic::Policy::Subroutines::RequireFinalReturn] +add_themes = knib +terminal_funcs = return carp croak die exec exit goto throw + +[Perl::Critic::Policy::TestingAndDebugging::ProhibitNoStrict] +add_themes = knib +allow = refs + +[Perl::Critic::Policy::TestingAndDebugging::ProhibitNoWarnings] +add_themes = knib +allow_with_category_restriction = 1 + +[Perl::Critic::Policy::TestingAndDebugging::ProhibitProlongedStrictureOverride] +add_themes = knib +statements = 3 + +# The following policy seems to have a bug for the ok() test. +#[Perl::Critic::Policy::TestingAndDebugging::RequireTestLabels] +#add_themes = knib +#modules = Test::Exception Test::More + +[Perl::Critic::Policy::TestingAndDebugging::RequireUseStrict] +add_themes = knib + +[Perl::Critic::Policy::TestingAndDebugging::RequireUseWarnings] +add_themes = knib + +[Perl::Critic::Policy::ValuesAndExpressions::ProhibitInterpolationOfLiterals] +add_themes = knib +allow_if_string_contains_single_quote = 1 + +[Perl::Critic::Policy::ValuesAndExpressions::ProhibitLeadingZeros] +add_themes = knib + +[Perl::Critic::Policy::ValuesAndExpressions::ProhibitLongChainsOfMethodCalls] +add_themes = knib +max_chain_length = 3 + +[Perl::Critic::Policy::ValuesAndExpressions::ProhibitMagicNumbers] +# Not yet configured completely. +add_themes = knib +# 14.01.2016 2 is considered a magic number as well after a discussion with the team +allowed_values = -1 0 1 + +# 11.02.2016 policy disabled after a discussion with the team +#[Perl::Critic::Policy::ValuesAndExpressions::ProhibitMismatchedOperators] +#add_themes = knib + +[Perl::Critic::Policy::ValuesAndExpressions::ProhibitMixedBooleanOperators] +add_themes = knib + +[Perl::Critic::Policy::ValuesAndExpressions::ProhibitQuotesAsQuotelikeOperatorDelimiters] +add_themes = knib +back_quote_allowed_operators = +double_quote_allowed_operators = +single_quote_allowed_operators = + +[Perl::Critic::Policy::ValuesAndExpressions::ProhibitSpecialLiteralHeredocTerminator] +add_themes = knib + +[Perl::Critic::Policy::ValuesAndExpressions::RequireConstantVersion] +add_themes = +allow_version_without_use_on_same_line = 1 + +[Perl::Critic::Policy::ValuesAndExpressions::RequireNumberSeparators] +add_themes = knib +min_value = 10000 + +[Perl::Critic::Policy::ValuesAndExpressions::RequireQuotedHeredocTerminator] +add_themes = knib + +[Perl::Critic::Policy::ValuesAndExpressions::RequireUpperCaseHeredocTerminator] +add_themes = knib + +[Perl::Critic::Policy::Variables::ProhibitConditionalDeclarations] +add_themes = knib + +[Perl::Critic::Policy::Variables::ProhibitLocalVars] +add_themes = knib + +[Perl::Critic::Policy::Variables::ProhibitMatchVars] +add_themes = knib + +[Perl::Critic::Policy::Variables::ProhibitUnusedVariables] +add_themes = knib + +[Perl::Critic::Policy::Variables::RequireInitializationForLocalVars] +add_themes = knib + +[Perl::Critic::Policy::Variables::RequireLexicalLoopIterators] +add_themes = knib + +[Perl::Critic::Policy::Variables::RequireLocalizedPunctuationVars] +add_themes = knib +allow = + +[Perl::Critic::Policy::Variables::RequireNegativeIndices] +add_themes = knib diff --git a/lib/Test/Expander.pm b/lib/Test/Expander.pm new file mode 100644 index 0000000..9ae9f4e --- /dev/null +++ b/lib/Test/Expander.pm @@ -0,0 +1,258 @@ +## 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 =~ /^ (? \w+) \s* = \s* (? \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; diff --git a/lib/Test/Expander.pod b/lib/Test/Expander.pod new file mode 100644 index 0000000..1ec53e4 --- /dev/null +++ b/lib/Test/Expander.pod @@ -0,0 +1,373 @@ +=pod + +=head1 NAME + +B - Expansion of test functionalities that appear to be frequently used while testing. + +=head1 SYNOPSIS + + # Tries to determine both class and method to be tested automatically, + # does not create any temporary directory: + use Test::Expander; + + # Tries to determine both class and method to be tested automatically, + # does not create any temporary directory, + # passes the option '-srand' to Test::V0 changing the random seed to the current time in seconds: + use Test::Expander -srand => time; + + # Tries to determine only the method to be tested automatically, class is explicitly supplied, + # a temporary directory is created with name corresponing to the template supplied: + use Test::Expander -target => 'My::Class', -tempdir => { TEMPLATE => 'my_dir.XXXXXXXX' }; + +=head1 DESCRIPTION + +B combines all advanced possibilities provided by L +with some specific functions available in the older module L only +(which allows a smooth migration from L-based tests to +L-based ones) and handy functions from some other modules +often used in test suites. + +Furthermore, this module provides a recognition of class to be tested (see variable B<$CLASS> below) so that +in contrast to L you do not need to specify this explicitly +if the path to the test file is in accordance with the name of class to be tested. + +A similar recognition is provided in regard to the method / subroutine to be tested +(see variables B<$METHOD> and B below) if the base name (without extension) of test file is +identical with the name of this method / subroutine. + +Finally, a configurable setting of specific environment variables is provided so that +there is no need to hard-code this in the test itself. + +For the time being the following options are accepted by B: + +=over 2 + +=item + +Options specific for this module only: + +=over 2 + +=item + +B<-target> - identical with the same-named option of L and +has the same purpose namely the explicit definition of class to be tested as a value of this option; + +=item + +B<-tempdir> - activates creation of a temporary directory by the function B provided by +L. + +=item + +B<-tempfile> - activates creation of a temporary file by the method B provided by +L. + +=back + +=item + +All other valid options (i.e. arguments starting with the dash sign B<->) are forwarded to +L along with their values. + +=item + +If an argument cannot be recognized as an option, an exception is raised. + +=back + +The proper application of B implies that is is used as the very first in your unit test. + +The only exception currently known is the case, when some actions performed on the module level +(e.g. determination of constants) base on results of other actions (e.g. mocking of built-ins). + +To explain this let us assume that your unit test file should mock the built-in B +to verify if the testee properly reacts both on its success and failure. +For this purpose a reasonable implementation might look as follows: + + my $closeSuccess = 1; + BEGIN { + *CORE::GLOBAL::close = sub (*) { return $closeSuccess ? CORE::close($_[0]) : 0 }; + } + + use Test::Expander; + +Furthermore, the automated recognition of name of class to be tested can only work properly +if the test file is located in the corresponding subdirectory of B, or B, or any other folder +containing a bunch of test files. +For instance, if the class to be tested is I, then the folder with test files +related to this class should be BIBIBI or BIBIBI +(the name of the top-level directory in this relative name - B, or B, or B is not important) - +otherwise the module name cannot be put into the exported variable B<$CLASS> and, if you want to use this variable, +should be supplied as a value of the option B<-target>: + + use Test::Expander -target => 'Foo::Bar::Baz'; + +What is more, the automated recognition of name of method / subroutine to be tested can only work properly +if the base name of the test file without extension (usually B<.t>) is equal to the method / subroutine +name. In other words, this recognition only works if the file containing the class mentioned above exists and +if this class has the method / subroutine with the same name as the test file base name without extension. +If this is the case, the exported variables B<$METHOD> and B<$METHOD_REF> contain the name of method / subroutine +to be tested and its reference, correspondingly, otherwise both variables are undefined. + +Finally, B supports testing inside of a clean environment containing only some clearly +specified environment variables required for the particular test. +Names and values of these environment variables should be configured in files, +which names are identical with pathes to single class levels or method to be tested, +and the extension is always B<.env>. +For instance, if the test file name is B, the following approach is applied: + +=over 2 + +=item + +if the file B exists, its content is used for the initialization of test environment, + +=item + +if the file B exists, its content is used either for extension of test environment +initialized in the previous step or for its initialization if the file B does not exist, + +=item + +if the file B exists, its content is used either for extension of test +environment initialized in one of the previous steps or for its initialization if neither the file B nor +the file B exists, + +=item + +if the file B exists, its content will be used either for extension of test environment +initialized in one of the previous steps or for its initialization if no one of B<.env> files mentioned above exists. + +=back + +If the B<.env> files existing on different levels have identical names of environment variables, +the priority is the higher the later they have been detected. +I.e. B in B overwrites B in B. + +If no one of these B<.env> files exists, the environment will not be changed by B +during the execution of B. + +An environment configuration file (B<.env> file) is a line-based text file, +which content is interpreted as follows: + +=over 2 + +=item + +if such files don't exist, the B<%ENV> hash remains unchanged; + +=item + +otherwise, if at least one of such files exists, the B<%ENV> gets emptied (without localization) and + +=over 2 + +=item + +lines not matching the RegEx B (some alphanumeric characters representing a name of +environment variable, optional blanks, the equal sign, again optional blanks and at least one non-blank +character representing the first sign of environment variable value) are skipped; + +=item + +in all other lines the value of the environment variable is everything from the first non-blank +character after the equal sign until end of the line; + +=item + +the value of the environment variable is evaluated by the L +so that + +=over 2 + +=item + +constant values must be quoted; + +=item + +variables and subroutines must not be quoted: + + NAME_CONST = 'VALUE' + NAME_VAR = $KNIB::App::MyApp::Constants::ABC + NAME_FUNC = join(' ', $KNIB::App::MyApp::Constants::DEF) + +=back + +=back + +=back + +Another feature frequently applied inside of test suites is creation of a temporary directory / file used as an +isolated container for some testing actions. +The module options B<-tempdir> and B<-tempfile> fully synactically compatible with +L / +L make sure that such temporary +directory / file are created after B and their names are stored in the variables +B<$TEMP_DIR> / B<$TEMP_FILE>, correspondingly. +Both temporary directory and file are removed by default after execution. + +All functions provided by this module are exported by default. These and the exported variables are: + +=over 2 + +=item + +all functions exported by default from L, + +=item + +all functions exported by default from L, + +=item + +all functions exported by default from L, + +=item + +all functions exported by default from L, + +=item + +some functions exported by default from L +and often used in older tests but not supported by L: + +=over 2 + +=item + +BAIL_OUT, + +=item + +is_deeply, + +=item + +new_ok, + +=item + +require_ok, + +=item + +use_ok, + +=back + +=item + +some functions exported by default from L +and often used in older tests but not supported by L: + +=over 2 + +=item + +dies_ok, + +=item + +explain, + +=item + +lives_ok, + +=item + +throws_ok, + +=back + +=item + +function exported by default from L: + +=over 2 + +=item + +const, + +=back + +=item + +some functions exported by request from L: + +=over 2 + +=item + +tempdir, + +=item + +tempfile, + +=back + +=item + +some functions exported by request from L: + +=over 2 + +=item + +cwd, + +=item + +path, + +=back + +=item + +variable B<$CLASS> containing the name of class to be tested, + +=item + +variable B<$METHOD> containing the name of method to be tested, + +=item + +variable B<$METHOD_REF> containing the reference to subroutine to be tested. + +=item + +variable B<$TEMP_DIR> containing the name of a temporary directory created at compile time +if the option B<-tempdir> was supplied. + +=item + +variable B<$TEMP_FILE> containing the name of a temporary file created at compile time +if the option B<-tempfile> was supplied. + +=back + +All variables mentioned above are read-only if they are defined after B. + +=head1 AUTHOR + +Jurij Fajnberg, + +=head1 BUGS + +Please report any bugs or feature requests through the web interface at +L. + +=head1 COPYRIGHT AND LICENSE + +=head2 LICENSE AND COPYRIGHT + +Copyright (c) 2021 Jurij Fajnberg + +This program is free software; you can redistribute it and/or modify it under the same terms +as the Perl 5 programming language system itself. + +=cut diff --git a/lib/Test/Expander/Constants.pm b/lib/Test/Expander/Constants.pm new file mode 100644 index 0000000..b1bdd9e --- /dev/null +++ b/lib/Test/Expander/Constants.pm @@ -0,0 +1,35 @@ +## no critic (RequireVersionVar) +package Test::Expander::Constants; + +use v5.14.2; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline portable recursion); + +use Const::Fast; +use Exporter qw(import); +use PadWalker qw(peek_our); + +const our $ANY_EXTENSION => qr/ \. [^.]+ $/x; +const our $CLASS_HIERARCHY_LEVEL => qr/^( \w+ ) (?: :: ( .+ ) )?/x; +const our $ERROR_WAS => ' Error was: '; +const our $FALSE => 0; +const our $EXCEPTION_PREFIX => 'BEGIN failed--compilation aborted at '; +const our $INVALID_ENV_ENTRY => "Erroneous line %d of '%s' containing '%s': %s\n"; +const our $INVALID_VALUE => "Option '%s' passed along with invalid value '%s'\n"; +const our $NEW_FAILED => '%s->new died.%s'; +const our $NEW_SUCCEEDED => "An object of class '%s' isa '%s'"; +const our $REPLACEMENT => $EXCEPTION_PREFIX . '%s line %s.'; +const our $REQUIRE_DESCRIPTION => 'require %s;%s'; +const our $REQUIRE_IMPLEMENTATION => 'package %s; require %s'; +const our $SEARCH_PATTERN => $EXCEPTION_PREFIX . '.*$'; +const our $TOP_DIR_IN_PATH => qr{^ ( [^/]+ )}x; +const our $TRUE => 1; +const our $UNKNOWN_OPTION => "Unknown option '%s' => '%s' supplied.\n"; +const our $USE_DESCRIPTION => 'use %s;%s'; +const our $USE_IMPLEMENTATION => 'package %s; use %s%s; 1'; +const our $VERSION_NUMBER => qr/^ \d+ (?: \. \d+ )* $/x; + +push(our @EXPORT_OK, keys(%{peek_our(0)})); + +1; diff --git a/t/.perlcriticrc b/t/.perlcriticrc new file mode 100644 index 0000000..d3da5fe --- /dev/null +++ b/t/.perlcriticrc @@ -0,0 +1,325 @@ +# You may disable specific policies appending the following annotation +# +# ## no critic (..., ...) +# +# to the corresponding code line. To direct perlcritic to ignore the +# "## no critic" annotations, use the --force option. + +# Policies shipped with Perl::Critic 1.125 were considered for the below +# defintion of the new policy theme "critic". + +severity = brutal +theme = critic +verbose = %f: %m at line %l, column %c. (Policy: %p)\n + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitLvalueSubstr] +add_themes = critic + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitStringyEval] +add_themes = critic +allow_includes = 1 + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitStringySplit] +add_themes = critic + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitUniversalCan] +add_themes = critic + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitUniversalIsa] +add_themes = critic + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitUselessTopic] +# KNOWN BUGS: This policy flags a false positive on reverse() called in list +# context, since reverse() in list context does not assume $_. +add_themes = critic + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitVoidGrep] +add_themes = critic + +[Perl::Critic::Policy::BuiltinFunctions::ProhibitVoidMap] +add_themes = critic + +[Perl::Critic::Policy::BuiltinFunctions::RequireBlockGrep] +add_themes = critic + +[Perl::Critic::Policy::BuiltinFunctions::RequireBlockMap] +add_themes = critic + +[Perl::Critic::Policy::BuiltinFunctions::RequireSimpleSortBlock] +add_themes = critic + +[Perl::Critic::Policy::ClassHierarchies::ProhibitExplicitISA] +# Note: Some people prefer parent over base. +add_themes = critic + +[Perl::Critic::Policy::CodeLayout::ProhibitHardTabs] +add_themes = critic +allow_leading_tabs = 0 + +[Perl::Critic::Policy::CodeLayout::ProhibitTrailingWhitespace] +add_themes = critic + +[Perl::Critic::Policy::CodeLayout::RequireConsistentNewlines] +add_themes = critic + +[Perl::Critic::Policy::ControlStructures::ProhibitCStyleForLoops] +add_themes = critic + +[Perl::Critic::Policy::ControlStructures::ProhibitCascadingIfElse] +add_themes = critic +max_elsif = 1 + +[Perl::Critic::Policy::ControlStructures::ProhibitDeepNests] +# Martin Fowler's book "Refactoring: Improving The Design of Existing Code". +add_themes = critic +max_nests = 5 + +[Perl::Critic::Policy::ControlStructures::ProhibitLabelsWithSpecialBlockNames] +add_themes = critic + +[Perl::Critic::Policy::ControlStructures::ProhibitMutatingListFunctions] +# Read the LIMITATIONS that this policy has. +add_themes = critic + +[Perl::Critic::Policy::ControlStructures::ProhibitNegativeExpressionsInUnlessAndUntilConditions] +add_themes = critic + +[Perl::Critic::Policy::ControlStructures::ProhibitUnreachableCode] +add_themes = critic + +[Perl::Critic::Policy::Documentation::PodSpelling] +add_themes = + +[Perl::Critic::Policy::Documentation::RequirePackageMatchesPodName] +add_themes = critic + +[Perl::Critic::Policy::InputOutput::ProhibitBacktickOperators] +add_themes = critic +only_in_void_context = 1 + +[Perl::Critic::Policy::InputOutput::ProhibitBarewordFileHandles] +add_themes = critic + +[Perl::Critic::Policy::InputOutput::ProhibitExplicitStdin] +add_themes = critic + +[Perl::Critic::Policy::InputOutput::ProhibitInteractiveTest] +add_themes = critic + +[Perl::Critic::Policy::InputOutput::ProhibitJoinedReadline] +add_themes = critic + +[Perl::Critic::Policy::InputOutput::ProhibitReadlineInForLoop] +add_themes = critic + +[Perl::Critic::Policy::InputOutput::ProhibitTwoArgOpen] +add_themes = critic + +[Perl::Critic::Policy::InputOutput::RequireBriefOpen] +# http://www.perlmonks.org/?node_id=1134785 +add_themes = critic +lines = 9 + +[Perl::Critic::Policy::InputOutput::RequireCheckedSyscalls] +# Covers the policies +# Perl::Critic::Policy::InputOutput::RequireCheckedClose and +# Perl::Critic::Policy::InputOutput::RequireCheckedOpen +add_themes = critic +exclude_functions = print say +functions = :builtins + +[Perl::Critic::Policy::InputOutput::RequireEncodingWithUTF8Layer] +add_themes = critic + +[Perl::Critic::Policy::Miscellanea::ProhibitUnrestrictedNoCritic] +add_themes = critic + +[Perl::Critic::Policy::Miscellanea::ProhibitUselessNoCritic] +add_themes = critic + +[Perl::Critic::Policy::Modules::ProhibitAutomaticExportation] +add_themes = critic + +[Perl::Critic::Policy::Modules::ProhibitConditionalUseStatements] +add_themes = critic + +[Perl::Critic::Policy::Modules::ProhibitEvilModules] +add_themes = critic +modules = Class::ISA Error Pod::Plainer Shell Switch + +[Perl::Critic::Policy::Modules::ProhibitExcessMainComplexity] +# http://en.wikipedia.org/wiki/Cyclomatic_complexity +add_themes = critic +max_mccabe = 20 + +[Perl::Critic::Policy::Modules::ProhibitMultiplePackages] +add_themes = critic + +[Perl::Critic::Policy::Modules::RequireBarewordIncludes] +add_themes = critic + +[Perl::Critic::Policy::Modules::RequireEndWithOne] +add_themes = critic + +[Perl::Critic::Policy::Modules::RequireFilenameMatchesPackage] +add_themes = critic + +[Perl::Critic::Policy::Modules::RequireNoMatchVarsWithUseEnglish] +add_themes = critic + +[Perl::Critic::Policy::Objects::ProhibitIndirectSyntax] +add_themes = critic +# The new() subroutine is configured by default; any additional forbid values +# are in addition to new(). +forbid = create destroy + +[Perl::Critic::Policy::RegularExpressions::ProhibitFixedStringMatches] +add_themes = critic + +[Perl::Critic::Policy::RegularExpressions::ProhibitSingleCharAlternation] +add_themes = critic + +[Perl::Critic::Policy::RegularExpressions::ProhibitUnusedCapture] +add_themes = critic + +[Perl::Critic::Policy::RegularExpressions::ProhibitUnusualDelimiters] +add_themes = critic +allow_all_brackets = 0 + +[Perl::Critic::Policy::RegularExpressions::ProhibitUselessTopic] +add_themes = critic + +[Perl::Critic::Policy::RegularExpressions::RequireBracesForMultiline] +add_themes = critic +allow_all_brackets = 0 + +[Perl::Critic::Policy::Subroutines::ProhibitAmpersandSigils] +add_themes = critic + +[Perl::Critic::Policy::Subroutines::ProhibitBuiltinHomonyms] +# Read the CAVEATS. +add_themes = critic + +[Perl::Critic::Policy::Subroutines::ProhibitExcessComplexity] +# http://en.wikipedia.org/wiki/Cyclomatic_complexity +add_themes = critic +max_mccabe = 20 + +[Perl::Critic::Policy::Subroutines::ProhibitExplicitReturnUndef] +# http://perlmonks.org/index.pl?node_id=741847 +add_themes = critic + +[Perl::Critic::Policy::Subroutines::ProhibitManyArgs] +add_themes = critic +max_arguments = 5 + +[Perl::Critic::Policy::Subroutines::ProhibitNestedSubs] +add_themes = critic + +[Perl::Critic::Policy::Subroutines::ProhibitReturnSort] +# KNOWN BUGS: This Policy is not sensitive to the wantarray() function. +add_themes = critic + +[Perl::Critic::Policy::Subroutines::ProhibitSubroutinePrototypes] +add_themes = critic + +[Perl::Critic::Policy::Subroutines::ProhibitUnusedPrivateSubroutines] +add_themes = critic + +[Perl::Critic::Policy::Subroutines::ProtectPrivateSubs] +add_themes = critic + +[Perl::Critic::Policy::Subroutines::RequireArgUnpacking] +add_themes = critic +allow_delegation_to = SUPER:: NEXT:: +allow_subscripts = 0 +short_subroutine_statements = 0 + +[Perl::Critic::Policy::Subroutines::RequireFinalReturn] +add_themes = critic +terminal_funcs = return carp croak die exec exit goto throw + +[Perl::Critic::Policy::TestingAndDebugging::ProhibitNoStrict] +add_themes = critic +allow = refs + +[Perl::Critic::Policy::TestingAndDebugging::ProhibitNoWarnings] +add_themes = critic +allow_with_category_restriction = 1 + +[Perl::Critic::Policy::TestingAndDebugging::ProhibitProlongedStrictureOverride] +add_themes = critic +statements = 3 + +[Perl::Critic::Policy::TestingAndDebugging::RequireUseStrict] +add_themes = critic + +[Perl::Critic::Policy::TestingAndDebugging::RequireUseWarnings] +add_themes = critic + +[Perl::Critic::Policy::ValuesAndExpressions::ProhibitInterpolationOfLiterals] +add_themes = critic +allow_if_string_contains_single_quote = 1 + +[Perl::Critic::Policy::ValuesAndExpressions::ProhibitLeadingZeros] +add_themes = critic + +[Perl::Critic::Policy::ValuesAndExpressions::ProhibitLongChainsOfMethodCalls] +add_themes = critic +max_chain_length = 3 + +[Perl::Critic::Policy::ValuesAndExpressions::ProhibitMagicNumbers] +# Not yet configured completely. +add_themes = critic +allowed_values = -1 0 1 + +[Perl::Critic::Policy::ValuesAndExpressions::ProhibitMixedBooleanOperators] +add_themes = critic + +[Perl::Critic::Policy::ValuesAndExpressions::ProhibitQuotesAsQuotelikeOperatorDelimiters] +add_themes = critic +back_quote_allowed_operators = +double_quote_allowed_operators = +single_quote_allowed_operators = + +[Perl::Critic::Policy::ValuesAndExpressions::ProhibitSpecialLiteralHeredocTerminator] +add_themes = critic + +[Perl::Critic::Policy::ValuesAndExpressions::RequireConstantVersion] +add_themes = critic +allow_version_without_use_on_same_line = 1 + +[Perl::Critic::Policy::ValuesAndExpressions::RequireNumberSeparators] +add_themes = critic +min_value = 10000 + +[Perl::Critic::Policy::ValuesAndExpressions::RequireQuotedHeredocTerminator] +add_themes = critic + +[Perl::Critic::Policy::ValuesAndExpressions::RequireUpperCaseHeredocTerminator] +add_themes = critic + +[Perl::Critic::Policy::Variables::ProhibitConditionalDeclarations] +add_themes = critic + +[Perl::Critic::Policy::Variables::ProhibitLocalVars] +add_themes = critic + +[Perl::Critic::Policy::Variables::ProhibitMatchVars] +add_themes = critic + +[Perl::Critic::Policy::Variables::ProhibitUnusedVariables] +add_themes = critic + +[Perl::Critic::Policy::Variables::RequireInitializationForLocalVars] +add_themes = critic + +[Perl::Critic::Policy::Variables::RequireLexicalLoopIterators] +add_themes = critic + +[Perl::Critic::Policy::Variables::RequireLocalizedPunctuationVars] +add_themes = critic +allow = + +[Perl::Critic::Policy::Variables::RequireNegativeIndices] +add_themes = critic diff --git a/t/.proverc b/t/.proverc new file mode 100644 index 0000000..ee651cd --- /dev/null +++ b/t/.proverc @@ -0,0 +1,4 @@ +--lib +--recurse +--shuffle +-I. \ No newline at end of file diff --git a/t/.proverc-cover b/t/.proverc-cover new file mode 100644 index 0000000..ee651cd --- /dev/null +++ b/t/.proverc-cover @@ -0,0 +1,4 @@ +--lib +--recurse +--shuffle +-I. \ No newline at end of file diff --git a/t/Test/Expander/Boilerplate.pm b/t/Test/Expander/Boilerplate.pm new file mode 100644 index 0000000..6d299b6 --- /dev/null +++ b/t/Test/Expander/Boilerplate.pm @@ -0,0 +1,14 @@ +package t::Test::Expander::Boilerplate; + +use v5.14; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline once portable redefine recursion uninitialized); + +sub new { + my ($class, @args) = @_; + + return bless([\@args], $class); +} + +1; diff --git a/t/Test/Expander/NoCLASS/NoMETHOD.t b/t/Test/Expander/NoCLASS/NoMETHOD.t new file mode 100644 index 0000000..7b2de1f --- /dev/null +++ b/t/Test/Expander/NoCLASS/NoMETHOD.t @@ -0,0 +1,12 @@ +#!/usr/bin/env perl + +use v5.14; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline once portable redefine recursion uninitialized); + +use Test::Expander; + +is($CLASS, undef, 'there is no class corresponding to this test file'); + +done_testing(); diff --git a/t/Test/Expander/_error.t b/t/Test/Expander/_error.t new file mode 100644 index 0000000..a014394 --- /dev/null +++ b/t/Test/Expander/_error.t @@ -0,0 +1,28 @@ +#!/usr/bin/env perl +## no critic (ProtectPrivateSubs RequireLocalizedPunctuationVars) + +use v5.14; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline once portable redefine recursion uninitialized); + +use Test::Expander::Constants qw($ERROR_WAS); +use constant TEST_CASES => { + 'no exception' => { exception => '', args => [], output => '' }, + 'exception raised, no replacement required' => { exception => 'ABC', args => [], output => "${ERROR_WAS}ABC" }, + 'exception raised, replacement required' => { exception => 'ABC', args => [qw(B b)], output => "${ERROR_WAS}AbC" }, +}; +use Test::Builder::Tester tests => scalar(keys(%{TEST_CASES()})); + +use Test::Expander; + +foreach my $title (keys(%{TEST_CASES()})) { + test_out("ok 1 - $title"); + $@ = TEST_CASES->{$title}->{exception}; + is( + Test::Expander::_error(@{TEST_CASES->{$title}->{args}}), + TEST_CASES->{$title}->{output}, + $title + ); + test_test($title); +} diff --git a/t/Test/Expander/_newTestMessage.t b/t/Test/Expander/_newTestMessage.t new file mode 100644 index 0000000..a70f6ab --- /dev/null +++ b/t/Test/Expander/_newTestMessage.t @@ -0,0 +1,24 @@ +#!/usr/bin/env perl +## no critic (ProtectPrivateSubs RequireLocalizedPunctuationVars) + +use v5.14; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline once portable redefine recursion uninitialized); + +use Test::Expander::Constants qw($NEW_FAILED $NEW_SUCCEEDED); +use constant TEST_CASES => { + "'new' succeeded" => { exception => '', output => $NEW_SUCCEEDED }, + "'new' failed" => { exception => 'ABC', output => $NEW_FAILED }, +}; +use Test::Builder::Tester tests => scalar(keys(%{TEST_CASES()})); + +use Test::Expander; + +foreach my $title (keys(%{TEST_CASES()})) { + test_out("ok 1 - $title"); + $@ = TEST_CASES->{$title}->{exception}; + my $expected = TEST_CASES->{$title}->{output} =~ s/%s/.*/gr; + like(Test::Expander::_newTestMessage('CLASS'), qr/$expected/, $title); + test_test($title); +} diff --git a/t/Test/Expander/_setEnv.t b/t/Test/Expander/_setEnv.t new file mode 100644 index 0000000..f6467f1 --- /dev/null +++ b/t/Test/Expander/_setEnv.t @@ -0,0 +1,99 @@ +#!/usr/bin/env perl +## no critic (RequireLocalizedPunctuationVars) + +use v5.14; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline once portable redefine recursion uninitialized); + +use File::chdir; + +use Test::Expander -tempdir => {}, -srand => time; + +can_ok($CLASS, $METHOD); + +ok(-d $TEMP_DIR, "temporary directory '$TEMP_DIR' created"); + +my $classPath = $CLASS =~ s{::}{/}gr; +my $testPath = path($TEMP_DIR)->child('t'); +$testPath->child($classPath)->mkpath; + +{ + local $CWD = $testPath->parent->stringify; ## no critic (ProhibitLocalVars) + + my $testFile = path('t')->child($classPath)->child($METHOD . '.t')->stringify; + my $envFile = path('t')->child($classPath)->child($METHOD . '.env'); + + is(Test2::Plugin::SRand->from, 'import arg', "random seed is supplied as 'time'"); + + subtest 'env variable filled from a variable' => sub { + our $var = 'abc'; + my $name = 'ABC'; + my $value = '$' . __PACKAGE__ . '::var'; + $envFile->spew("$name = $value\nJust a comment line"); + %ENV = (xxx => 'yyy'); + + ok(lives { $METHOD_REF->($METHOD, $CLASS, $testFile) }, 'successfully executed'); + is(\%ENV, { $name => lc($name) }, "'%ENV' has the expected content"); + }; + + subtest 'env variable filled by a self-implemented sub' => sub { + my $name = 'ABC'; + my $value = __PACKAGE__ . "::testEnv(lc('$name'))"; + $envFile->spew("$name = $value"); + %ENV = (xxx => 'yyy'); + + ok(lives { $METHOD_REF->($METHOD, $CLASS, $testFile) }, 'successfully executed'); + is(\%ENV, { $name => lc($name) }, "'%ENV' has the expected content"); + }; + + subtest "env variable filled by a 'File::Temp::tempdir'" => sub { + my $name = 'ABC'; + my $value = 'File::Temp::tempdir'; + $envFile->spew("$name = $value"); + %ENV = (xxx => 'yyy'); + + ok(lives { $METHOD_REF->($METHOD, $CLASS, $testFile) }, 'successfully executed'); + is([ keys(%ENV) ], [ $name ], "'%ENV' has the expected keys"); + ok(-d $ENV{$name}, 'temporary directory exists'); + }; + + subtest 'env file does not exist' => sub { + $envFile->remove; + %ENV = (xxx => 'yyy'); + + ok(lives { $METHOD_REF->($METHOD, $CLASS, $testFile) }, 'successfully executed'); + is(\%ENV, { xxx => 'yyy' }, "'%ENV' remained unchanged"); + }; + + subtest 'directory structure does not correspond to class hierarchy' => sub { + $envFile->remove; + %ENV = (xxx => 'yyy'); + + ok(lives { $METHOD_REF->($METHOD, 'ABC::' . $CLASS, $testFile) }, 'successfully executed'); + is(\%ENV, { xxx => 'yyy' }, "'%ENV' remained unchanged"); + }; + + subtest 'env files exist on multiple levels' => sub { + path($envFile->parent . '.env')->spew("A = '1'\nB = '2'"); + path($envFile->parent->parent . '.env')->spew("C = '0'"); + $envFile->spew("C = '3'"); + %ENV = (xxx => 'yyy'); + + local $CWD = $TEMP_DIR; ## no critic (ProhibitLocalVars) + ok(lives { $METHOD_REF->($METHOD, $CLASS, $testFile) }, 'successfully executed'); + is(\%ENV, { A => '1', B => '2', C => '3' }, "'%ENV' has the expected content"); + }; + + subtest 'env file invalid' => sub { + my $name = 'ABC'; + my $value = 'abc->'; + $envFile->spew("$name = $value"); + + like(dies { $METHOD_REF->($METHOD, $CLASS, $testFile) }, qr/syntax error/, 'expected exception raised'); + }; +} + +done_testing(); + +sub testEnv { return $_[0] } ## no critic (RequireArgUnpacking) diff --git a/t/Test/Expander/_useImports.t b/t/Test/Expander/_useImports.t new file mode 100644 index 0000000..3246b46 --- /dev/null +++ b/t/Test/Expander/_useImports.t @@ -0,0 +1,22 @@ +#!/usr/bin/env perl +## no critic (ProtectPrivateSubs) + +use v5.14; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline once portable redefine recursion uninitialized); + +use constant TEST_CASES => { + 'module version required' => { input => [ '1.22.333' ], output => ' 1.22.333' }, + 'single import but not a module version' => { input => [ 'x' ], output => '' }, + 'multiple imports' => { input => [ qw(x y) ], output => '' }, +}; +use Test::Builder::Tester tests => scalar(keys(%{TEST_CASES()})); + +use Test::Expander; + +foreach my $title (keys(%{TEST_CASES()})) { + test_out("ok 1 - $title"); + is(Test::Expander::_useImports(TEST_CASES->{$title}->{input}), TEST_CASES->{$title}->{output}, $title); + test_test($title); +} diff --git a/t/Test/Expander/compare_ok.t b/t/Test/Expander/compare_ok.t new file mode 100644 index 0000000..b45017b --- /dev/null +++ b/t/Test/Expander/compare_ok.t @@ -0,0 +1,16 @@ +#!/usr/bin/env perl + +use v5.14; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline once portable redefine recursion uninitialized); + +use Test::Builder::Tester tests => 1; + +use Test::Expander; + +my $dir = path(__FILE__)->parent->child($METHOD); +my $title = 'execution'; +test_out("ok 1 - $title"); +compare_ok($dir->child('got'), $dir->child('expected'), $title); +test_test($title); diff --git a/t/Test/Expander/compare_ok/expected b/t/Test/Expander/compare_ok/expected new file mode 100644 index 0000000..d84edfd --- /dev/null +++ b/t/Test/Expander/compare_ok/expected @@ -0,0 +1,2 @@ +A +BC diff --git a/t/Test/Expander/compare_ok/got b/t/Test/Expander/compare_ok/got new file mode 100644 index 0000000..d84edfd --- /dev/null +++ b/t/Test/Expander/compare_ok/got @@ -0,0 +1,2 @@ +A +BC diff --git a/t/Test/Expander/dies_ok.t b/t/Test/Expander/dies_ok.t new file mode 100644 index 0000000..70a3505 --- /dev/null +++ b/t/Test/Expander/dies_ok.t @@ -0,0 +1,15 @@ +#!/usr/bin/env perl + +use v5.14; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline once portable redefine recursion uninitialized); + +use Test::Builder::Tester tests => 1; + +use Test::Expander; + +my $title = 'execution'; +test_out("ok 1 - $title"); +dies_ok(sub { die() }, $title); +test_test($title); diff --git a/t/Test/Expander/import.t b/t/Test/Expander/import.t new file mode 100644 index 0000000..2fd078c --- /dev/null +++ b/t/Test/Expander/import.t @@ -0,0 +1,98 @@ +#!/usr/bin/env perl + +use v5.14; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline once portable redefine recursion uninitialized); + +my (@functions, @variables); +BEGIN { + use Const::Fast; + use File::Temp qw(tempdir tempfile); + use Path::Tiny qw(cwd path); + use Test::Output; + use Test::Warn; + use Test2::Tools::Explain; + use Test2::V0; + @functions = ( + @{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(BAIL_OUT dies_ok is_deeply lives_ok new_ok require_ok use_ok), + ); + @variables = qw($CLASS $METHOD $METHOD_REF $TEMP_DIR $TEMP_FILE); +} + +use Scalar::Readonly qw(readonly_off); +use Test::Builder::Tester tests => @functions + @variables + 4; + +use Test::Expander -target => 'Test::Expander', + -tempdir => { CLEANUP => 1 }, + -tempfile => { UNLINK => 1 }; +use Test::Expander::Constants qw($INVALID_VALUE $UNKNOWN_OPTION); + +foreach my $function (sort @functions) { + my $title = "$CLASS->can('$function')"; + test_out("ok 1 - $title"); + can_ok($CLASS, $function); + test_test($title); +} + +foreach my $variable (sort @variables) { + my $title = "$CLASS exports '$variable'"; + test_out("ok 1 - $title"); + ok(eval("defined($variable)"), $title); ## no critic (ProhibitStringyEval) + test_test($title); +} + +my $title; +my $expected; + +$title = "invalid option value of '-tempdir'"; +$expected = $INVALID_VALUE =~ s/%s/.+/gr; +readonly_off($CLASS); +readonly_off($METHOD); +readonly_off($METHOD_REF); +readonly_off($TEMP_DIR); +readonly_off($TEMP_FILE); +test_out("ok 1 - $title"); +like(dies { $CLASS->$METHOD(-tempdir => 1) }, qr/$expected/, $title); +test_test($title); + +$title = "invalid option value of '-tempfile'"; +$expected = $INVALID_VALUE =~ s/%s/.+/gr; +readonly_off($CLASS); +readonly_off($METHOD); +readonly_off($METHOD_REF); +readonly_off($TEMP_DIR); +readonly_off($TEMP_FILE); +test_out("ok 1 - $title"); +like(dies { $CLASS->$METHOD(-tempfile => 1) }, qr/$expected/, $title); +test_test($title); + +$title = 'unknown option with some value'; +$expected = $UNKNOWN_OPTION =~ s/%s/.+/gr; +readonly_off($CLASS); +readonly_off($METHOD); +readonly_off($METHOD_REF); +readonly_off($TEMP_DIR); +readonly_off($TEMP_FILE); +test_out("ok 1 - $title"); +like(dies { $CLASS->$METHOD(unknown => 1) }, qr/$expected/, $title); +test_test($title); + +$title = 'unknown option without value'; +$expected = $UNKNOWN_OPTION =~ s/%s/.+/r =~ s/%s//r; +readonly_off($CLASS); +readonly_off($METHOD); +readonly_off($METHOD_REF); +readonly_off($TEMP_DIR); +readonly_off($TEMP_FILE); +test_out("ok 1 - $title"); +like(dies { $CLASS->$METHOD('unknown') }, qr/$expected/, $title); +test_test($title); diff --git a/t/Test/Expander/is_deeply.t b/t/Test/Expander/is_deeply.t new file mode 100644 index 0000000..5655fc1 --- /dev/null +++ b/t/Test/Expander/is_deeply.t @@ -0,0 +1,18 @@ +#!/usr/bin/env perl + +use v5.14; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline once portable redefine recursion uninitialized); + +use Clone qw(clone); +use Test::Builder::Tester tests => 1; + +use Test::Expander; + +my $title = 'execution'; +test_out("ok 1 - $title"); +my $got = bless({A => 0, B => [(0 .. 1)]}, 'some class'); +my $expected = clone($got); +is_deeply($got, $expected, $title); +test_test($title); diff --git a/t/Test/Expander/lives_ok.t b/t/Test/Expander/lives_ok.t new file mode 100644 index 0000000..a4da458 --- /dev/null +++ b/t/Test/Expander/lives_ok.t @@ -0,0 +1,15 @@ +#!/usr/bin/env perl + +use v5.14; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline once portable redefine recursion uninitialized); + +use Test::Builder::Tester tests => 1; + +use Test::Expander; + +my $title = 'execution'; +test_out("ok 1 - $title"); +lives_ok(sub {}, $title); +test_test($title); diff --git a/t/Test/Expander/new_ok.t b/t/Test/Expander/new_ok.t new file mode 100644 index 0000000..2ba10f3 --- /dev/null +++ b/t/Test/Expander/new_ok.t @@ -0,0 +1,24 @@ +#!/usr/bin/env perl + +use v5.14; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline once portable redefine recursion uninitialized); + +use constant { + CLASS => 't::Test::Expander::Boilerplate', + TEST_CASES => { + 'no args' => undef, + 'args supplied' => [ 0 .. 1 ], + }, +}; +use Test::Builder::Tester tests => scalar(keys(%{TEST_CASES()})); + +use Test::Expander; +use t::Test::Expander::Boilerplate; + +foreach my $title (keys(%{TEST_CASES()})) { + test_out("ok 1 - An object of class '@{[CLASS]}' isa '@{[CLASS]}'"); + new_ok(CLASS, TEST_CASES->{$title}, $title); + test_test($title); +} diff --git a/t/Test/Expander/require_ok.t b/t/Test/Expander/require_ok.t new file mode 100644 index 0000000..3a48c6b --- /dev/null +++ b/t/Test/Expander/require_ok.t @@ -0,0 +1,17 @@ +#!/usr/bin/env perl + +use v5.14; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline once portable redefine recursion uninitialized); + +use Test::Builder::Tester tests => 1; + +use Test::Expander; + +use constant CLASS => 't::Test::Expander::Boilerplate'; + +my $title = "require @{[CLASS]}"; +test_out("ok 1 - $title;"); +require_ok(CLASS); +test_test($title); diff --git a/t/Test/Expander/throws_ok.t b/t/Test/Expander/throws_ok.t new file mode 100644 index 0000000..65cc7b6 --- /dev/null +++ b/t/Test/Expander/throws_ok.t @@ -0,0 +1,16 @@ +#!/usr/bin/env perl + +use v5.14; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline once portable redefine recursion uninitialized); + +use Test::Builder::Tester tests => 1; + +use Test::Expander; + +my $title = 'execution'; +test_out("ok 1 - $title"); +my $expected = 'DIE TEST'; +throws_ok(sub { die($expected) }, $expected, $title); +test_test($title); diff --git a/t/Test/Expander/use_ok.t b/t/Test/Expander/use_ok.t new file mode 100644 index 0000000..592de86 --- /dev/null +++ b/t/Test/Expander/use_ok.t @@ -0,0 +1,17 @@ +#!/usr/bin/env perl + +use v5.14; +use warnings + FATAL => qw(all), + NONFATAL => qw(deprecated exec internal malloc newline once portable redefine recursion uninitialized); + +use Test::Builder::Tester tests => 1; + +use Test::Expander; + +use constant CLASS => 't::Test::Expander::Boilerplate'; + +my $title = "use @{[CLASS]}"; +test_out("ok 1 - $title;"); +use_ok(CLASS); +test_test($title);