Fennec-2.018000755001750001750 013306515055 13246 5ustar00exodistexodist000000000000Fennec-2.018/Build.PL000444001750001750 176113306515055 14704 0ustar00exodistexodist000000000000#!/usr/bin/perl use strict; use warnings; use Module::Build; my $build = Module::Build->new( module_name => 'Fennec', license => 'perl', dist_author => 'Chad Granum ', create_readme => 1, requires => { 'Carp' => 0, 'Child' => '0.010', 'Exporter::Declare' => 0, 'Mock::Quick' => '1.106', 'Parallel::Runner' => '0.013', 'Scalar::Util' => 0, 'List::Util' => 0, 'Test::Exception' => '0.29', 'Test::Simple' => '0.88', 'Test::Warn' => 0, }, recommends => { 'Fennec::Declare' => '1.001', }, build_requires => { }, meta_merge => { resources => { repository => 'http://github.com/exodist/Fennec', bugtracker => 'http://github.com/exodist/Fennec/issues', homepage => 'http://exodist.github.io/Fennec/' }, }, ); $build->create_build_script; Fennec-2.018/CHANGES000444001750001750 312413306515055 14376 0ustar00exodistexodist000000000000v2.018 - Jun 08 2018 * Doc fixes, deprecate in favor of Test2::Suite v2.017 - ? ? v2.016 - Mar 18 2014 * Bugfix for 2.015 issue v2.015 - Mar 18 2014 * Better subclass support V2.014 - Mar 17 2014 * Support FENNEC_PARALLEL=0 env var. V2.009 - Better mocking scoping * Make it so that Mock::Quick control objects can be implicitly captured by Fennec scopes. V2.008 - Supress messages on syntax check V2.007 - Supress never run message on syntax check V2.002-V2.006 - Minor Bugfixes V2.001 - May 18 2013 * Add 'class' import argument v2.000 - May 18 2013 * Replaced listener system with collector system * Removed evil auto-run hack * LOTS of bugfixes * Fixed win32 (!!!) * Added Child.pm as a default import * Lots of new features * Architecture overhaul * Removed dead code * Lots of simplifications * Additional testing v1.014 - May 07 2013 * Fixed more ordering issues v1.013 - May 06 2013 * Add 2 test files for badly broken behavior * Fixed broken behavior * Nested test structures did not run in proper separate procs * test_sort => 'ordered' did not run any tests vXXXX - Lots * Missed a lot :-( v1.008 - May 12, 2011 - Chad Granum: * Fix? hisenbug in TB listener. v1.002-v1.007 * Forgot to fill this out when I made the changes :-( v1.001 - Feb 18, 2011 - Chad Granum: * Create shortcut for using runner, improve runner re-exec command * Remove verbose messages from STDERR, using STDOUT they cause less spam v1.000 - Jan 24, 2011 - Chad Granum First stable release, API is now set. Fennec-2.018/GOALS000444001750001750 73413306515055 14157 0ustar00exodistexodist000000000000 * Work with Test::Builder * Do not force Test::Builder * Forking should just plain work * In a verbose handler error diagnostics should be with their failed test. * Errors should still be in stderr in a non-verbose handler * Desired test dist's should be loaded all at once (Test::More, etc.) * Subclass Fennec.pm to configure things, then load that subclass in all tests * Test groups (Test-Workflow) * Case workflow * SPEC workflow * Easy mocking (Object-Quick) Fennec-2.018/MANIFEST000444001750001750 215713306515055 14541 0ustar00exodistexodist000000000000Build.PL CHANGES GOALS lib/Fennec.pm lib/Fennec/Collector.pm lib/Fennec/Collector/TB.pm lib/Fennec/Collector/TB/TempFiles.pm lib/Fennec/EndRunner.pm lib/Fennec/Finder.pm lib/Fennec/Manual.pod lib/Fennec/Manual/CustomFennec.pod lib/Fennec/Meta.pm lib/Fennec/Runner.pm lib/Fennec/Test.pm lib/Fennec/Util.pm lib/Test/Workflow.pm lib/Test/Workflow/Block.pm lib/Test/Workflow/Layer.pm lib/Test/Workflow/Meta.pm lib/Test/Workflow/Test.pm MANIFEST This list of files README t/around_all.t t/before_case.t t/CantFindLayer.t t/Case-Scoping.t t/Child.t t/Class.t t/Declare.t t/FennecLN/CantFindLayer.ft t/FennecLN/Case-Scoping.ft t/FennecLN/hash_warning.ft t/FennecLN/import_skip.ft t/FennecLN/inner_todo.ft t/FennecLN/Mock.ft t/FennecLN/order.ft t/FennecLN/procs.ft t/FennecLN/RunSpecific.ft t/FennecLN/Todo.ft t/FennecLN/Workflow_Fennec.ft t/Finder.t t/hash_warning.t t/import_skip.t t/inner_todo.t t/lib/FinderTest.pm t/lib/WorkflowTest.pm t/MatchedT.t t/Mock.t t/order.t t/POD.t t/procs.t t/RunSpecific.t t/Self-Running-Legacy.t t/Self-Running.t t/subclass.t t/Test-Isolation.t t/Todo.t t/Workflow.t t/Workflow_Fennec.t META.yml META.json Fennec-2.018/META.json000444001750001750 522613306515055 15031 0ustar00exodistexodist000000000000{ "abstract" : "A testers toolbox, and best friend", "author" : [ "Chad Granum " ], "dynamic_config" : 1, "generated_by" : "Module::Build version 0.4224", "license" : [ "perl_5" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : 2 }, "name" : "Fennec", "prereqs" : { "configure" : { "requires" : { "Module::Build" : "0.42" } }, "runtime" : { "recommends" : { "Fennec::Declare" : "1.001" }, "requires" : { "Carp" : "0", "Child" : "0.010", "Exporter::Declare" : "0", "List::Util" : "0", "Mock::Quick" : "1.106", "Parallel::Runner" : "0.013", "Scalar::Util" : "0", "Test::Exception" : "0.29", "Test::Simple" : "0.88", "Test::Warn" : "0" } } }, "provides" : { "Fennec" : { "file" : "lib/Fennec.pm", "version" : "2.018" }, "Fennec::Collector" : { "file" : "lib/Fennec/Collector.pm" }, "Fennec::Collector::TB" : { "file" : "lib/Fennec/Collector/TB.pm" }, "Fennec::Collector::TB::TempFiles" : { "file" : "lib/Fennec/Collector/TB/TempFiles.pm" }, "Fennec::EndRunner" : { "file" : "lib/Fennec/EndRunner.pm" }, "Fennec::Finder" : { "file" : "lib/Fennec/Finder.pm" }, "Fennec::Meta" : { "file" : "lib/Fennec/Meta.pm" }, "Fennec::Runner" : { "file" : "lib/Fennec/Runner.pm" }, "Fennec::Test" : { "file" : "lib/Fennec/Test.pm" }, "Fennec::Util" : { "file" : "lib/Fennec/Util.pm" }, "Test::Workflow" : { "file" : "lib/Test/Workflow.pm" }, "Test::Workflow::Block" : { "file" : "lib/Test/Workflow/Block.pm" }, "Test::Workflow::Layer" : { "file" : "lib/Test/Workflow/Layer.pm" }, "Test::Workflow::Meta" : { "file" : "lib/Test/Workflow/Meta.pm" }, "Test::Workflow::Test" : { "file" : "lib/Test/Workflow/Test.pm" } }, "release_status" : "stable", "resources" : { "bugtracker" : { "web" : "http://github.com/exodist/Fennec/issues" }, "homepage" : "http://exodist.github.io/Fennec/", "license" : [ "http://dev.perl.org/licenses/" ], "repository" : { "url" : "http://github.com/exodist/Fennec" } }, "version" : "2.018", "x_serialization_backend" : "JSON::PP version 2.97001" } Fennec-2.018/META.yml000444001750001750 334013306515055 14654 0ustar00exodistexodist000000000000--- abstract: 'A testers toolbox, and best friend' author: - 'Chad Granum ' build_requires: {} configure_requires: Module::Build: '0.42' dynamic_config: 1 generated_by: 'Module::Build version 0.4224, CPAN::Meta::Converter version 2.150010' license: perl meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: '1.4' name: Fennec provides: Fennec: file: lib/Fennec.pm version: '2.018' Fennec::Collector: file: lib/Fennec/Collector.pm Fennec::Collector::TB: file: lib/Fennec/Collector/TB.pm Fennec::Collector::TB::TempFiles: file: lib/Fennec/Collector/TB/TempFiles.pm Fennec::EndRunner: file: lib/Fennec/EndRunner.pm Fennec::Finder: file: lib/Fennec/Finder.pm Fennec::Meta: file: lib/Fennec/Meta.pm Fennec::Runner: file: lib/Fennec/Runner.pm Fennec::Test: file: lib/Fennec/Test.pm Fennec::Util: file: lib/Fennec/Util.pm Test::Workflow: file: lib/Test/Workflow.pm Test::Workflow::Block: file: lib/Test/Workflow/Block.pm Test::Workflow::Layer: file: lib/Test/Workflow/Layer.pm Test::Workflow::Meta: file: lib/Test/Workflow/Meta.pm Test::Workflow::Test: file: lib/Test/Workflow/Test.pm recommends: Fennec::Declare: '1.001' requires: Carp: '0' Child: '0.010' Exporter::Declare: '0' List::Util: '0' Mock::Quick: '1.106' Parallel::Runner: '0.013' Scalar::Util: '0' Test::Exception: '0.29' Test::Simple: '0.88' Test::Warn: '0' resources: bugtracker: http://github.com/exodist/Fennec/issues homepage: http://exodist.github.io/Fennec/ license: http://dev.perl.org/licenses/ repository: http://github.com/exodist/Fennec version: '2.018' x_serialization_backend: 'CPAN::Meta::YAML version 0.018' Fennec-2.018/README000444001750001750 6707013306515055 14315 0ustar00exodistexodist000000000000NAME Fennec - A testers toolbox, and best friend DESCRIPTION Fennec ties together several testing related modules and enhances their functionality in ways you don't get loading them individually. Fennec makes testing easier, and more useful. CAVEAT EMPTOR This module is deprecated in favor of Test2::Suite, specifically Test2::Tools::Spec and Test2::Bundle::SpecDeclare. SYNOPSIS There are 2 ways to use Fennec. You can use Fennec directly, or you can use the shiny sugar-coated interface provided by the add-on module Fennec::Declare. VANILLA SYNTAX If Devel::Declare and its awesome power of syntax specification scares you, you can always write your Fennec tests in the stone age like this... just don't miss any semicolons. t/some_test.t: package TEST::SomeTest; use strict; use warnings; use Fennec( parallel => 3, test_sort => 'random', ); # This is optional, there is a default 'new' if you do not override it. sub new { ... } # Test blocks are called as methods on an instance of your test package. tests group_1 => sub { my $self = shift; ok( 1, "1 is true" ); }; test group_2 => ( todo => 'This is not ready yet', code => sub { my $self = shift; ok( 0, "Not ready" ); } ); # It is important to always end a Fennec test with this function call. done_testing(); DECLARE SYNTAX Note: In order to use this you MUST install Fennec::Declare which is a separate distribution on cpan. This module is separate because it uses the controversial Devel::Declare module. t/some_test.t: package TEST::SomeTest; use strict; use warnings; use Fennec::Declare( parallel => 3, test_sort => 'random', ); # This is optional, there is a default 'new' if you do not override it. sub new { ... } # Test blocks are called as methods on an instance of your test package. tests group_1 { # Note: $self is automatically shifted for you. ok( $self, "Got self automatically" ); }; test group_2 ( todo => 'This is not ready yet' ) { # Note: $self is automatically shifted for you. ok( 0, "Not ready" ); } # It is important to always end a Fennec test with this function call. done_testing; FEATURES PROVIDED DIRECTLY BY FENNEC Forking just works Forking in perl tests that use Test::Builder is perilous at best. Fennec initiates an Fennec::Collector class which sets up Test::Builder to funnel all test results to the main thread for rendering. A result of this is that forking just works. Concurrency, test blocks can run in parallel By default all test blocks are run in parallel with a cap of 3 concurrent processes. The process cap can be set with the parallel import argument. No need to maintain a test count The test count traditionally was used to ensure your file finished running instead of exiting silently too early. With Test::Builder and friends this has largely been replaced with the done_testing() function typically called at the end of tests. Fennec shares this concept, but takes it further, you MUST call done_testing() at the end of your test files. This is safer because it can be used to ensure your test script ran completely. Can be decoupled from Test::Builder Fennec is built with the assumption that Test::Builder and tools built from it will be used. However custom Fennec::Collector and Fennec::Runner classes can replace this assumption with any testing framework you want to use. Can run specific test blocks, excluding others Have you ever had a huge test that took a long time to run? Have you ever needed to debug a failing test at the end of the file? How many times did you need to sit through tests that didn't matter? With Fennec you can specify the FENNEC_TEST environment variable with either a line number or test block name. Only tests defined on that line, or with that name will be run. Predictability: Rand is always seeded with the date Randomizing the order in which test blocks are run can help find subtle interaction bugs. At the same time if tests are always in random order you cannot reliably reproduce a failure. Fennec always seeds rand with the current date. This means that on any given date the test run order will always be the same. However different days test different orders. You can always specify the FENNEC_SEED environment variable to override the value used to seed rand. Diag output is coupled with test output When you run a Fennec test with a verbose harness (prove -v) the diagnostic output will be coupled with the TAP output. This is done by sending both output to STDOUT. In a non-verbose harness the diagnostics will be sent to STDERR per usual. Works with Moose All your test classes are instantiated objects. You can use Moose to define these test classes. But you do not have to, you are not forced to use OOP in your tests. PROVIDED BY MODULES LOADED BY FENNEC The 3 most common and useful Test::* modules Test::More, Test::Warn, Test::Exception RSPEC support Those familiar with Ruby may already know about the RSPEC testing process. In general you describe something that is to be tested, then you define setup and teardown methods (before_all, before_each, after_all, after_each) and then finally you test it. See the "EXAMPLES" section or Test::Workflow for more details. Test re-ordering, tests can run in random, sorted, or defined order. When you load Fennec you can specify a test order. The default is random. You can also use the order in which they are defined, or sorted (alphabetically) order. If necessary you can pass in a sorting function that takes a list of all test-objects as arguments. Provided by Test::Workflow Reusable test modules You can write tests in modules using Test::Workflow and then import those tests into Fennec tests. This is useful if you have tests that you want run in several, or even all test files. Provided by Test::Workflow Incredibly powerful mocking with a simple API You can create classless object instances from a specification on the fly, define new packages, or override existing packages. Provided by Mock::Quick DEFAULT IMPORTED MODULES Note: These can be overridden either on import, or by subclassing Fennec. Child - Forking for dummies Child is an OO interface to forking that removes all the boilderplate such as checking if the pid changed, and making sure you exit the child process. Mock::Quick - Mocking without the eye gouging Mock::Quick is a mocking library that makes mocking easy. In addition it uses a declarative style interface. Unlike most other mocking libraries on CPAN, it does not make people want to gouge their eyes out and curl up in the fetal position. Test::Workflow - RSPEC for perl. Test::Workflow is a testing library written specifically for Fennec. This library provides RSPEC workflow functions and structure. It can be useful on its own, but combined with Fennec it gets concurrency. Test::More Tried and True testing module that everyone uses. Test::Warn Test::Warn - Test code that issues warnings. Test::Exception Test::Exception - Test code that throws exceptions IMPORT ARGUMENTS base => 'Some::Base' Load the specified module and make it the base class for your test class. class => 'What::To::Test' Used to specify the name of the package your test file is validating. When this parameter is specified 3 things are done for you: The class is automatically loaded, the $CLASS variable is imported and contains the module name, and the class() subroutine is defined and returns the name. use Fennec class => 'Foo::Bar'; ok( $INC{'Foo/Bar.pm'}, "Loaded 'Foo::Bar'" ); is( $CLASS, 'Foo::Bar', "We have \$CLASS" ); is( class(), 'Foo::Bar', "We have class()" ); tests method => sub { my $self = shift; is( $self->class(), 'Foo::Bar', "We have class() method" ); }; done_testing; parallel => $PROC_LIMIT How many test blocks can be run in parallel. Default is 3. Set to 1 to fork for each test, but only run one at a time. Set to 0 to prevent forking. You can also set this using the $FENNEC_PARALLEL environment variable. debug => 1 Enable tracking debugging information. At the end of the Fennec run it will present you with a CSV temp file. This file lists all blocks that are run, and mocks that are made in sequence from top to bottom. The actions are split into columns by PID. This is usedul when debugging potential race-conditions when using parallel testing. Example: 26150,26151,26152,26153,26154 0 26150 BLOCK 54->78 child: outer_wrap, , , , , ,1 26151 BLOCK 47->52 test: class_store, , , , 0 26150 MOCK Foo => (outer), , , , , 0 26150 BLOCK 58->61 before_all: ba, , , , , , ,2 26152 MOCK Foo => (outer), , , , ,2 26152 BLOCK 63->66 before_each: be, , , , ,2 26152 BLOCK 68->72 test: the_check, , , , , ,3 26153 BLOCK 16->31 test: object, , , , , ,4 26154 BLOCK 33->45 test: class, You can use this in a spreadsheet program, or use this command to look at it in a more friendly way. column -s, -t < '/path/to/tempfile' | less -#2 -S collector_class => 'Fennec::Collector::TB::TempFiles' Specify which collector to use. Defaults to a Test::Builder based collector that uses temp files to funnel tests from child procs to the parent. You generally won't need to specify this, unless you use a test infrastructure that is neither TAP nore Test::Builder based. runner_class => 'Fennec::Runner' Specify the runner class. You probably don't need this. runner_params => { ... } Lets you specify arguments used when Fennec::Runner is initialized. skip_without => [ 'Need::This', 'And::This' ] Tell Fennec to skip the test file if any of the specified modules are missing. test_sort => $SORT Options: 'random', 'sorted', 'ordered', or a code block. Code block accepts a list of Test::Workflow::Test objects. utils => [ 'Test::Foo', ... ] Load these modules instead of the default list. If you need to specify import arguments for any specific util class, you can use the class name as the key with an arrayref containing the arguments. use Fennec( utils => [ 'Some::Module' ], 'Some::Module' => [ arg => $val, ... ], ); with_tests => [ 'Reusable::Tests', 'Common::Tests' ] Load these modules that have reusable tests. Reusable tests are tests that are common to multiple test files. seed => '...' Set the random seed to be used. Defaults to current date, can be overridden by the FENNEC_SEED environment variable. ENVIRONMENT VARIABLES FENNEC_SEED Can be used to set a specific random seed FENNEC_TEST Can be used to tell Fennec to only run specific tests (can be given a line number or a block name). FENNEC_DEBUG When true internal debugging is turned on. EXPORTED FUNCTIONS FROM FENNEC done_testing() done_testing(sub { ... }) Should be called at the end of your test file to kick off the RSPEC tests. Always returns 1, so you can use it as the last statement of your module. You must only ever call this once per test file. Never put tests below the done_testing call. If you want tests to run AFTER the RSPEC workflow completes, you can pass done_testing a coderef with the tests. done_testing( sub { ok( 1, "This runs after the RSPEC workflow" ); }); FROM Test::Workflow See Test::Workflow or "EXAMPLES" for more details. with_tests 'Module::Name'; Import tests from a module tests $name => sub { ... }; tests $name => ( %params ); it $name => sub { ... }; it $name => ( %params ); Define a test block describe $name => sub { ... }; Describe a set of tests (group tests and setup/teardown functions) case $name => sub { ... }; Used to run a set of tests against multiple conditions before_all $name => sub { ... }; Setup, run once before any tests in the describe scope run. before_case $name => sub { ... }; Setup, run before any case blocks are run. before_each $name => sub { ... }; after_case $name => sub { ... }; Setup, run once per test, just before it runs. Both run after the case block (if there is one). around_each $name => sub { ... }; Setup and/or teardown. after_each $name => sub { ... }; Teardown, run once per test, after it finishes. after_all $name => sub { ... }; Teardown, run once, after all tests in the describe scope complete. FROM Mock::Quick See Mock::Quick or "EXAMPLES" for more details. my $control = qclass $CLASS => ( %PARAMS, %OVERRIDES ); my $control = qtakeover $CLASS => ( %PARAMS, %OVERRIDES ); my $control = qimplement $CLASS => ( %PARAMS, %OVERRIDES ); my $control = qcontrol $CLASS => ( %PARAMS, %OVERRIDES ); Used to define, takeover, or override parts of other packages. my $obj = qobj( %PARAMS ); my ( $obj, $control ) = qobjc( %PARAMS ); my $obj = qstrict( %PARAMS ); my ( $obj, $control ) = qstrictc( %PARAMS ); Define an object specification, quickly. my $clear = qclear(); Used to clear a field in a quick object. my $meth = qmeth { ... }; my $meth = qmeth( sub { ... } ); Used to define a method for a quick object. OTHER See Test::More, Test::Warn, and Test::Exception EXAMPLES Examples can be the best form of documentation. SIMPLE VANILLA SYNTAX t/simple.t use strict; use warnings; use Fennec; use_ok 'Data::Dumper'; tests dumper => sub { my $VAR1; is_deeply( eval Dumper({ a => 1 }), { a => 1 }, "Serialize and De-Serialize" ); }; tests future => ( todo => "Not ready yet", code => sub { ok( 0, "I still have to write these" ); }, ); done_testing; DECLARE SYNTAX t/simple.t use strict; use warnings; use Fennec::Declare; use_ok 'Data::Dumper'; tests dumper { my $VAR1; is_deeply( eval Dumper({ a => 1 }), { a => 1 }, "Serialize and De-Serialize" ); is( eval { no strict; Dumper( { a => 1 } ) }, { a => 1 }, "Serialize and De-Serialize" ); } tests future( todo => "Not ready yet" ) { ok( 0, "I still have to write these" ); } done_testing; RUN TESTS UNDER DIFFERENT CONDITIONS This example shows 4 conditions ($letter as 'a', 'b', 'c', and 'd'). It also has 2 test blocks, one that verifies $letter is a letter, the other verifies it is lowercase. Each test block will be run once for each condition, 2*4=8, so in total 8 tests will be run. VANILLA sample.t: use strict; use warnings; use Fennec; my $letter; case a => sub { $letter = 'a' }; case b => sub { $letter = 'b' }; case c => sub { $letter = 'c' }; case d => sub { $letter = 'd' }; tests is_letter => sub { like( $letter, qr/^[a-z]$/i, "Got a letter" ); }; tests is_lowercase => sub { is( $letter, lc( $letter ), "Letter is lowercase" ); }; done_testing; OBJECT ORIENTED sample.t use strict; use warnings; use Fennec; sub letter { my $self = shift; ( $self->{letter} ) = @_ if @_; return $self->{letter}; } describe letters => sub { case a => sub { shift->letter('a') }; case b => sub { shift->letter('b') }; case c => sub { shift->letter('c') }; case d => sub { shift->letter('d') }; tests is_letter => sub { my $self = shift; like( $self->letter, qr/^[a-z]$/i, "Got a letter" ); }; tests is_lowercase => sub { my $self = shift; is( $self->letter, lc( $self->letter ), "Letter is lowercase" ); }; }; done_testing; DECLARE Note: no need to shift $self, it is done for you! sample.t use strict; use warnings; use Fennec::Declare; sub letter { my $self = shift; ( $self->{letter} ) = @_ if @_; return $self->{letter}; } describe letters { case a { $self->letter('a') } case b { $self->letter('b') } case c { $self->letter('c') } case d { $self->letter('d') } tests is_letter { like( $self->letter, qr/^[a-z]$/i, "Got a letter" ); } tests is_lowercase { is( $self->letter, lc( $self->letter ), "Letter is lowercase" ); } } done_testing; MOCKING See Mock::Quick for more details OBJECT ON THE FLY my $obj = qobj( foo => 'foo', bar => qmeth { 'bar' }, baz => sub { 'baz' }, ); is( $obj->foo, 'foo' ); is( $obj->bar, 'bar' ); is( ref $obj->baz, 'CODE', "baz is a method that returns a coderef" ); # All methods autovivify as read/write accessors: lives_ok { $obj->blah( 'x' ) }; # use qstrict() to make an object that does not autovivify accessors. SCOPE OF MOCKS IN FENNEC With vanilla Mock::Quick a mock is destroyed when the control object is destroyed. my $control = qtakeover Foo => (blah => 'blah'); is( Foo->blah, 'blah', "got mock" ); $control = undef; ok( !Foo->can('blah'), "Mock destroyed" ); # WITHOUT FENNEC This issues a warning, the $control object is ignored so # the mock is destroyed before it can be used. qtakover Foo => (blah => 'blah'); ok( !Foo->can('blah'), "Mock destroyed before it could be used" ); With the workflow support provided by Fennec, you can omit the control object and let the mock be scoped implicitly. tests implicit_mock_scope => sub { my $self = shift; can_ok( $self, 'QINTERCEPT' ); qtakeover Foo => (blah => sub { 'blah' }); is( Foo->blah, 'blah', "Mock not auto-destroyed" ); }; describe detailed_implicit_mock_scope => sub { qtakeover Foo => ( outer => 'outer' ); ok( !Foo->can( 'outer' ), "No Leak" ); before_all ba => sub { qtakeover Foo => ( ba => 'ba' ); can_ok( 'Foo', qw/outer ba/ ); }; before_each be => sub { qtakeover Foo => ( be => 'be' ); can_ok( 'Foo', qw/outer ba be/ ); }; tests the_check => sub { qtakeover Foo => ( inner => 'inner' ); can_ok( 'Foo', qw/outer ba be inner/ ); }; ok( !Foo->can( 'outer' ), "No Leak" ); ok( !Foo->can( 'ba' ), "No Leak" ); ok( !Foo->can( 'be' ), "No Leak" ); ok( !Foo->can( 'inner' ), "No Leak" ); }; TAKEOVER AN EXISTING CLASS require Some::Class; my $control = qtakeover 'Some::Class' => ( # Override some methods: foo => sub { 'foo' }, bar => sub { 'bar' }, # For methods that return a simple value you don't actually need to # wrap them in a sub. baz => 'bat', ); is( Some::Class->foo, 'foo' ); is( Some::Class->bar, 'bar' ); # Use the control object to make another override $control->override( foo => 'FOO' ); is( Some::Class->foo, 'FOO' ); # Original class is restored when $control falls out of scope. $control = undef; MOCK A CLASS INSTEAD OF LOADING THE REAL ONE This will prevent the real class from loading if code tries to require or use it. However when the control object falls out of scope you will be able to load the real one again. my $control = qimplement 'Some::Class' => ( my_method => sub { ... } simple => 'foo', ); MOCK AN ANONYMOUS CLASS my $control = qclass( -with_new => 1, # Make a constructor for us method => sub { ... }, simple => 'foo', ); my $obj = $control->package->new; REUSABLE TEST LIBRARIES This is a test library that verifies your test file uses strict in the first 3 lines. You can also pass with_tests => [ 'Some::Test::Lib' ] as an import argument to Fennec. This matters because you can subclass Fennec to always include this library. t/test.t use strict; use warnings; use Fennec; with_tests 'Some::Test::Lib'; done_testing; lib/Some/Test/Lib.pm package Some::Test::Lib; use Test::Workflow; use Test::More; use Scalar::Util qw/blessed/; tests check_use_strict => sub { my $self = shift; my $class = blessed $self; my $file = $class; $file =~ s{::}{/}g; $file .= '.pm'; my $full = $INC{$file}; ok( -e $full, "Found path and filename for $class" ); open( my $fh, '<', $full ) || die $!; my $found = 0; for ( 1 .. 3 ) { $found = <$fh> =~ m/^\s*use strict;\s*$/; last if $found; } close($fh); ok( $found, "'use strict;' is in the first 3 lines of the test file" ); } 1; POST TESTS You cannot put any tests under done_testing() Doing so will cause problems. However you can put tests IN done_testing. use strict; use warnings; use Fennec; my $foo = 1; is( $foo, 1, "foo is 1" ); done_testing( sub { is( $foo, 1, "foo is still 1" ); } ); RSPEC The following test will produce output similar to the following. Keep in mind that the concurrent nature of Fennec means that the lines for each process may appear out of order relative to lines from other processes. Lines for any given process will always be in the correct order though. Spacing has been added, and process output has been grouped together, except for the main process to demonstrate that after_all really does come last. # PID OUTPUT #--------------------------------------------- 7253 describe runs long before everything else 7253 before_all runs first 7254 case runs between before_all and before_each 7254 before_each runs just before tests 7254 tests run in the middle 7254 after_each runs just after tests 7255 before_each runs just before tests 7255 This test inherits the before and after blocks from the parent describe. 7255 after_each runs just after tests 7253 after_all runs last. sample.t use strict; use warnings; use Fennec; describe order => sub { print "$$ describe runs long before everything else\n"; before_all setup_a => sub { print "$$ before_all runs first\n"; }; case a_case => sub { print "$$ case runs between before_all and before_each\n"; }; before_each setup_b => sub { print "$$ before_each runs just before tests\n"; }; tests a_test => sub { print "$$ tests run in the middle\n"; }; after_each teardown_b => sub { print "$$ after_each runs just after tests\n"; }; after_all teardown_a => sub { print "$$ after_all runs last.\n"; }; describe nested => sub { tests b_test => sub { print "$$ This test inherits the before/after/case blocks from the parent describe.\n"; }; }; }; done_testing; MANUAL The manual can be found here: Fennec::Manual it is a sort of Nexus for documentation, including this document. VIM INTEGRATION Insert this into your .vimrc file to bind the F8 key to running the test block directly under your cursor. You can be on any line of the test block (except in some cases the first or last line. function! RunFennecLine() let cur_line = line(".") exe "!FENNEC_TEST='" . cur_line . "' prove -v -I lib %" endfunction " Go to command mode, save the file, run the current test :map :w:call RunFennecLine() :imap :w:call RunFennecLine() RUNNING FENNEC TEST FILES IN PARALLEL The best option is to use prove with the -j flag. Note: The following is no longer a recommended practice, it is however still supported You can also create a custom runner using a single .t file to run all your Fennec tests. This has caveats though, such as not knowing which test file had problems without checking the failure messages. This will find all *.ft and/or *.pm modules under the t/ directory. It will load and run any found. These will be run in parallel t/runner.t #!/usr/bin/perl use strict; use warnings; # Paths are optional, if none are specified it defaults to 't/' use Fennec::Finder( 't/' ); # The next lines are optional, if you have no custom configuration to apply # you can jump right to 'done_testing'. # Get the runner (singleton) my $runner = Fennec::Finder->new; $runner->parallel( 3 ); # You must call this. run(); AUTHORS Chad Granum exodist7@gmail.com COPYRIGHT Copyright (C) 2013 Chad Granum Fennec is free software; Standard perl license (GPL and Artistic). Fennec is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/lib000755001750001750 013306515055 14014 5ustar00exodistexodist000000000000Fennec-2.018/lib/Fennec.pm000444001750001750 10055113306515055 15747 0ustar00exodistexodist000000000000package Fennec; use strict; use warnings; BEGIN { require Fennec::Runner } use Fennec::Test; use Fennec::Util qw/inject_sub require_module verbose_message/; use Carp qw/croak carp/; our $VERSION = '2.018'; sub defaults { ( utils => [ 'Test::More', 'Test::Warn', 'Test::Exception', 'Test::Workflow', 'Mock::Quick', 'Child', ], parallel => defined $ENV{'FENNEC_PARALLEL'} ? $ENV{'FENNEC_PARALLEL'} : 3, runner_class => 'Fennec::Runner', with_tests => [], Child => ['child'], debug => $ENV{'FENNEC_DEBUG'} || 0, ); } sub _setup_class { my $class = shift; my ( $runner, $importer, $load ) = @_; return unless $load; require_module $load; no strict 'refs'; *{"$importer\::CLASS"} = \$load; *{"$importer\::class"} = sub { $load }; } sub import { my $class = shift; my $importer = caller; my %defaults = $class->defaults; $defaults{runner_class} ||= 'Fennec::Runner'; my %params = ( %defaults, @_ ); $ENV{FENNEC_SEED} ||= $params{seed} if $params{seed}; $ENV{FENNEC_DEBUG} ||= $params{debug} if $params{debug}; my ( $runner, $runner_init ) = $class->_get_runner( $importer, $defaults{runner_class}, $defaults{runner_params}, ); verbose_message("Entering build stage: $importer\n"); push @{$runner->test_classes} => $importer; my $meta = $class->_init_meta( $importer, %params ); $class->_setup_class( $runner, $importer, $params{class} ); $class->_process_deps( $runner, $params{skip_without} ); $class->_set_isa( $importer, 'Fennec::Test', $meta->base ); $class->_load_utils( $importer, %params ); # Intercept Mock::Quick mocks my $wfmeta = $importer->TEST_WORKFLOW; if ( $wfmeta && grep { $_ eq 'Mock::Quick' } @{$defaults{utils} || []}) { my $intercept = sub { my ($code) = @_; my @caller = caller; my $store = $wfmeta->control_store; return push @$store => $code->() if $store; my $layer = $wfmeta->peek_layer || $wfmeta->root_layer; $layer->add_control($code); }; no strict 'refs'; *{"$importer\::QINTERCEPT"} = sub{ $intercept }; } $class->_with_tests( $importer, $params{with_tests} ); $class->init( %params, importer => $importer, meta => $meta ); if ($ENV{FENNEC_DEBUG} || $params{debug}) { require Time::HiRes; my $collector; my $debug = sub { my $msg = pop; my ($sec, $ms) = Time::HiRes::gettimeofday(); my $line = sprintf( "FENNEC_DEBUG_CUSTOM:PID:%d\0SEC:%d\0MSEC:%d\0MESSAGE:%s\n", $$, $sec, $ms, $msg ); $collector ||= Fennec::Runner->new->collector; $collector->diag($line); }; no strict 'refs'; *{"$importer\::fennec_debug"} = $debug; } $class->_export_done_testing( $importer, $runner, $runner_init, ); $class->after_import({ importer => $importer, runner => $runner, meta => $meta, wf_meta => $wfmeta, layer => $wfmeta->peek_layer || $wfmeta->root_layer, }) if $class->can('after_import'); verbose_message("Entering primary stage: $importer\n"); } sub init { my $class = shift; my %params = @_; my $importer = $params{importer}; my $meta = $params{meta}; my $wfmeta = $importer->TEST_WORKFLOW; $wfmeta->test_sort( $meta->test_sort ) if $meta->test_sort; no strict 'refs'; my $stash = \%{"$importer\::"}; delete $stash->{$_} for qw/run_tests done_testing/; } sub _get_runner { my $class = shift; my ( $importer, $runner_class, $runner_params ) = @_; require_module $runner_class; my $runner_init = $runner_class->is_initialized; croak "Fennec cannot be used in package 'main' when the test is used with Fennec::Finder" if $runner_init && $importer eq 'main'; if ($runner_init) { my $runner = $runner_class->new; carp "Runner is already initialized, but it is not a $runner_class" unless $runner->isa($runner_class); carp "Runner is already initialized, ignoring 'runner_params'" if $runner_params; return ( $runner, $runner_init ); } my $runner = $runner_class->new( parallel => 0, $runner_params ? (%$runner_params) : (), ); require Fennec::EndRunner; Fennec::EndRunner->set_pid($$); Fennec::EndRunner->set_runner($runner); return ( $runner, $runner_init ); } sub _process_deps { my $class = shift; my ( $runner, $deps ) = @_; return unless $deps && @$deps; for my $require (@$deps) { unless ( eval { require_module $require; 1 } ) { $runner->_skip_all(1); $runner->collector->skip("'$require' is not installed"); $runner->collector->finish; exit 0; } } } sub _init_meta { my $class = shift; my ( $importer, %params ) = @_; require Fennec::Meta; my $meta = Fennec::Meta->new( %params, # Well, this is confusing. fennec => $class, class => $importer, ); inject_sub( $importer, 'FENNEC', sub { $meta } ); return $meta; } sub _set_isa { my $class = shift; my ( $importer, @bases ) = @_; for my $base (@bases) { next unless $base; no strict 'refs'; require_module $base; push @{"$importer\::ISA"} => $base unless grep { $_ eq $base } @{"$importer\::ISA"}; } } sub _load_utils { my $class = shift; my ( $importer, %params ) = @_; my $utils = $params{utils}; return unless $utils && @$utils; for my $util (@$utils) { require_module $util; my $args = $params{$util} || []; my $code = "package $importer; $util\->import(\@\$args); 1"; eval $code || die $@; } } sub _with_tests { my $class = shift; my ( $importer, $classes ) = @_; return unless $classes && @$classes; $importer->TEST_WORKFLOW->root_layer->merge_in( undef, @$classes ); } sub _export_done_testing { my $class = shift; my ( $importer, $runner, $runner_init ) = @_; if ($runner_init) { no strict 'refs'; no warnings 'redefine'; *{"$importer\::done_testing"} = sub { $importer->FENNEC->post(@_) if @_; return 1; }; } else { no strict 'refs'; no warnings 'redefine'; my $has_run = 0; *{"$importer\::done_testing"} = sub { croak "done_testing() called more than once!" if $has_run++; Fennec::EndRunner->set_runner(undef); $importer->FENNEC->post(@_) if @_; $runner->run(); 1; }; } } 1; __END__ =pod =head1 NAME Fennec - A testers toolbox, and best friend =head1 DESCRIPTION Fennec ties together several testing related modules and enhances their functionality in ways you don't get loading them individually. Fennec makes testing easier, and more useful. =head2 CAVEAT EMPTOR This module is deprecated in favor of L, specifically L and L. =head1 SYNOPSIS There are 2 ways to use Fennec. You can use Fennec directly, or you can use the shiny sugar-coated interface provided by the add-on module L. =head2 VANILLA SYNTAX If L and its awesome power of syntax specification scares you, you can always write your Fennec tests in the stone age like this... just don't miss any semicolons. t/some_test.t: package TEST::SomeTest; use strict; use warnings; use Fennec( parallel => 3, test_sort => 'random', ); # This is optional, there is a default 'new' if you do not override it. sub new { ... } # Test blocks are called as methods on an instance of your test package. tests group_1 => sub { my $self = shift; ok( 1, "1 is true" ); }; test group_2 => ( todo => 'This is not ready yet', code => sub { my $self = shift; ok( 0, "Not ready" ); } ); # It is important to always end a Fennec test with this function call. done_testing(); =head2 DECLARE SYNTAX B In order to use this you B install L which is a separate distribution on cpan. This module is separate because it uses the controversial L module. t/some_test.t: package TEST::SomeTest; use strict; use warnings; use Fennec::Declare( parallel => 3, test_sort => 'random', ); # This is optional, there is a default 'new' if you do not override it. sub new { ... } # Test blocks are called as methods on an instance of your test package. tests group_1 { # Note: $self is automatically shifted for you. ok( $self, "Got self automatically" ); }; test group_2 ( todo => 'This is not ready yet' ) { # Note: $self is automatically shifted for you. ok( 0, "Not ready" ); } # It is important to always end a Fennec test with this function call. done_testing; =head1 FEATURES =head2 PROVIDED DIRECTLY BY FENNEC =over 4 =item Forking just works Forking in perl tests that use L is perilous at best. Fennec initiates an L class which sets up Test::Builder to funnel all test results to the main thread for rendering. A result of this is that forking just works. =item Concurrency, test blocks can run in parallel By default all C blocks are run in parallel with a cap of 3 concurrent processes. The process cap can be set with the C import argument. =item No need to maintain a test count The test count traditionally was used to ensure your file finished running instead of exiting silently too early. With L and friends this has largely been replaced with the C function typically called at the end of tests. Fennec shares this concept, but takes it further, you MUST call C at the end of your test files. This is safer because it can be used to ensure your test script ran completely. =item Can be decoupled from Test::Builder Fennec is built with the assumption that L and tools built from it will be used. However custom L and L classes can replace this assumption with any testing framework you want to use. =item Can run specific test blocks, excluding others Have you ever had a huge test that took a long time to run? Have you ever needed to debug a failing test at the end of the file? How many times did you need to sit through tests that didn't matter? With Fennec you can specify the C environment variable with either a line number or test block name. Only tests defined on that line, or with that name will be run. =item Predictability: Rand is always seeded with the date Randomizing the order in which test blocks are run can help find subtle interaction bugs. At the same time if tests are always in random order you cannot reliably reproduce a failure. Fennec always seeds rand with the current date. This means that on any given date the test run order will always be the same. However different days test different orders. You can always specify the C environment variable to override the value used to seed rand. =item Diag output is coupled with test output When you run a Fennec test with a verbose harness (prove -v) the diagnostic output will be coupled with the TAP output. This is done by sending both output to STDOUT. In a non-verbose harness the diagnostics will be sent to STDERR per usual. =item Works with Moose All your test classes are instantiated objects. You can use Moose to define these test classes. But you do not have to, you are not forced to use OOP in your tests. =back =head2 PROVIDED BY MODULES LOADED BY FENNEC =over 4 =item The 3 most common and useful Test::* modules L, L, L =item RSPEC support Those familiar with Ruby may already know about the RSPEC testing process. In general you C something that is to be tested, then you define setup and teardown methods (C, C, C, C) and then finally you test C. See the L section or L for more details. =item Test re-ordering, tests can run in random, sorted, or defined order. When you load Fennec you can specify a test order. The default is random. You can also use the order in which they are defined, or sorted (alphabetically) order. If necessary you can pass in a sorting function that takes a list of all test-objects as arguments. I =item Reusable test modules You can write tests in modules using L and then import those tests into Fennec tests. This is useful if you have tests that you want run in several, or even all test files. I =item Incredibly powerful mocking with a simple API You can create classless object instances from a specification on the fly, define new packages, or override existing packages. I =back =head1 DEFAULT IMPORTED MODULES B These can be overridden either on import, or by subclassing Fennec. =over 4 =item Child - Forking for dummies Child is an OO interface to forking that removes all the boilderplate such as checking if the pid changed, and making sure you exit the child process. =item Mock::Quick - Mocking without the eye gouging L is a mocking library that makes mocking easy. In addition it uses a declarative style interface. Unlike most other mocking libraries on CPAN, it does not make people want to gouge their eyes out and curl up in the fetal position. =item Test::Workflow - RSPEC for perl. L is a testing library written specifically for Fennec. This library provides RSPEC workflow functions and structure. It can be useful on its own, but combined with Fennec it gets concurrency. =item Test::More Tried and True testing module that everyone uses. =item Test::Warn L - Test code that issues warnings. =item Test::Exception L - Test code that throws exceptions =back =head1 IMPORT ARGUMENTS =over 4 =item base => 'Some::Base' Load the specified module and make it the base class for your test class. =item class => 'What::To::Test' Used to specify the name of the package your test file is validating. When this parameter is specified 3 things are done for you: The class is automatically loaded, the $CLASS variable is imported and contains the module name, and the class() subroutine is defined and returns the name. use Fennec class => 'Foo::Bar'; ok( $INC{'Foo/Bar.pm'}, "Loaded 'Foo::Bar'" ); is( $CLASS, 'Foo::Bar', "We have \$CLASS" ); is( class(), 'Foo::Bar', "We have class()" ); tests method => sub { my $self = shift; is( $self->class(), 'Foo::Bar', "We have class() method" ); }; done_testing; =item parallel => $PROC_LIMIT How many test blocks can be run in parallel. Default is 3. Set to 1 to fork for each test, but only run one at a time. Set to 0 to prevent forking. You can also set this using the C<$FENNEC_PARALLEL> environment variable. =item debug => 1 Enable tracking debugging information. At the end of the Fennec run it will present you with a CSV temp file. This file lists all blocks that are run, and mocks that are made in sequence from top to bottom. The actions are split into columns by PID. This is usedul when debugging potential race-conditions when using parallel testing. Example: 26150,26151,26152,26153,26154 0 26150 BLOCK 54->78 child: outer_wrap, , , , , ,1 26151 BLOCK 47->52 test: class_store, , , , 0 26150 MOCK Foo => (outer), , , , , 0 26150 BLOCK 58->61 before_all: ba, , , , , , ,2 26152 MOCK Foo => (outer), , , , ,2 26152 BLOCK 63->66 before_each: be, , , , ,2 26152 BLOCK 68->72 test: the_check, , , , , ,3 26153 BLOCK 16->31 test: object, , , , , ,4 26154 BLOCK 33->45 test: class, You can use this in a spreadsheet program, or use this command to look at it in a more friendly way. column -s, -t < '/path/to/tempfile' | less -#2 -S =item collector_class => 'Fennec::Collector::TB::TempFiles' Specify which collector to use. Defaults to a Test::Builder based collector that uses temp files to funnel tests from child procs to the parent. You generally won't need to specify this, unless you use a test infrastructure that is neither TAP nore Test::Builder based. =item runner_class => 'Fennec::Runner' Specify the runner class. You probably don't need this. =item runner_params => { ... } Lets you specify arguments used when Fennec::Runner is initialized. =item skip_without => [ 'Need::This', 'And::This' ] Tell Fennec to skip the test file if any of the specified modules are missing. =item test_sort => $SORT Options: 'random', 'sorted', 'ordered', or a code block. Code block accepts a list of Test::Workflow::Test objects. =item utils => [ 'Test::Foo', ... ] Load these modules instead of the default list. If you need to specify import arguments for any specific util class, you can use the class name as the key with an arrayref containing the arguments. use Fennec( utils => [ 'Some::Module' ], 'Some::Module' => [ arg => $val, ... ], ); =item with_tests => [ 'Reusable::Tests', 'Common::Tests' ] Load these modules that have reusable tests. Reusable tests are tests that are common to multiple test files. =item seed => '...' Set the random seed to be used. Defaults to current date, can be overridden by the FENNEC_SEED environment variable. =back =head1 ENVIRONMENT VARIABLES =over 4 =item FENNEC_SEED Can be used to set a specific random seed =item FENNEC_TEST Can be used to tell Fennec to only run specific tests (can be given a line number or a block name). =item FENNEC_DEBUG When true internal debugging is turned on. =back =head1 EXPORTED FUNCTIONS =head2 FROM FENNEC =over 4 =item done_testing() =item done_testing(sub { ... }) Should be called at the end of your test file to kick off the RSPEC tests. Always returns 1, so you can use it as the last statement of your module. You must only ever call this once per test file. B put tests below the done_testing call. If you want tests to run AFTER the RSPEC workflow completes, you can pass done_testing a coderef with the tests. done_testing( sub { ok( 1, "This runs after the RSPEC workflow" ); }); =back =head2 FROM Test::Workflow See L or L for more details. =over 4 =item with_tests 'Module::Name'; Import tests from a module =item tests $name => sub { ... }; =item tests $name => ( %params ); =item it $name => sub { ... }; =item it $name => ( %params ); Define a test block =item describe $name => sub { ... }; Describe a set of tests (group tests and setup/teardown functions) =item case $name => sub { ... }; Used to run a set of tests against multiple conditions =item before_all $name => sub { ... }; Setup, run once before any tests in the describe scope run. =item before_case $name => sub { ... }; Setup, run before any case blocks are run. =item before_each $name => sub { ... }; =item after_case $name => sub { ... }; Setup, run once per test, just before it runs. Both run after the case block (if there is one). =item around_each $name => sub { ... }; Setup and/or teardown. =item after_each $name => sub { ... }; Teardown, run once per test, after it finishes. =item after_all $name => sub { ... }; Teardown, run once, after all tests in the describe scope complete. =back =head2 FROM Mock::Quick See L or L for more details. =over 4 =item my $control = qclass $CLASS => ( %PARAMS, %OVERRIDES ); =item my $control = qtakeover $CLASS => ( %PARAMS, %OVERRIDES ); =item my $control = qimplement $CLASS => ( %PARAMS, %OVERRIDES ); =item my $control = qcontrol $CLASS => ( %PARAMS, %OVERRIDES ); Used to define, takeover, or override parts of other packages. =item my $obj = qobj( %PARAMS ); =item my ( $obj, $control ) = qobjc( %PARAMS ); =item my $obj = qstrict( %PARAMS ); =item my ( $obj, $control ) = qstrictc( %PARAMS ); Define an object specification, quickly. =item my $clear = qclear(); Used to clear a field in a quick object. =item my $meth = qmeth { ... }; =item my $meth = qmeth( sub { ... } ); Used to define a method for a quick object. =back =head2 OTHER See L, L, and L =head1 EXAMPLES Examples can be the best form of documentation. =head2 SIMPLE =head3 VANILLA SYNTAX t/simple.t use strict; use warnings; use Fennec; use_ok 'Data::Dumper'; tests dumper => sub { my $VAR1; is_deeply( eval Dumper({ a => 1 }), { a => 1 }, "Serialize and De-Serialize" ); }; tests future => ( todo => "Not ready yet", code => sub { ok( 0, "I still have to write these" ); }, ); done_testing; =head3 DECLARE SYNTAX t/simple.t use strict; use warnings; use Fennec::Declare; use_ok 'Data::Dumper'; tests dumper { my $VAR1; is_deeply( eval Dumper({ a => 1 }), { a => 1 }, "Serialize and De-Serialize" ); is( eval { no strict; Dumper( { a => 1 } ) }, { a => 1 }, "Serialize and De-Serialize" ); } tests future( todo => "Not ready yet" ) { ok( 0, "I still have to write these" ); } done_testing; =head2 RUN TESTS UNDER DIFFERENT CONDITIONS This example shows 4 conditions (C<$letter> as 'a', 'b', 'c', and 'd'). It also has 2 test blocks, one that verifies C<$letter> is a letter, the other verifies it is lowercase. Each test block will be run once for each condition, 2*4=8, so in total 8 tests will be run. =head3 VANILLA sample.t: use strict; use warnings; use Fennec; my $letter; case a => sub { $letter = 'a' }; case b => sub { $letter = 'b' }; case c => sub { $letter = 'c' }; case d => sub { $letter = 'd' }; tests is_letter => sub { like( $letter, qr/^[a-z]$/i, "Got a letter" ); }; tests is_lowercase => sub { is( $letter, lc( $letter ), "Letter is lowercase" ); }; done_testing; =head3 OBJECT ORIENTED sample.t use strict; use warnings; use Fennec; sub letter { my $self = shift; ( $self->{letter} ) = @_ if @_; return $self->{letter}; } describe letters => sub { case a => sub { shift->letter('a') }; case b => sub { shift->letter('b') }; case c => sub { shift->letter('c') }; case d => sub { shift->letter('d') }; tests is_letter => sub { my $self = shift; like( $self->letter, qr/^[a-z]$/i, "Got a letter" ); }; tests is_lowercase => sub { my $self = shift; is( $self->letter, lc( $self->letter ), "Letter is lowercase" ); }; }; done_testing; =head3 DECLARE B no need to shift $self, it is done for you! sample.t use strict; use warnings; use Fennec::Declare; sub letter { my $self = shift; ( $self->{letter} ) = @_ if @_; return $self->{letter}; } describe letters { case a { $self->letter('a') } case b { $self->letter('b') } case c { $self->letter('c') } case d { $self->letter('d') } tests is_letter { like( $self->letter, qr/^[a-z]$/i, "Got a letter" ); } tests is_lowercase { is( $self->letter, lc( $self->letter ), "Letter is lowercase" ); } } done_testing; =head2 MOCKING See L for more details =head3 OBJECT ON THE FLY my $obj = qobj( foo => 'foo', bar => qmeth { 'bar' }, baz => sub { 'baz' }, ); is( $obj->foo, 'foo' ); is( $obj->bar, 'bar' ); is( ref $obj->baz, 'CODE', "baz is a method that returns a coderef" ); # All methods autovivify as read/write accessors: lives_ok { $obj->blah( 'x' ) }; # use qstrict() to make an object that does not autovivify accessors. =head3 SCOPE OF MOCKS IN FENNEC With vanilla L a mock is destroyed when the control object is destroyed. my $control = qtakeover Foo => (blah => 'blah'); is( Foo->blah, 'blah', "got mock" ); $control = undef; ok( !Foo->can('blah'), "Mock destroyed" ); # WITHOUT FENNEC This issues a warning, the $control object is ignored so # the mock is destroyed before it can be used. qtakover Foo => (blah => 'blah'); ok( !Foo->can('blah'), "Mock destroyed before it could be used" ); With the workflow support provided by Fennec, you can omit the control object and let the mock be scoped implicitly. tests implicit_mock_scope => sub { my $self = shift; can_ok( $self, 'QINTERCEPT' ); qtakeover Foo => (blah => sub { 'blah' }); is( Foo->blah, 'blah', "Mock not auto-destroyed" ); }; describe detailed_implicit_mock_scope => sub { qtakeover Foo => ( outer => 'outer' ); ok( !Foo->can( 'outer' ), "No Leak" ); before_all ba => sub { qtakeover Foo => ( ba => 'ba' ); can_ok( 'Foo', qw/outer ba/ ); }; before_each be => sub { qtakeover Foo => ( be => 'be' ); can_ok( 'Foo', qw/outer ba be/ ); }; tests the_check => sub { qtakeover Foo => ( inner => 'inner' ); can_ok( 'Foo', qw/outer ba be inner/ ); }; ok( !Foo->can( 'outer' ), "No Leak" ); ok( !Foo->can( 'ba' ), "No Leak" ); ok( !Foo->can( 'be' ), "No Leak" ); ok( !Foo->can( 'inner' ), "No Leak" ); }; =head3 TAKEOVER AN EXISTING CLASS require Some::Class; my $control = qtakeover 'Some::Class' => ( # Override some methods: foo => sub { 'foo' }, bar => sub { 'bar' }, # For methods that return a simple value you don't actually need to # wrap them in a sub. baz => 'bat', ); is( Some::Class->foo, 'foo' ); is( Some::Class->bar, 'bar' ); # Use the control object to make another override $control->override( foo => 'FOO' ); is( Some::Class->foo, 'FOO' ); # Original class is restored when $control falls out of scope. $control = undef; =head3 MOCK A CLASS INSTEAD OF LOADING THE REAL ONE This will prevent the real class from loading if code tries to C or C it. However when the control object falls out of scope you will be able to load the real one again. my $control = qimplement 'Some::Class' => ( my_method => sub { ... } simple => 'foo', ); =head3 MOCK AN ANONYMOUS CLASS my $control = qclass( -with_new => 1, # Make a constructor for us method => sub { ... }, simple => 'foo', ); my $obj = $control->package->new; =head2 REUSABLE TEST LIBRARIES This is a test library that verifies your test file uses strict in the first 3 lines. You can also pass C [ 'Some::Test::Lib' ]> as an import argument to Fennec. This matters because you can subclass Fennec to always include this library. t/test.t use strict; use warnings; use Fennec; with_tests 'Some::Test::Lib'; done_testing; lib/Some/Test/Lib.pm package Some::Test::Lib; use Test::Workflow; use Test::More; use Scalar::Util qw/blessed/; tests check_use_strict => sub { my $self = shift; my $class = blessed $self; my $file = $class; $file =~ s{::}{/}g; $file .= '.pm'; my $full = $INC{$file}; ok( -e $full, "Found path and filename for $class" ); open( my $fh, '<', $full ) || die $!; my $found = 0; for ( 1 .. 3 ) { $found = <$fh> =~ m/^\s*use strict;\s*$/; last if $found; } close($fh); ok( $found, "'use strict;' is in the first 3 lines of the test file" ); } 1; =head2 POST TESTS You cannot put any tests under C Doing so will cause problems. However you can put tests IN done_testing. use strict; use warnings; use Fennec; my $foo = 1; is( $foo, 1, "foo is 1" ); done_testing( sub { is( $foo, 1, "foo is still 1" ); } ); =head2 RSPEC The following test will produce output similar to the following. Keep in mind that the concurrent nature of Fennec means that the lines for each process may appear out of order relative to lines from other processes. Lines for any given process will always be in the correct order though. Spacing has been added, and process output has been grouped together, except for the main process to demonstrate that after_all really does come last. # PID OUTPUT #--------------------------------------------- 7253 describe runs long before everything else 7253 before_all runs first 7254 case runs between before_all and before_each 7254 before_each runs just before tests 7254 tests run in the middle 7254 after_each runs just after tests 7255 before_each runs just before tests 7255 This test inherits the before and after blocks from the parent describe. 7255 after_each runs just after tests 7253 after_all runs last. sample.t use strict; use warnings; use Fennec; describe order => sub { print "$$ describe runs long before everything else\n"; before_all setup_a => sub { print "$$ before_all runs first\n"; }; case a_case => sub { print "$$ case runs between before_all and before_each\n"; }; before_each setup_b => sub { print "$$ before_each runs just before tests\n"; }; tests a_test => sub { print "$$ tests run in the middle\n"; }; after_each teardown_b => sub { print "$$ after_each runs just after tests\n"; }; after_all teardown_a => sub { print "$$ after_all runs last.\n"; }; describe nested => sub { tests b_test => sub { print "$$ This test inherits the before/after/case blocks from the parent describe.\n"; }; }; }; done_testing; =head1 MANUAL The manual can be found here: L it is a sort of Nexus for documentation, including this document. =head1 VIM INTEGRATION Insert this into your .vimrc file to bind the F8 key to running the test block directly under your cursor. You can be on any line of the test block (except in some cases the first or last line. function! RunFennecLine() let cur_line = line(".") exe "!FENNEC_TEST='" . cur_line . "' prove -v -I lib %" endfunction " Go to command mode, save the file, run the current test :map :w:call RunFennecLine() :imap :w:call RunFennecLine() =head1 RUNNING FENNEC TEST FILES IN PARALLEL The best option is to use prove with the -j flag. B You can also create a custom runner using a single .t file to run all your Fennec tests. This has caveats though, such as not knowing which test file had problems without checking the failure messages. This will find all *.ft and/or *.pm modules under the t/ directory. It will load and run any found. These will be run in parallel t/runner.t #!/usr/bin/perl use strict; use warnings; # Paths are optional, if none are specified it defaults to 't/' use Fennec::Finder( 't/' ); # The next lines are optional, if you have no custom configuration to apply # you can jump right to 'done_testing'. # Get the runner (singleton) my $runner = Fennec::Finder->new; $runner->parallel( 3 ); # You must call this. run(); =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Fennec is free software; Standard perl license (GPL and Artistic). Fennec is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/lib/Fennec000755001750001750 013306515055 15212 5ustar00exodistexodist000000000000Fennec-2.018/lib/Fennec/Collector.pm000444001750001750 1012513306515055 17652 0ustar00exodistexodist000000000000package Fennec::Collector; use strict; use warnings; use Carp qw/confess/; use Fennec::Util qw/accessors require_module/; use File::Temp qw/tempfile/; accessors qw/test_count test_failed debug_data/; sub ok { confess "Must override ok" } sub diag { confess "Must override diag" } sub end_pid { confess "Must override end_pid" } sub collect { confess "Must override collect" } sub init { } sub new { my $class = shift; my %params = @_; my $self = bless \%params, $class; $self->debug_data([]); $self->init; return $self; } sub inc_test_count { my $self = shift; my $count = $self->test_count || 0; $self->test_count( $count + 1 ); } sub inc_test_failed { my $self = shift; my $count = $self->test_failed || 0; $self->test_failed( $count + 1 ); } sub debug { my $self = shift; my ($msg) = @_; my ($action, $data) = $msg =~ m/^ ?# ?FENNEC_DEBUG_(MOCK|BLOCK|CUSTOM):(.*)$/; my $set = { ACTION => $action }; for my $field (split "\0", $data) { my ($key, $val) = $field =~ m/([^:]+):(.*)/; $set->{lc($key)} = $val; } push @{$self->debug_data} => $set; } sub finish { my $self = shift; return unless @{$self->debug_data}; my @data = sort { return $a->{sec} <=> $b->{sec} || $a->{msec} <=> $b->{msec} } @{ $self->debug_data }; my $index = 0; my $map = { $$ => $index++ }; my @out; for my $item (@data) { $map->{$item->{pid}} = $index++ unless defined $map->{$item->{pid}}; my $idx = $map->{$item->{pid}}; if ($item->{ACTION} eq 'MOCK') { push @out => [ $idx, "MOCK $item->{class} => ($item->{overrides})" ]; } elsif ($item->{ACTION} eq 'BLOCK') { push @out => [ $idx, "BLOCK $item->{start_line}\->$item->{end_line} $item->{type}: $item->{name} ($item->{state})" ]; } else { push @out => [ $idx, "CUSTOM: $item->{message}" ]; } } my @pids = sort { $map->{$a} <=> $map->{$b} } keys %$map; my ($fh, $filename) = tempfile( CLEANUP => 0 ); print $fh join "," => @pids; print $fh "\n"; for my $row (@out) { print $fh " ," x $row->[0]; print $fh $row->[1]; print $fh ", " x ($index - $row->[0]); print $fh "\n"; } close($fh); print "# See $filename for process debugging\n"; print "# Try column -s, -t < '$filename' | less -#2 -S\n"; } 1; __END__ =head1 NAME Fennec::Collector - Funnel results from child to parent =head1 DESCRIPTION The collector is responsible for 2 jobs: 1) In the parent process it is responsible for gathering all test results from the child processes. 2) In the child processes it is responsible for sending results to the parent process. =head1 METHODS SUBCLASSES MUST OVERRIDE =over 4 =item $bool = ok( $bool, $description ) Fennec sometimes needs to report the result of an internal check. These checks will pass a boolean true/false value and a description. =item diag( $msg ) Fennec uses this to report internal diagnostics messages =item end_pid Called just before a child process exits. =item collect Used by the parent process at an interval to get results from children and display them. =back =head1 METHODS SUBCLASSES MAY OVERRIDE =over 4 =item new Builds the object from params, then calls init. =item init Called by new =item finish Called at the very end of C no tests should be reported after this. =back =head1 METHODS SUBCLASSES MUST BE AWARE OF =over 4 =item test_count Holds the test count so far. =item test_failed Holds the number of tests failed so far. =item inc_test_count Used to add 1 to the number of tests. =item inc_test_failed Used to add 1 to the number of failed tests. =back =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Fennec is free software; Standard perl license (GPL and Artistic). Fennec is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/lib/Fennec/EndRunner.pm000444001750001750 464113306515055 17612 0ustar00exodistexodist000000000000package Fennec::EndRunner; use strict; use warnings; my $RUNNER; my $PID; sub set_runner { $RUNNER = pop if @_; return $RUNNER; } sub set_pid { $PID = pop if @_; return $PID; } END { return unless $PID && $PID == $$; return if $?; return unless $RUNNER; return if $RUNNER->_skip_all; return if $^C; # Do not print this message if perl is called with -c print STDERR <<" EOT"; ############################################################################### # **** It does not look like done_testing() was ever called! **** # # # # As of Fennec 2 automatically-running standalone fennect tests are # # deprecated. This descision was made because all run after run-time # # methods are hacky and/or qwerky. # # # # Since there are so many legacy Fennec tests that relied on this behavior # # it has been carried forward in this deprecated form. An END block has # # been used to display this message, and will next run your tests. # # # # For most legacy tests this should work fine, however it may cause issues # # with any tests that relied on other END blocks, or various hacky things. # # # # DO NOT RELY ON THIS BEHAVIOR - It may go away in the near future. # ############################################################################### EOT $RUNNER->run(); my $failed = $RUNNER->collector->test_failed; return unless $failed; $? = $failed; } 1; __END__ =head1 NAME Fennec::EndRunner - Used to run Fennec test when legacy code does not call done_testing(). =head1 DESCRIPTION Fennec::EndRunner - Used to run Fennec test when legacy code does not call done_testing(). Basically a big ugly deprecated END block. =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Fennec is free software; Standard perl license. Fennec is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/lib/Fennec/Finder.pm000444001750001750 710213306515055 17114 0ustar00exodistexodist000000000000package Fennec::Finder; use strict; use warnings; use base 'Fennec::Runner'; use File::Find qw/find/; use Fennec::Util qw/accessors verbose_message/; use List::Util qw/shuffle/; accessors qw/test_files parallel/; sub import { my $self = shift->new; $self->find_files(@_); $self->inject_run( scalar caller ); } sub init { my $self = shift; my (%params) = @_; $self->test_files( [] ); $self->parallel( defined $params{parallel} ? $params{parallel} : 2 ); return $self; } sub find_files { my $self = shift; my @paths = @_ ? @_ : -d './t' ? ('./t') : ('./'); find( { wanted => sub { my $file = $File::Find::name; return unless $self->validate_file($file); push @{$self->test_files} => $file; }, no_chdir => 1, }, @paths ); } sub validate_file { my $self = shift; my ($file) = @_; return unless $file =~ m/\.(pm|ft)$/; return 1; } sub run { my $self = shift; my ($follow) = @_; $self->_ran(1); my $frunner = $self->prunner( $self->parallel ); for my $file ( @{$self->test_files} ) { $frunner->run( sub { $self->load_file($file); for my $class ( shuffle @{$self->test_classes} ) { next unless $class; $self->run_test_class($class); } }, 1 ); $self->check_pid; } $frunner->finish(); if ($follow) { $self->collector->collect; verbose_message("Entering final follow-up stage\n"); eval { $follow->(); 1 } || $self->exception( 'done_testing', $@ ); } $self->collector->collect; $self->collector->finish(); } 1; __END__ =pod =head1 NAME Fennec::Finder - Create one .t file to find all .pm test files. =head1 DESCRIPTION Originally Fennec made use of a runner loaded in t/Fennec.t that sought out test files (modules) to run. This modules provides similar, but greatly simplified functionality. =head1 SYNOPSIS Fennec.t: #!/usr/bin/perl use strict; use warnings; use Fennec::Finder; run(); This will find all .pm and .ft files under t/ and load them. Any that contain Fennec tests will register themselves to be run once run() is called. B =head1 CUSTOMISATIONS =head2 SEARCH PATHS When you C the './t/' directory will be searched if it exists, otherwise the './' directory will be used. You may optionally provide alternate paths at use time: C #!/usr/bin/perl use strict; use warnings; use Fennec::Finder './Fennec', './SomeDir'; run(); =head2 FILE VALIDATION If you wish to customize which files are loaded you may subclass L and override the C method. This method takes the filename to verify as an argument. Return true if the file should be loaded, false if it should not. Currently the only check is that the filename ends with a C<.pm>. =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Fennec is free software; Standard perl license. Fennec is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. =cut Fennec-2.018/lib/Fennec/Manual.pod000444001750001750 305713306515055 17275 0ustar00exodistexodist000000000000 =head1 NAME Fennec::Manual - The Fennec Manual =head1 COMPONENT DOCS =over 4 =item Fennec L - The main glue and interface module. =item Fennec::Declare B L is a separate distribution, and must be installed separately. L - Provides the syntax sugar. =item Test::Workflow L - Provides RSPEC utilities. =item Mock::Quick L - Provides the mocking capabilities. =item Child L - Provides the forking interface =item Parallel::Runner L - Used to manage concurrency. =item Exporter::Declare L - The exporting interface used in most components of Fennec. =back =head1 CUSTOMIZATION See the L documentation. =head1 SEE ALSO These are modules loaded by Fennec, but not built as part of the Fennec project. =over 4 =item Test::Builder L - The popular TAP outputting testing library. =item Test::More L - The most commonly used testing tools. =item Test::Warn L - Check your code that produces warnings. =item Test::Exception L - Check your code that throws exceptions. =back =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Fennec is free software; Standard perl license (GPL and Artistic). Fennec is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/lib/Fennec/Meta.pm000444001750001750 253713306515055 16602 0ustar00exodistexodist000000000000package Fennec::Meta; use strict; use warnings; use Fennec::Util qw/accessors/; accessors qw/parallel class fennec base test_sort with_tests post debug/; sub new { my $class = shift; my %proto = @_; bless( { $proto{fennec}->defaults(), %proto, }, $class ); } 1; __END__ =head1 NAME Fennec::Meta - The meta-object added to all Fennec test classes. =head1 DESCRIPTION When you C a function is added to you class named 'FENNEC' that returns the single Fennec meta-object that tracks information about your class. =head1 ATTRIBUTES =over 4 =item parallel Maximum number of parallel tests that can be run for your class. =item class Name of your class. =item fennec Name of the class that was used to load fennec (usually 'Fennec') =item base Base class Fennec put in place, if any. =item test_sort What method of test sorting was specified, if any. =item with_tests List of test templates loaded into your class. =back =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Fennec is free software; Standard perl license. Fennec is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/lib/Fennec/Runner.pm000444001750001750 1502413306515055 17200 0ustar00exodistexodist000000000000package Fennec::Runner; use strict; use warnings; use Fennec::Util qw/verbose_message/; BEGIN { my @ltime = localtime; $ltime[5] += 1900; $ltime[4] += 1; # months start at 0? for ( 3, 4 ) { $ltime[4] = "0$ltime[$_]" unless $ltime[$_] > 9; } my $seed = $ENV{FENNEC_SEED} || join( '', @ltime[5, 4, 3] ); verbose_message("\n*** Seeding random with date ($seed) ***\n"); srand($seed); } use Cwd qw/abs_path/; use Carp qw/carp croak confess/; use List::Util qw/shuffle/; use Scalar::Util qw/blessed/; use Fennec::Util qw/accessors require_module/; use Fennec::Collector::TB::TempFiles; use Parallel::Runner; accessors qw/pid test_classes collector _ran _skip_all/; my $SINGLETON; sub is_initialized { $SINGLETON ? 1 : 0 } sub init { } sub import { my $self = shift->new(); return unless @_; $self->_load_guess($_) for @_; $self->inject_run( scalar caller ); } sub inject_run { my $self = shift; my ( $caller, $sub ) = @_; $sub ||= sub { $self->run(@_) }; require Fennec::Util; Fennec::Util::inject_sub( $caller, 'run', $sub ); } sub new { my $class = shift; my @caller = caller; croak "listener_class is deprecated, it was thought nobody used it... sorry. See Fennec::Collector now" if $class->can('listener_class'); croak "Runner was already initialized!" if $SINGLETON && @_; return $SINGLETON if $SINGLETON; my %params = @_; my $collector_class = $params{collector_class} || 'Fennec::Collector::TB::TempFiles'; my $collector = $collector_class->new(); $SINGLETON = bless( { test_classes => [], pid => $$, collector => $collector, }, $class ); $SINGLETON->init(%params); return $SINGLETON; } sub _load_guess { my $self = shift; my ($item) = @_; if ( ref $item && ref $item eq 'CODE' ) { $self->_load_guess($_) for ( $self->$item ); return; } return $self->load_file($item) if $item =~ m/\.(pm|t|pl|ft)$/i || $item =~ m{/}; return $self->load_module($item) if $item =~ m/::/ || $item =~ m/^\w[\w\d_]+$/; die "Not sure how to load '$item'\n"; } sub load_file { my $self = shift; my ($file) = @_; print "Loading: $file\n"; eval { require $file; 1 } || $self->exception( $file, $@ ); } sub load_module { my $self = shift; my $module = shift; print "Loading: $module\n"; eval { require_module $module } || $self->exception( $module, $@ ); } sub check_pid { my $self = shift; return unless $self->pid != $$; die "PID has changed! Did you forget to exit a child process?\n"; } sub exception { my $self = shift; my ( $name, $exception ) = @_; if ( $exception =~ m/^FENNEC_SKIP: (.*)\n/ ) { $self->collector->ok( 1, "SKIPPING $name: $1" ); $self->_skip_all(1); } else { $self->collector->ok( 0, $name ); $self->collector->diag($exception); } } sub prunner { my $self = shift; my ($max) = @_; my $runner = Parallel::Runner->new($max); $runner->reap_callback( sub { my ( $status, $pid, $pid_again, $proc ) = @_; # Status as returned from system, so 0 is good, 1+ is bad. $self->exception( "Child process did not exit cleanly", "Status: $status" ) if $status; } ); $runner->iteration_callback( sub { $self->collector->collect } ); return $runner; } sub run { my $self = shift; my ($follow) = @_; $self->_ran(1); for my $class ( shuffle @{$self->test_classes} ) { next unless $class; $self->run_test_class($class); $self->check_pid; } if ($follow) { $self->collector->collect; verbose_message("Entering final follow-up stage\n"); $follow->(); } $self->collector->collect; $self->collector->finish(); } sub run_test_class { my $self = shift; my ($class) = @_; return unless $class; verbose_message("Entering workflow stage: $class\n"); return unless $class->can('TEST_WORKFLOW'); my $instance = $class->can('new') ? $class->new : bless( {}, $class ); my $ptests = $self->prunner( $class->FENNEC->parallel ); my $pforce = $class->FENNEC->parallel ? 1 : 0; my $meta = $instance->TEST_WORKFLOW; my $orig_cwd = abs_path; $meta->test_wait( sub { $ptests->finish } ); $meta->test_run( sub { my ($run) = @_; $ptests->run( sub { chdir $orig_cwd; local %ENV = %ENV; $run->(); $self->collector->end_pid(); }, $pforce ); } ); Test::Workflow::run_tests($instance); $ptests->finish; if ( my $post = $class->FENNEC->post ) { $self->collector->collect; verbose_message("Entering follow-up stage: $class\n"); eval { $post->(); 1 } || $self->exception( 'done_testing', $@ ); } } sub DESTROY { my $self = shift; return unless $self->pid == $$; return if $self->_ran; return if $self->_skip_all; return if $^C; # No warning in syntax check my $tests = join "\n" => map { "# * $_" } @{$self->test_classes}; print STDERR <<" EOT"; # ***************************************************************************** # ERROR: done_testing() was never called! # # This usually means you ran a Fennec test file directly with prove or perl, # but the file does not call done_testing at the end. # # Fennec Tests loaded, but not run: $tests # # ***************************************************************************** EOT exit(1); } # Set exit code to failed tests my $PID = $$; END { return if $?; return unless $SINGLETON; return unless $PID == $$; my $failed = $SINGLETON->collector->test_failed; return unless $failed; $? = $failed; } 1; __END__ =head1 NAME Fennec::Runner - Responsible for Test::Workflow interaction =head1 DESCRIPTION Handles L processing and concurrency. This class is a singleton instantiated by import() or new(), whichever comes first. =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Fennec is free software; Standard perl license. Fennec is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/lib/Fennec/Test.pm000444001750001750 121213306515055 16620 0ustar00exodistexodist000000000000package Fennec::Test; use strict; use warnings; 1; __END__ =pod =head1 NAME Fennec::Test - Base class for Fennec tests. =head1 DESCRIPTION Base class for Fennec tests. Currently this is an empty base class, it is simply useful for checking C<$Thing-Eisa( 'Fennec::Test' );> =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Fennec is free software; Standard perl license. Fennec is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. =cut Fennec-2.018/lib/Fennec/Util.pm000444001750001750 702013306515055 16621 0ustar00exodistexodist000000000000package Fennec::Util; use strict; use warnings; use Exporter::Declare; use Carp qw/croak/; use Scalar::Util qw/blessed/; exports qw{ inject_sub accessors get_test_call require_module verbose_message }; sub inject_sub { my ( $package, $name, $code ) = @_; croak "inject_sub() takes a package, a name, and a coderef" unless $package && $name && $code && $code =~ /CODE/; no strict 'refs'; *{"$package\::$name"} = $code; } sub accessors { my $caller = caller; _accessor( $caller, $_ ) for @_; } sub require_module { my $module = shift; # Is it defined? croak "No module specified" unless defined $module; # Is the caller using utf8? require utf8; my $with_utf8 = ( caller(0) )[8] & $utf8::hint_bits; # Are Unicode package names ok? my $check = $with_utf8 ? qr{\A [[:alpha:]_] [[:word:]]* (?: :: [[:word:]]+ )* \z}x : qr{\A [A-Z_a-z] [0-9A-Z_a-z]* (?: :: [0-9A-Z_a-z]+ )* \z}x; # Is it a syntactically valid module name? croak "Invalid Module '$module'" unless $module =~ $check; # Transform to a pm file path my $file = $module; $file .= ".pm"; $file =~ s{::}{/}g; # What were we doing again? return require $file; } sub _accessor { my ( $caller, $attribute ) = @_; inject_sub( $caller, $attribute, sub { my $self = shift; croak "$attribute() called on '$self' instead of an instance" unless blessed($self); ( $self->{$attribute} ) = @_ if @_; return $self->{$attribute}; } ); } sub get_test_call { my $runner; my $i = 1; while ( my @call = caller( $i++ ) ) { $runner = \@call if !$runner && $call[0]->isa('Fennec::Runner'); return @call if $call[0]->can('FENNEC'); } return ( $runner ? @$runner : ( "UNKNOWN", "UNKNOWN", 0 ) ); } sub verbose_message { return if $ENV{HARNESS_ACTIVE} && !$ENV{HARNESS_IS_VERBOSE}; # Do not print the messages on syntax check return if $^C; print @_; } sub tb_ok { Test::Builder->new->ok(@_) } sub tb_diag { Test::Builder->new->diag(@_) } sub tb_skip { Test::Builder->new->skip(@_) } sub tb_todo_start { Test::Builder->new->todo_start(@_) } sub tb_todo_end { Test::Builder->new->todo_end } 1; __END__ =head1 NAME Fennec::Util - Utility functions =head1 DESCRIPTION This class provides useful utility functions used all over Fennec. =head1 EXPORTS =over 4 =item require_module( 'Some::Module' ) Can be used to load modules stored in strings. =item inject_sub( $package, $name, $code ) Inject a sub into a package. =item accessors( @attributes ) Generate basic accessors for the given attributes into the calling package. =item @call = get_test_call() Look back through the stack and find the last call that took place in a test class. =back =head1 API STABILITY Fennec versions below 1.000 were considered experimental, and the API was subject to change. As of version 1.0 the API is considered stabilized. New versions may add functionality, but not remove or significantly alter existing functionality. =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Fennec is free software; Standard perl license. Fennec is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/lib/Fennec/Collector000755001750001750 013306515055 17140 5ustar00exodistexodist000000000000Fennec-2.018/lib/Fennec/Collector/TB.pm000444001750001750 504213306515055 20141 0ustar00exodistexodist000000000000package Fennec::Collector::TB; use strict; use warnings; use Carp qw/confess/; use base 'Fennec::Collector'; use Fennec::Util qw/accessors/; accessors qw/skip/; sub ok { shift; Test::Builder->new->ok(@_) } sub diag { shift; Test::Builder->new->diag(@_) } sub report { confess "Must override report" } sub finish { my $self = shift; $self->SUPER::finish(); my $count = $self->test_count || 0; print STDOUT "1..$count"; print STDOUT " # SKIP " . $self->skip if $self->skip; print STDOUT "\n"; } sub init { my $self = shift; require Test::Builder; my $tbout = tie( *TBOUT, 'Fennec::Collector::TB::_Handle', 'STDOUT', $self ); my $tberr = tie( *TBERR, 'Fennec::Collector::TB::_Handle', 'STDERR', $self ); my $tb = Test::Builder->new(); $tb->use_numbers(0); $tb->no_header(1); $tb->no_ending(1); my $old = select(TBOUT); $| = 1; select(TBERR); $| = 1; select($old); $tb->output( \*TBOUT ); $tb->todo_output( \*TBOUT ); $tb->failure_output( \*TBERR ); } sub render { my $self = shift; my ( $handle, $part ) = @_; if ( $handle eq 'STDOUT' && $part =~ m/^\s*(?:(not)\s+)?ok(\s|$)/ ) { my $fail = $1 && $1 eq 'not' ? 1 : 0; my ( $mod, $reason ) = $part =~ m/ # (TODO|skip) (.*)$/; $self->inc_test_failed if $fail && !$mod; $self->inc_test_count; } return $self->debug($part) if $part =~ m/# FENNEC_DEBUG/; if ( $ENV{HARNESS_IS_VERBOSE} || $handle eq 'STDOUT' ) { print STDOUT "$part\n"; } else { print STDERR "$part\n"; } } package Fennec::Collector::TB::_Handle; use Fennec::Util qw/accessors get_test_call/; accessors qw/name collector/; sub TIEHANDLE { my $class = shift; my ( $name, $collector ) = @_; return bless( {name => $name, collector => $collector}, $class ); } sub PRINT { my $self = shift; my @data = @_; my @call = get_test_call(); $self->collector->report( pid => $$, source => join( ", " => @call[0 .. 2] ), data => \@data, name => $self->name, ); } 1; __END__ =head1 NAME Fennec::Collector::TB - Base class for Test::Builder collectors =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Fennec is free software; Standard perl license (GPL and Artistic). Fennec is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/lib/Fennec/Collector/TB000755001750001750 013306515055 17445 5ustar00exodistexodist000000000000Fennec-2.018/lib/Fennec/Collector/TB/TempFiles.pm000444001750001750 730613306515055 22036 0ustar00exodistexodist000000000000package Fennec::Collector::TB::TempFiles; use strict; use warnings; use base 'Fennec::Collector::TB'; use File::Temp; use Fennec::Util qw/ accessors verbose_message /; accessors qw/tempdir handles tempobj _pid/; sub new { my $class = shift; my $self = $class->SUPER::new(@_); my $temp = File::Temp::tempdir( CLEANUP => 0 ); verbose_message("# Using temp dir: '$temp' for process results\n"); $self->_pid($$); $self->handles( {} ); $self->tempobj($temp); $self->tempdir("$temp"); return $self; } sub report { my $self = shift; my %params = @_; if ( $$ == $self->_pid ) { for my $item ( @{$params{data}} ) { for my $part ( split /\r?\n/, $item ) { $self->render( $params{name}, $part ); } } return; } my $handle; if ( $self->handles->{$$} ) { $handle = $self->handles->{$$}; } else { my $path = $self->tempdir . "/$$"; open( $handle, '>', $path ) || die "$!"; $self->handles->{$$} = $handle; } for my $item ( @{$params{data}} ) { for my $part ( split /\r?\n/, $item ) { print $handle "$params{name}|$params{source}|$part\n"; } } } sub collect { my $self = shift; return unless $self->_pid == $$; my $handle; if ( $self->handles->{tempdir} ) { $handle = $self->handles->{tempdir}; rewinddir $handle; } else { opendir( $handle, $self->tempdir ) || die "$!"; $self->handles->{tempdir} = $handle; } while ( my $file = readdir $handle ) { my $path = $self->tempdir . "/$file"; next unless -f $path; next unless $path =~ m/\.ready$/; open( my $fh, '<', $path ) || die $!; while ( my $line = <$fh> ) { chomp($line); next unless $line; my ( $handle, $source, $part ) = ( $line =~ m/^(\w+)\|([^\|]+)\|(.*)$/g ); warn "Bad Input: '$line'\n" unless $handle && $source; $self->render( $handle, $part ); } close($fh); rename( $path => "$path.done" ) || die "Could not rename file: $!"; } } sub finish { my $self = shift; return unless $self->_pid == $$; $self->ready() if $self->handles->{$$}; $self->collect; $self->SUPER::finish(); my $handle = $self->handles->{tempdir}; rewinddir $handle; die "($$) Not all files were collected?!" if grep { m/^\d+(\.ready)?$/ } readdir $handle; if ( !$ENV{FENNEC_DEBUG} ) { rewinddir $handle; while ( my $file = readdir $handle ) { next unless $file =~ m/\.done$/; unlink( $self->tempdir . '/' . $file ) || warn "error deleting $file: $!"; } close($handle); rmdir( $self->tempdir ) || warn "Could not cleanup temp dir: $!"; } } sub ready { my $self = shift; warn "No Temp Dir! $$" unless $self->tempdir; my $path = $self->tempdir . "/$$"; return unless -e $path; close( $self->handles->{$$} ) || warn "Could not close file $path - $!"; rename( $path => "$path.ready" ) || warn "Could not rename file $path - $!"; } sub end_pid { } sub DESTROY { my $self = shift; $self->ready; } 1; __END__ =head1 NAME Fennec::Collector::TB::TempFiles - Test::Builder collector that uses temporary files to convey results. =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Fennec is free software; Standard perl license (GPL and Artistic). Fennec is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/lib/Fennec/Manual000755001750001750 013306515055 16427 5ustar00exodistexodist000000000000Fennec-2.018/lib/Fennec/Manual/CustomFennec.pod000444001750001750 553213306515055 21666 0ustar00exodistexodist000000000000=head1 NAME Fennec::Manual::CustomFennec - Customizing Fennec for you project. =head1 DESCRIPTION L automatically loads several utilities for you. In addition it assumes you want to use L. Subclassing Fennec will let you specify exactly what utilities you want, and what collector to use. This way you can use your subclass in each test file instead of copying C into each file. =head1 FENNEC SUBCLASS You can subclass Fennec and override the defaults sub to your specification. The defaults sub should return a list of Fennec import argument key-pairs. My/Fennec.pm use strict; use warnings; use base 'Fennec'; sub defaults { my $class = shift; my %params = $class->SUPER::defaults; # Add a new autoloading utility with import arguments. push @{ $params->{utils} } => 'My::Util'; $params->{'My::Util'} = [ 'util' => 'args' ]; # Default number of concurrent procs for the test to use. $params->{parallel} = 3; return %params; } sub after_import { my $class = shift; my ($info) = @_; # $info is a hashref with the importer, runner, and importer meta # object, and some other fun things. # Example of adding cases to any Fennec test that uses this subclass: # The first arg to add case should be an array matching the return of # caller. The idea is to give us the start and end line, as well as # file name where the case is defined. normally the exports from # Test::Workflow provide that for you, but at this low-level we need to # provide it ourselfs. Since we define the subs here, we give current # line/file. Use the importer for package name. $info->{layer}->add_case([$info->{importer}, __FILE__, __LINE__], case_a => sub { $main::CASE_A = 1 }); $info->{layer}->add_case([$info->{importer}, __FILE__, __LINE__], case_b => sub { $main::CASE_B = 1 }); } 1; =head1 CUSTOM COLLECTOR The collector is responsible for 2 jobs: 1) In the parent process it is responsible for gathering all test results from the child processes. 2) In the child processes it is responsible for sending results to the parent process. If TAP is not your thing, or you want to use Fennec with existing tests that do not use Test::Builder, you can create a custom collector to work for you. Documentation for a custom Collector can be found in the L POD. =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Fennec is free software; Standard perl license (GPL and Artistic). Fennec is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/lib/Test000755001750001750 013306515055 14733 5ustar00exodistexodist000000000000Fennec-2.018/lib/Test/Workflow.pm000444001750001750 3221013306515055 17256 0ustar00exodistexodist000000000000package Test::Workflow; use strict; use warnings; use Exporter::Declare; use Test::Workflow::Meta; use Test::Workflow::Test; use Test::Workflow::Layer; use List::Util qw/shuffle/; use Carp qw/croak/; use Scalar::Util qw/blessed/; our @CARP_NOT = qw/ Test::Workflow Test::Workflow::Test /; default_exports qw/ tests run_tests describe it cases case before_case after_case before_each after_each around_each before_all after_all around_all with_tests test_sort /; gen_default_export TEST_WORKFLOW => sub { my ( $class, $importer ) = @_; my $meta = Test::Workflow::Meta->new($importer); return sub { $meta }; }; { no warnings 'once'; @DB::CARP_NOT = qw/ DB Test::Workflow / } sub _get_layer { my ( $offset, $sub, $caller ) = @_; my $meta = $caller->[0]->TEST_WORKFLOW; croak "$sub() can only be used within a describe or case block, or at the package level." if $meta->build_complete; my $layer = $meta->peek_layer; if ( blessed($layer) && blessed($layer)->isa('Test::Workflow::Layer') ) { croak "Layer has already been finalized!" if $layer->finalized; return $layer; } return $meta->root_layer; } sub with_tests { my @caller = caller; my $layer = _get_layer( 0, 'with_tests', \@caller ); $layer->merge_in( \@caller, @_ ); } { no warnings 'once'; *it = \&tests; } sub tests { my $name = shift; my @caller = caller; my $layer = _get_layer( 0, 'tests', \@caller ); $layer->add_test( \@caller, $name, verbose => 1, @_ ); } sub describe { _add_child( 'describe', @_ ) } sub cases { _add_child( 'case', @_ ) } sub _add_child { my $type = shift; my @caller = caller(1); my $layer = _get_layer( 1, $type, \@caller ); $layer->add_child( \@caller, @_ ); } sub case { _add_type( 'case', @_ ) } sub before_case { _add_type( 'before_case', @_ ) } sub before_each { _add_type( 'before_each', @_ ) } sub before_all { _add_type( 'before_all', @_ ) } sub after_each { _add_type( 'after_each', @_ ) } sub after_all { _add_type( 'after_all', @_ ) } sub after_case { _add_type( 'before_each', @_ ) } sub around_each { _add_type( 'around_each', @_ ) } sub around_all { _add_type( 'around_all', @_ ) } sub _add_type { my $type = shift; my $meth = "add_$type"; my @caller = caller(1); my $layer = _get_layer( 1, $type, \@caller ); $layer->$meth( \@caller, @_ ); } sub test_sort { caller->TEST_WORKFLOW->test_sort(@_) } sub run_tests { my ($instance) = @_; unless ($instance) { my $caller = caller; $instance = $caller->new() if $caller->can('new'); $instance ||= bless( {}, $caller ); } my $layer = $instance->TEST_WORKFLOW->root_layer; my @tests = get_tests( $instance, $layer, 'PACKAGE LEVEL', [], [], [], [], [] ); $instance->TEST_WORKFLOW->build_complete(1); my $sort = $instance->TEST_WORKFLOW->test_sort || 'rand'; @tests = order_tests( $sort, @tests ); $_->run($instance) for @tests; } sub order_tests { my ( $sort, @tests ) = @_; if ( "$sort" =~ /^sort/ ) { @tests = sort { $a->name cmp $b->name } @tests; } elsif ( "$sort" =~ /^rand/ ) { @tests = shuffle @tests; } elsif ( ref $sort eq 'CODE' ) { @tests = $sort->(@tests); } elsif ( $sort !~ /^ord/ ) { croak "'$sort' is not a recognized option to test_sort"; } return sort { return 0 if $a->is_wrap == $b->is_wrap; return 1 if $a->is_wrap; return 0; } @tests; } #<<< no-tidy sub get_tests { my ( $instance, $layer, $name, $before_case, $before_each, $after_each, $around_each, $control, $todo ) = @_; # get before_each and after_each push @$before_case => @{ $layer->before_case }; push @$before_each => @{ $layer->before_each }; push @$around_each => @{ $layer->around_each }; push @$control => @{ $layer->control }; unshift @$after_each => @{ $layer->after_each }; my @tests = @{ $layer->test }; if ($todo) { $_->todo( $todo ) for @tests } if ( my $specific = $ENV{FENNEC_TEST}) { @tests = grep { my $out = 0; if ( $specific =~ m/^\d+$/ ) { $out = 1 if $_->start_line <= $specific && $_->end_line >= $specific; } else { $out = 1 if $_->name eq $specific; } $out; } @tests; } my @cases = @{ $layer->case }; if ( @cases ) { my @new_tests; for my $test ( @tests ) { for my $case ( @cases ) { push @new_tests => Test::Workflow::Test->new( setup => [ @$before_case, $case, @$before_each ], tests => [ $test->clone_with( name => "'" . $case->name . "' x '" . $test->name . "'" ) ], teardown => [ @$after_each ], around => [ @$around_each ], control => [ @$control ], block_name => $name, ); } } @tests = @new_tests; } else { @tests = map { Test::Workflow::Test->new( setup => [ @$before_each ], tests => [ $_ ], teardown => [ @$after_each ], around => [ @$around_each ], control => [ @$control ], block_name => $name, )} @tests; } push @tests => map { my $layer = Test::Workflow::Layer->new; $instance->TEST_WORKFLOW->push_layer( $layer ); $_->todo( $todo ) if $todo; $_->run( $instance, $layer ); my @tests = get_tests( $instance, $layer, $_->name, [@$before_case], [@$before_each], [@$after_each], [@$around_each], [@$control], $_->todo, ); $instance->TEST_WORKFLOW->pop_layer( $layer ); unless (@tests) { my $name = $_->name; my $start = $_->start_line; my $end = $_->end_line; warn "No tests in block '$name' approx lines $start -> $end\n" unless $ENV{FENNEC_TEST}; } @tests; } @{ $layer->child }; my @before_all = @{ $layer->before_all }; my @after_all = @{ $layer->after_all }; my @around_all = @{ $layer->around_all }; my @control = @{ $layer->control }; return Test::Workflow::Test->new( setup => [ @before_all ], tests => [ @tests ], teardown => [ @after_all ], around => [ @around_all ], control => [ @control ], block_name => $name, is_wrap => 1, ) if @before_all || @after_all || @around_all || @control; return @tests; } #>>> 1; __END__ =head1 NAME Test::Workflow - Provide test grouping, reusability, and structuring such as RSPEC and cases. =head1 DESCRIPTION Test::Workflow provides tools for grouping and structuring tests. There is also a facility to write re-usable tests. Test::Workflow test files define classes. Tests within the files are defined as a type of method. Test::Workflow provides an RSPEC implementation. This implementation includes C blocks, C blocks, as well as C, C, C, C. There are even two new types: C and C. Test::Workflow provides a cases workflow. This workflow allows you to create multiple cases, and multiple tests. Each test will be run under each case. This allows you to write a test that should pass under multiple conditions, then write a case that builds that specific condition. Test::Workflow provides a way to 'inherit' tests. You can write classes that use Test::Workflow, and define test groups, but not run them. These classes can then be imported into as many test classes as you want. This is achieved with the C function. Test::Workflow gives you control over the order in which test groups will be run. You can use the predefined 'random', 'ordered', or 'sort' settings. You may also provide your own ordering function. This is achieved using the C function. =head1 SYNOPSIS package MyTest; use strict; use warnings; use Test::More; use Test::Workflow; with_tests qw/ Test::TemplateA Test::TemplateB /; test_sort 'rand'; # Tests can be at the package level use_ok( 'MyClass' ); tests loner => sub { my $self = shift; ok( 1, "1 is the loneliest number... " ); }; tests not_ready => ( todo => "Feature not implemented", code => sub { ... }, ); tests very_not_ready => ( skip => "These tests will die if run" code => sub { ... }, ); run_tests(); done_testing(); =head2 RSPEC WORKFLOW Here setup/teardown methods are declared in the order in which they are run, but they can really be declared anywhere within the describe block and the behavior will be identical. describe example => sub { my $self = shift; my $number = 0; my $letter = 'A'; before_all setup => sub { $number = 1 }; before_each letter_up => sub { $letter++ }; # it() is an alias for tests() it check => sub { my $self = shift; is( $letter, 'B', "Letter was incremented" ); is( $number, 2, "number was incremented" ); }; after_each reset => sub { $number = 1 }; after_all teardown => sub { is( $number, 1, "number is back to 1" ); }; describe nested => sub { # This nested describe block will inherit before_each and # after_each from the parent block. ... }; describe maybe_later => ( todo => "We might get to this", code => { ... }, ); }; describe addon => sub { my $self = shift; around_each localize_env => sub { my $self = shift; my ( $inner ) = @_; local %ENV = ( %ENV, foo => 'bar' ); $inner->(); }; tests foo => sub { is( $ENV{foo}, 'bar', "in the localized environment" ); }; }; =head2 CASE WORKFLOW Cases are used when you have a test that you wish to run under several tests conditions. The following is a trivial example. Each test will be run once under each case. B this will run (cases x tests), with many tests and cases this can be a huge set of actual tests. In this example 8 in total will be run. B The 'cases' keyword is an alias to describe. case blocks can go into any workflow and will work as expected. cases check_several_numbers => sub { my $number; case two => sub { $number = 2 }; case four => sub { $number = 4 }; case six => sub { $number = 6 }; case eight => sub { $number = 8 }; tests is_even => sub { ok( !$number % 2, "number is even" ); }; tests only_digits => sub { like( $number, qr/^\d+$/i, "number is all digits" ); }; }; =head1 EXPORTS =over 4 =item with_tests( @CLASSES ) Use tests defined in the specified classes. =item test_sort( sub { my @tests = @_; ...; return @tests }) =item test_sort( 'sort' ) =item test_sort( 'random' ) =item test_sort( 'ordered' ) Declare how tests should be sorted. =item cases( name => sub { ... }) =item cases( 'name', %params, code => sub { ... }) =item describe( name => sub { ... }) =item describe( 'name', %params, code => sub { ... }) Define a block that encapsulates workflow elements. =item tests( name => sub { ... }) =item tests( 'name', %params, code => sub { ... }) =item it( name => sub { ... }) =item it( 'name', %params, code => sub { ... }) Define a test block. =item case( name => sub { ... }) =item case( 'name', %params, code => sub { ... }) Define a case, each test will be run once per case that is defined at the same level (within the same describe or cases block). =item before_each( name => sub { ... }) =item after_each( name => sub { ... }) =item before_all( name => sub { ... }) =item after_all( name => sub { ... }) These define setup and teardown functions that will be run around tests. =item around_each( name => sub { ... }) =item around_all( name => sub { ... }) These are special additions to the setup and teardown methods. They are used like so: around_each localize_env => sub { my $self = shift; my ( $inner ) = @_; local %ENV = ( %ENV, foo => 'bar' ); $inner->(); }; =item run_tests() This will finalize the meta-data (forbid addition of new tests) and run the tests. =back =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Test-Workflow is free software; Standard perl license. Test-Workflow is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/lib/Test/Workflow000755001750001750 013306515055 16545 5ustar00exodistexodist000000000000Fennec-2.018/lib/Test/Workflow/Block.pm000444001750001750 1032613306515055 20314 0ustar00exodistexodist000000000000package Test::Workflow::Block; use strict; use warnings; use Fennec::Util qw/accessors/; use Carp qw/croak/; use B (); use Scalar::Util qw/blessed/; require Time::HiRes; our @CARP_NOT = qw{ Test::Workflow Test::Workflow::Meta Test::Workflow::Block Test::Workflow::Layer }; accessors qw{ name start_line end_line code verbose package diag skip todo should_fail subtype }; sub new { my $class = shift; my ( $caller, $name, @args ) = @_; my $code; croak "You must provide a caller (got: $caller)" unless $caller && ref $caller && ref $caller eq 'ARRAY' && @$caller; croak "You must provide a name" unless $name and !ref $name; # If code is first, grab it $code = shift(@args) if ref $args[0] && ref $args[0] eq 'CODE'; # If code is last, grab it my $ref = ref $args[-1] || ''; if ( !$code && $ref eq 'CODE' ) { $code = pop(@args); # if code was last, and in key => code form, pop the key pop(@args) if $args[-1] =~ m/^(code|method|sub)$/; } # Code must be a param my %proto = @args; $code ||= $proto{code} || $proto{method} || $proto{sub}; croak "You must provide a codeblock" unless $code && ref $code eq 'CODE'; my $start_line = B::svref_2object($code)->START->line; my $end_line = $caller->[2]; $start_line-- unless $start_line == $end_line; %proto = ( %proto, code => $code, name => $name, package => $caller->[0], start_line => $start_line, end_line => $end_line, diag => ( $start_line == $end_line ) ? "line $start_line" : "lines $start_line -> $end_line", ); return bless( \%proto, $class ); } sub clone_with { my $self = shift; my %params = @_; bless( {%$self, %params}, blessed($self) ); } sub run { my $self = shift; my ( $instance, $layer ) = @_; my $meta = $instance->TEST_WORKFLOW; my $name = "Group: " . $self->name; my $debug = $instance->can('FENNEC') && $instance->FENNEC->debug; return $meta->skip->( $name, $self->skip ) if $self->skip; my $ref = ref $self; $ref =~ s/^.*:://; if ($debug) { my $collector = Fennec::Runner->new->collector; my ($sec, $ms) = Time::HiRes::gettimeofday(); my $msg = sprintf( "FENNEC_DEBUG_BLOCK:PID:%d\0START_LINE:%d\0END_LINE:%d\0TYPE:%s\0NAME:%s\0SEC:%d\0MSEC:%d\0STATE:START\n", $$, $self->start_line, $self->end_line, $self->subtype, $self->name, $sec, $ms, ); $collector->diag($msg); } $meta->todo_start->( $self->todo ) if $self->todo; my $success = eval { $self->code->(@_); 1 } || $self->should_fail || 0; my $error = $@ || "Error masked!"; chomp($error); $meta->todo_end->() if $self->todo; if ($debug) { my $collector = Fennec::Runner->new->collector; my ($sec, $ms) = Time::HiRes::gettimeofday(); my $msg = sprintf( "FENNEC_DEBUG_BLOCK:PID:%d\0START_LINE:%d\0END_LINE:%d\0TYPE:%s\0NAME:%s\0SEC:%d\0MSEC:%d\0STATE:END\n", $$, $self->start_line, $self->end_line, $self->subtype, $self->name, $sec, $ms, ); $collector->diag($msg); } return if $success && !$self->verbose; $meta->ok->( $success || 0, $name ); $meta->diag->( " ================================" . "\n Error: " . $error . "\n Package: " . $self->package . "\n Block: '" . $self->name . "' on " . $self->diag . "\n\n" ) unless $success; } 1; __END__ =head1 NAME Test::Workflow::Block - Track information about test blocks. =head1 DESCRIPTION Test::Workflow blocks such as tests and describes are all instances of this class. =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Test-Workflow is free software; Standard perl license. Test-Workflow is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/lib/Test/Workflow/Layer.pm000444001750001750 353013306515055 20315 0ustar00exodistexodist000000000000package Test::Workflow::Layer; use strict; use warnings; use Test::Workflow::Block; use Fennec::Util qw/accessors require_module/; use Scalar::Util qw/blessed/; use Carp qw/croak/; our @ATTRIBUTES = qw{ test case child before_case before_each before_all after_each after_all around_each around_all control }; accessors 'finalized', @ATTRIBUTES; sub new { bless( {map { ( $_ => [] ) } @ATTRIBUTES}, shift ); } sub merge_in { my $self = shift; my ( $caller, @classes ) = @_; for my $class (@classes) { require_module $class; push @{$self->$_} => @{$class->TEST_WORKFLOW->root_layer->$_} for @ATTRIBUTES; } } sub add_control { my $self = shift; push @{$self->control} => @_; } sub add_after_case { goto &before_each; } for my $type (qw/test case child before_case before_each before_all around_each around_all/) { my $add = sub { my $self = shift; my $block = Test::Workflow::Block->new(@_); $block->subtype($type); push @{$self->$type} => $block; }; no strict 'refs'; *{"add_$type"} = $add; } for my $type (qw/after_each after_all/) { my $add = sub { my $self = shift; my $block = Test::Workflow::Block->new(@_); $block->subtype($type); unshift @{$self->$type} => $block; }; no strict 'refs'; *{"add_$type"} = $add; } 1; __END__ =head1 NAME Test::Workflow::Layer - Used to track per-encapsulation meta-data =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Test-Workflow is free software; Standard perl license. Test-Workflow is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/lib/Test/Workflow/Meta.pm000444001750001750 361113306515055 20127 0ustar00exodistexodist000000000000package Test::Workflow::Meta; use strict; use warnings; use Test::Workflow::Layer; use Test::Builder; use Fennec::Util qw/accessors/; accessors qw{ test_class build_complete root_layer test_run test_wait test_sort ok diag skip todo_start todo_end control_store }; sub new { my $class = shift; my ($test_class) = @_; my $tb = "tb"; my $root_layer = Test::Workflow::Layer->new(); my $self = bless( { test_class => $test_class, root_layer => $root_layer, ok => Fennec::Util->can("${tb}_ok"), diag => Fennec::Util->can("${tb}_diag"), skip => Fennec::Util->can("${tb}_skip"), todo_start => Fennec::Util->can("${tb}_todo_start"), todo_end => Fennec::Util->can("${tb}_todo_end"), layer_stack => [$root_layer], }, $class ); return $self; } my @LAYER_STACK; sub push_layer { my $self = shift; push @LAYER_STACK => @_; } sub pop_layer { my $self = shift; my ($check) = @_; my $layer = pop @LAYER_STACK; die "Bad pop!" unless $layer == $check; return $layer; } sub peek_layer { my $self = shift; return $LAYER_STACK[-1]; } 1; __END__ =head1 NAME Test::Workflow::Meta - The meta-object added to all Test-Workflow test classes. =head1 DESCRIPTION When you C a function is added to you class named 'TEST_WORKFLOW' that returns the single Test-Workflow meta-object that tracks information about your class. =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Test-Workflow is free software; Standard perl license. Test-Workflow is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/lib/Test/Workflow/Test.pm000444001750001750 665613306515055 20174 0ustar00exodistexodist000000000000package Test::Workflow::Test; use strict; use warnings; use Fennec::Util qw/accessors/; use List::Util qw/shuffle/; use Carp qw/cluck/; require Time::HiRes; accessors qw/setup tests teardown around block_name is_wrap control/; sub new { my $class = shift; my %params = @_; return bless( { setup => $params{setup} || [], tests => $params{tests} || [], teardown => $params{teardown} || [], around => $params{around} || [], control => $params{control} || [], block_name => $params{block_name} || "", is_wrap => $params{is_wrap} || 0, }, $class ); } sub name { my $self = shift; return $self->tests->[0]->name if @{$self->tests} == 1; return $self->block_name; } sub run { my $self = shift; my ($instance) = @_; my $run = $self->_wrap_tests($instance); my $prunner = $instance->TEST_WORKFLOW->test_run; my $testcount = @{$self->tests}; return $run->() if $self->is_wrap; return $prunner->( $run, $self, $instance ) if $prunner && $testcount == 1; $run->(); } sub _wrap_tests { my $self = shift; my ($instance) = @_; my $ref = ref $self; $ref =~ s/^.*:://; my $meta = $instance->TEST_WORKFLOW; my $sort = $meta->test_sort || 'rand'; my @tests = Test::Workflow::order_tests( $sort, @{$self->tests} ); my $wait = $meta->test_wait; my $pid = $$; my $debug = $instance->can('FENNEC') && $instance->FENNEC->debug; my $collector = $debug ? Fennec::Runner->new->collector : undef; return sub { my $control_store = []; $meta->control_store($control_store); for my $sub (@{$self->control}) { my $control = $sub->(); if ($debug) { my ($sec, $ms) = Time::HiRes::gettimeofday(); my $msg = sprintf( "FENNEC_DEBUG_MOCK:PID:%d\0CLASS:%s\0SEC:%d\0MSEC:%d\0OVERRIDES:%s\n", $$, $control->package, $sec, $ms, join ' ' => grep { /^\w/ } keys %$control ); $collector->diag($msg); } push @$control_store => $control; } $wait->() if $wait && $self->can('is_wrap') && $self->is_wrap; $_->run($instance) for @{$self->setup}; my $base = sub { for my $test (@tests) { $test->run($instance); } }; my $outer = $base; for my $around ( @{$self->around} ) { my $inner = $outer; $outer = sub { $around->run( $instance, $inner ) }; } $outer->(); $_->run($instance) for @{$self->teardown}; $meta->control_store(undef); $control_store = undef; $wait->() if $wait && $self->can('is_wrap') && $self->is_wrap; }; } 1; __END__ =head1 NAME Test::Workflow::Test - A test block wrapped with setup/teardown methods, ready to be run. =head1 AUTHORS Chad Granum L =head1 COPYRIGHT Copyright (C) 2013 Chad Granum Test-Workflow is free software; Standard perl license. Test-Workflow is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the license for more details. Fennec-2.018/t000755001750001750 013306515055 13511 5ustar00exodistexodist000000000000Fennec-2.018/t/CantFindLayer.t000444001750001750 102213306515055 16511 0ustar00exodistexodist000000000000#!/usr/bin/perl package TEST::LayerErrors; use strict; use warnings; use Fennec; tests foo => sub { throws_ok { tests not_here => sub { 1 } } qr/tests\(\) can only be used within a describe or case block, or at the package level\./, "Layer error"; }; describe bar => sub { tests inner => sub { throws_ok { tests not_here => sub { 1 } } qr/tests\(\) can only be used within a describe or case block, or at the package level\./, "Layer error"; }; }; done_testing; Fennec-2.018/t/Case-Scoping.t000444001750001750 122013306515055 16301 0ustar00exodistexodist000000000000#!/usr/bin/perl package CaseScoping; use strict; use warnings; use Fennec; my $var; my $before_var; my $before_all; case alpha => sub { $var = 'a' }; case bravo => sub { $var = 'b' }; before_all clear_the_room => sub { # If scoping works properly, this should have no case applied $before_all = $var; }; before_each set_the_before => sub { # If scoping works properly, we should hit this twice, once # for alpha and once for bravo, with $var set appropriately. $before_var = $var; }; tests check_before_each => sub { is( $before_var, $var ); }; tests check_before_all => sub { is( $before_all, undef ); }; done_testing; Fennec-2.018/t/Child.t000444001750001750 51513306515055 15037 0ustar00exodistexodist000000000000#!/usr/bin/perl use strict; use warnings; use Fennec; my $pid = $$; my $child = child { ok( $pid != $$, "New process $$, parent: $pid" ); }; $child->wait; my $collector = Fennec::Runner->new->collector; $collector->collect; is( Fennec::Runner->new->collector->test_count, 1, "Got test from child process" ); done_testing; Fennec-2.018/t/Class.t000444001750001750 52113306515055 15056 0ustar00exodistexodist000000000000use strict; use warnings; use Fennec class => 'Data::Dumper'; ok( $INC{'Data/Dumper.pm'}, "Loaded \$CLASS" ); can_ok( __PACKAGE__, 'class' ); lives_ok { is( $CLASS, 'Data::Dumper', "Imported \$CLASS" ) }; tests method => sub { my $self = shift; is( $self->class, 'Data::Dumper', "Injected 'class' method" ); }; done_testing; Fennec-2.018/t/Declare.t000444001750001750 126013306515055 15371 0ustar00exodistexodist000000000000package TEST::Fennec::Declare; use strict; use warnings; BEGIN { my $ok = eval { require Fennec::Declare; 1 }; return if $ok; require Test::More; Test::More->import( skip_all => 'Fennec::Declare not installed' ); } use Fennec::Declare; tests group1 { ok( 1, "Here" ); } ok( 1, "there" ); describe more { tests deep { ok( 1, 'everywhere' ); } } done_testing( sub { my $runner = Fennec::Runner->new(); my $want = 5; my $got = $runner->collector->test_count; return if $runner->collector->ok( $got == $want, "Got expected test count" ); $runner->collector->diag("Got: $got\nWant: $want"); } ); Fennec-2.018/t/Finder.t000444001750001750 122513306515055 15242 0ustar00exodistexodist000000000000#!/usr/bin/perl use strict; use warnings; use Fennec::Finder; use Test::More; use Data::Dumper; is_deeply( [sort map { m{^.*/([^/]+$)}; $1 } @{Fennec::Finder->new->test_files}], [ sort qw{ CantFindLayer.ft Case-Scoping.ft FinderTest.pm Mock.ft RunSpecific.ft Todo.ft WorkflowTest.pm Workflow_Fennec.ft hash_warning.ft import_skip.ft inner_todo.ft order.ft procs.ft }, ], "Found all test files" ) || print STDERR Dumper( Fennec::Finder->new->test_files ); run(); 1; Fennec-2.018/t/MatchedT.t000444001750001750 131113306515055 15520 0ustar00exodistexodist000000000000#!/usr/bin/perl use strict; use warnings; use lib 't/lib'; BEGIN { return unless $^O =~ m/MSWin32/i; require Test::More; Test::More->import( skip_all => "feature broken on win32, but also deprecated so we do not care." ); exit(0); } use Fennec::Runner qw/FinderTest/; use Test::More; my $found = grep { m/FinderTest/ } @{Fennec::Runner->new->test_classes}; ok( $found, "Found test!" ); run( sub { my $runner = Fennec::Runner->new(); my $want = 3; my $got = $runner->collector->test_count; return if $runner->collector->ok( $got == $want, "Got expected test count" ); $runner->collector->diag("Got: $got\nWant: $want"); } ); 1; Fennec-2.018/t/Mock.t000444001750001750 402713306515055 14727 0ustar00exodistexodist000000000000#!/usr/bin/perl package TEST::Mock; use strict; use warnings; use Fennec; BEGIN { require_ok('Mock::Quick'); Mock::Quick->import(); can_ok( __PACKAGE__, qw/ qobj qclass qtakeover qclear qmeth / ); package Foo; } tests object => sub { is( qclear(), \$Mock::Quick::Util::CLEAR, "clear returns the clear reference" ); my $one = qobj( foo => 'bar' ); isa_ok( $one, 'Mock::Quick::Object' ); is( $one->foo, 'bar', "created properly" ); my $two = qmeth { 'vm' }; isa_ok( $two, 'Mock::Quick::Method' ); is( $two->(), "vm", "virtual method" ); my $three = qobj( foo => qmeth { 'bar' } ); is( $three->foo, 'bar', "ran virtual method" ); $three->foo( qclear() ); ok( !$three->foo, "cleared" ); }; tests class => sub { my $one = qclass( foo => 'bar' ); isa_ok( $one, 'Mock::Quick::Class' ); can_ok( $one->package, 'foo' ); my $two = qtakeover('Foo' => (blah => 'blah')); isa_ok( $two, 'Mock::Quick::Class' ); is( $two->package, 'Foo', "took over Foo" ); is( Foo->blah, 'blah', "got mock" ); $two = undef; ok( !Foo->can('blah'), "Mock destroyed" ); }; tests class_store => sub { my $self = shift; can_ok( $self, 'QINTERCEPT' ); qtakeover(Foo => (blah => sub { 'blah' })); is( Foo->blah, 'blah', "Mock not auto-destroyed" ); }; describe outer_wrap => sub { qtakeover( Foo => ( outer => 'outer' )); ok( !Foo->can( 'outer' ), "No Leak" ); before_all ba => sub { qtakeover( Foo => ( ba => 'ba' )); can_ok( 'Foo', qw/outer ba/ ); }; before_each be => sub { qtakeover( Foo => ( be => 'be' )); can_ok( 'Foo', qw/outer ba be/ ); }; tests the_check => sub { qtakeover( Foo => ( inner => 'inner' )); can_ok( 'Foo', qw/outer ba be inner/ ); }; ok( !Foo->can( 'outer' ), "No Leak" ); ok( !Foo->can( 'ba' ), "No Leak" ); ok( !Foo->can( 'be' ), "No Leak" ); ok( !Foo->can( 'inner' ), "No Leak" ); }; done_testing sub { ok( !Foo->can('blah'), "Mock did not leak" ); }; Fennec-2.018/t/POD.t000444001750001750 22113306515055 14430 0ustar00exodistexodist000000000000#!/usr/bin/perl use Test::More; eval "use Test::Pod 1.00"; plan skip_all => "Test::Pod 1.00 required for testing POD" if $@; all_pod_files_ok(); Fennec-2.018/t/RunSpecific.t000444001750001750 161313306515055 16246 0ustar00exodistexodist000000000000#!/usr/bin/perl use strict; use warnings; use Fennec; $ENV{FENNEC_TEST} = "only_these"; tests only_these => sub { ok( 1, "Should see this" ); }; describe foo => sub { my $check = 0; before_each check => sub { $check = 1; }; tests only_these => sub { ok( $check, "Should see this" ); }; describe foo => sub { my $check = 0; before_each check => sub { $check = 1; }; tests only_these => sub { ok( $check, "Should see this" ); }; }; describe bar => sub { before_each no => sub { ok( 0, "Should not run" ); }; tests blah => sub { ok( 0, "blah" ); }; }; }; describe bar => sub { before_each no => sub { ok( 0, "Should not run" ); }; tests blah => sub { ok( 0, "blah" ); }; }; done_testing; Fennec-2.018/t/Self-Running-Legacy.t000444001750001750 151113306515055 17542 0ustar00exodistexodist000000000000#!/usr/bin/perl package Fennec::Test::SelfRunning; use strict; use warnings; use Fennec; ok( !__PACKAGE__->can($_), "$_ not imported" ) for qw/run_tests/; describe blah => sub { tests group_a => code => sub { ok( 1, 'a' ) }; tests group_b => sub { ok( 1, 'b' ) }; tests group_c => sub { ok( 1, 'c' ) }; tests group_d => sub { ok( 1, 'd' ) }; tests group_e => sub { ok( 1, 'e' ) }; describe foo => sub { tests group_x => sub { ok( 1, 'x' ) }; }; }; tests todo_group => ( code => sub { ok( 0, "This should fail, no worries" ) }, todo => "This is a todo group", ); tests should_fail => ( should_fail => 1, code => sub { die "You should not see this!" }, ); tests skip_group => ( skip => "This is a skip group", code => sub { ok( 0, "You should not see this!" ) }, ); 1 Fennec-2.018/t/Self-Running.t000444001750001750 146613306515055 16351 0ustar00exodistexodist000000000000#!/usr/bin/perl use strict; use warnings; use Fennec; ok( !__PACKAGE__->can($_), "$_ not imported" ) for qw/run_tests/; describe blah => sub { tests group_a => code => sub { ok( 1, 'a' ) }; tests group_b => sub { ok( 1, 'b' ) }; tests group_c => sub { ok( 1, 'c' ) }; tests group_d => sub { ok( 1, 'd' ) }; tests group_e => sub { ok( 1, 'e' ) }; describe foo => sub { tests group_x => sub { ok( 1, 'x' ) }; }; }; tests todo_group => ( code => sub { ok( 0, "This should fail, no worries" ) }, todo => "This is a todo group", ); tests should_fail => ( should_fail => 1, code => sub { die "You should not see this!" }, ); tests skip_group => ( skip => "This is a skip group", code => sub { ok( 0, "You should not see this!" ) }, ); done_testing; 1; Fennec-2.018/t/Test-Isolation.t000444001750001750 122113306515055 16705 0ustar00exodistexodist000000000000#!/usr/bin/perl use Fennec parallel => 0, test_sort => 'random'; use Cwd qw(abs_path); my $Original_Cwd; my %Original_ENV; before_all name => sub { $Original_Cwd = abs_path; %Original_ENV = %ENV; note "Before All $$ $Original_Cwd"; }; tests "ENV change 1" => sub { is_deeply \%Original_ENV, \%ENV; $ENV{FOO} = 23; }; tests "ENV change 2" => sub { is_deeply \%Original_ENV, \%ENV; $ENV{FOO} = 42; }; tests "chdir 1" => sub { note "$$ $Original_Cwd"; is $Original_Cwd, abs_path; chdir ".."; }; tests "chdir 2" => sub { note "$$ $Original_Cwd"; is $Original_Cwd, abs_path; chdir "t"; }; done_testing; Fennec-2.018/t/Todo.t000444001750001750 44413306515055 14722 0ustar00exodistexodist000000000000#!/usr/bin/perl package TEST::Fennec::TODO; use strict; use warnings; use Fennec; tests blah1 => ( skip => 'whatever', code => sub { ok( 0, "fail 1" ); }, ); tests blah2 => ( todo => 'whatever', code => sub { ok( 0, "fail 2" ); }, ); done_testing; Fennec-2.018/t/Workflow.t000444001750001750 54013306515055 15624 0ustar00exodistexodist000000000000package TEST::Test::Workflow; use strict; use warnings; use Test::More; use Test::Workflow; test_sort 'sort'; can_ok( __PACKAGE__, qw/describe it before_each after_each before_all after_all/ ); use lib 't/lib'; with_tests 'WorkflowTest'; is( @{TEST_WORKFLOW->root_layer->child}, 2, "Loaded tests from WorkflowTest" ); run_tests; done_testing; 1; Fennec-2.018/t/Workflow_Fennec.t000444001750001750 51613306515055 17105 0ustar00exodistexodist000000000000package TEST::Test::Workflow; use strict; use warnings; use lib 't/lib'; use Fennec parallel => 0, test_sort => 'sort', with_tests => ['WorkflowTest']; can_ok( __PACKAGE__, qw/describe it before_each after_each before_all after_all/ ); is( @{TEST_WORKFLOW->root_layer->child}, 2, "Loaded tests from WorkflowTest" ); done_testing; Fennec-2.018/t/around_all.t000444001750001750 136213306515055 16155 0ustar00exodistexodist000000000000#!/usr/bin/perl use strict; use warnings; use Fennec; my $parent_pid = $$; describe set => sub { my $pid; my $count = 0; my $count2 = 0; my $count3 = 0; before_all blah => sub { $count2++; }; around_each bar => sub { is( $count3, 1, "already 1" ); $count3++; }; around_all foo => sub { my $self = shift; my ($run) = @_; $count++; $pid = $$; $count3++; $run->(); }; for my $i ( 1 .. 10 ) { tests $i => sub { is( $count, 1, "ran once" ); is( $count2, 1, "ran once" ); is( $count3, 2, "both ran" ); is( $pid, $parent_pid, "ran in parent" ); }; } }; done_testing; Fennec-2.018/t/before_case.t000444001750001750 66313306515055 16255 0ustar00exodistexodist000000000000#!/usr/bin/perl use strict; use warnings; use Fennec; describe set => sub { my $name = ""; before_case init_name => sub { $name .= "Foo"; }; case bar => sub { $name .= "Bar" }; case baz => sub { $name .= "Baz" }; after_case finish_name => sub { $name .= "End"; }; tests them => sub { like( $name, qr/^Foo(Bar|Baz)End$/, "Parts in correct order" ); }; }; done_testing; Fennec-2.018/t/hash_warning.t000444001750001750 125013306515055 16501 0ustar00exodistexodist000000000000#!/usr/bin/env perl package Test::HashWarning; use strict; use warnings; use Fennec; tests 'sub' => ( sub => sub { ok( 1, "sanity" ) }, ); tests 'code' => ( code => sub { ok( 1, "sanity" ) }, ); tests 'method' => ( method => sub { ok( 1, "sanity" ) }, ); tests 'sub tail' => ( foo => 'bar', sub { ok( 1, "sanity" ) }, ); tests 'sub param' => ( foo => 'bar', sub => sub { ok( 1, "sanity" ) }, baz => 'bar', ); tests 'method param' => ( foo => 'bar', method => sub { ok( 1, "sanity" ) }, baz => 'bar', ); tests 'code param' => ( foo => 'bar', code => sub { ok( 1, "sanity" ) }, baz => 'bar', ); done_testing; Fennec-2.018/t/import_skip.t000444001750001750 25513306515055 16355 0ustar00exodistexodist000000000000package TEST::SKIP; use strict; use warnings; use Fennec skip_without => [qw/ Some::Fake::Class:You::Better::Not::Have /]; ok( 0, "Should not see this." ); done_testing; Fennec-2.018/t/inner_todo.t000444001750001750 127513306515055 16200 0ustar00exodistexodist000000000000#!/usr/bin/env perl package Test::InnerTodo; use strict; use warnings; use Fennec; describe outer => ( todo => 'foo', code => sub { ok( 0, "outer: This should be todo" ); tests outer_tests => sub { ok( 0, "outer_test: This should be todo" ); }; describe inner => sub { ok( 0, "inner: This should be todo" ); tests inner_tests => sub { ok( 0, "inner_test: This should be todo" ); }; }; }, ); tests outside => ( todo => 'foo', code => sub { ok( 0, "outside_test: This should be todo" ); }, ); tests not_todo => sub { ok( 1, "Not todo" ); }; done_testing; Fennec-2.018/t/order.t000444001750001750 53113306515055 15125 0ustar00exodistexodist000000000000#!/usr/bin/env perl package Test::Order; use strict; use warnings; use Fennec parallel => 0, test_sort => 'ordered'; my @seen; for my $num ( 1 .. 20 ) { tests "$num" => sub { push @seen => $num; is_deeply( \@seen, [1 .. $num], "Ordered through $num", ); }; } done_testing; Fennec-2.018/t/procs.t000444001750001750 655513306515055 15174 0ustar00exodistexodist000000000000#!/usr/bin/env perl package Test::Procs; use strict; use warnings; use Fennec parallel => 1, test_sort => 'ordered'; use File::Temp qw/tempfile/; my ( $fh, $name ) = tempfile(); describe procs_1 => sub { my @pids = ($$); before_all setup => sub { ok( $pids[-1] == $$, "before_all happens in parent" ); push @pids => $$; }; tests a => sub { ok( $$ != $pids[-1], "New proc, even for just 1 test" ); push @pids => $$; }; after_all teardown => sub { ok( $$ == $pids[-1], "Same process as before_all" ); }; }; describe procs_2 => sub { my @pids = ($$); my $test_pid; before_all setup => sub { ok( $pids[-1] == $$, "before_all happens in parent" ); push @pids => $$; }; tests a => sub { ok( $$ != $pids[-1], "Multiple Tests, each should have a different proc" ); ok( !$test_pid, "Did not see other test" ); $test_pid = $$; }; tests b => sub { ok( $$ != $pids[-1], "Multiple Tests, each should have a different proc" ); ok( !$test_pid, "Did not see other test" ); $test_pid = $$; }; after_all teardown => sub { ok( $$ == $pids[-1], "Same process as before_all" ); }; }; describe procs_nested => sub { my @caller = caller; my @pids = ($$); my $test_pid; before_all setup => sub { print $fh "OUTER SETUP\n"; ok( $pids[-1] == $$, "before_all happens in parent" ); push @pids => $$; }; tests outer_a => sub { print $fh "OUTER TEST\n"; ok( $$ != $pids[-1], "Multiple Tests, each should have a different proc" ); ok( !$test_pid, "Did not see other test" ); $test_pid = $$; }; describe inner => sub { before_all inner_setup => sub { print $fh "INNER SETUP\n"; ok( $pids[-1] == $$, "before_all happens in parent" ); push @pids => $$; }; around_each spin_me_right_round => sub { my $self = shift; my ($run) = @_; print $fh "AROUND START\n"; $run->(); print $fh "AROUND END\n"; }; tests a => sub { print $fh "INNER TEST\n"; ok( $$ != $pids[-1], "Multiple Tests, each should have a different proc" ); ok( !$test_pid, "Did not see other test" ); $test_pid = $$; }; tests b => sub { print $fh "INNER TEST\n"; ok( $$ != $pids[-1], "Multiple Tests, each should have a different proc" ); ok( !$test_pid, "Did not see other test" ); $test_pid = $$; }; after_all inner_teardown => sub { ok( $$ == $pids[-1], "Same process as before_all" ); }; }; tests outer_b => sub { print $fh "OUTER TEST\n"; ok( $$ != $pids[-1], "Multiple Tests, each should have a different proc" ); ok( !$test_pid, "Did not see other test" ); $test_pid = $$; }; after_all teardown => sub { ok( $$ == $pids[-1], "Same process as before_all" ); }; }; done_testing( sub { close($fh); open( $fh, '<', $name ) || die $!; is_deeply( join( '' => <$fh> ), <<" EOT", "Order is correct" ); OUTER SETUP OUTER TEST OUTER TEST INNER SETUP AROUND START INNER TEST AROUND END AROUND START INNER TEST AROUND END EOT }); Fennec-2.018/t/subclass.t000444001750001750 206313306515055 15653 0ustar00exodistexodist000000000000#!/usr/bin/perl use strict; use warnings; BEGIN { package My::Fennec; $INC{'My/Fennec.pm'} = __FILE__; use base 'Fennec'; sub after_import { my $class = shift; my ($info) = @_; # The first arg to add case should be an array matching the return of # caller. The idea is to give us the start and end line, as well as # file name where the case is defined. normally the exports from # Test::Workflow provide that for you, but at this low-level we need to # provide it ourselfs. Since we define the subs here, we give current # line/file. Use the importer for package name. $info->{layer}->add_case([$info->{importer}, __FILE__, __LINE__], case_a => sub { $main::CASE_A = 1 }); $info->{layer}->add_case([$info->{importer}, __FILE__, __LINE__], case_b => sub { $main::CASE_B = 1 }); } } use My::Fennec; tests both_cases => sub { ok( $main::CASE_A || $main::CASE_B, "In a case" ); ok( !($main::CASE_A && $main::CASE_B), "Not in both cases" ); }; done_testing; Fennec-2.018/t/FennecLN000755001750001750 013306515055 15141 5ustar00exodistexodist000000000000Fennec-2.018/t/FennecLN/CantFindLayer.ft000444001750001750 102213306515055 20307 0ustar00exodistexodist000000000000#!/usr/bin/perl package TEST::LayerErrors; use strict; use warnings; use Fennec; tests foo => sub { throws_ok { tests not_here => sub { 1 } } qr/tests\(\) can only be used within a describe or case block, or at the package level\./, "Layer error"; }; describe bar => sub { tests inner => sub { throws_ok { tests not_here => sub { 1 } } qr/tests\(\) can only be used within a describe or case block, or at the package level\./, "Layer error"; }; }; done_testing; Fennec-2.018/t/FennecLN/Case-Scoping.ft000444001750001750 122013306515055 20077 0ustar00exodistexodist000000000000#!/usr/bin/perl package CaseScoping; use strict; use warnings; use Fennec; my $var; my $before_var; my $before_all; case alpha => sub { $var = 'a' }; case bravo => sub { $var = 'b' }; before_all clear_the_room => sub { # If scoping works properly, this should have no case applied $before_all = $var; }; before_each set_the_before => sub { # If scoping works properly, we should hit this twice, once # for alpha and once for bravo, with $var set appropriately. $before_var = $var; }; tests check_before_each => sub { is( $before_var, $var ); }; tests check_before_all => sub { is( $before_all, undef ); }; done_testing; Fennec-2.018/t/FennecLN/Mock.ft000444001750001750 402713306515055 16525 0ustar00exodistexodist000000000000#!/usr/bin/perl package TEST::Mock; use strict; use warnings; use Fennec; BEGIN { require_ok('Mock::Quick'); Mock::Quick->import(); can_ok( __PACKAGE__, qw/ qobj qclass qtakeover qclear qmeth / ); package Foo; } tests object => sub { is( qclear(), \$Mock::Quick::Util::CLEAR, "clear returns the clear reference" ); my $one = qobj( foo => 'bar' ); isa_ok( $one, 'Mock::Quick::Object' ); is( $one->foo, 'bar', "created properly" ); my $two = qmeth { 'vm' }; isa_ok( $two, 'Mock::Quick::Method' ); is( $two->(), "vm", "virtual method" ); my $three = qobj( foo => qmeth { 'bar' } ); is( $three->foo, 'bar', "ran virtual method" ); $three->foo( qclear() ); ok( !$three->foo, "cleared" ); }; tests class => sub { my $one = qclass( foo => 'bar' ); isa_ok( $one, 'Mock::Quick::Class' ); can_ok( $one->package, 'foo' ); my $two = qtakeover('Foo' => (blah => 'blah')); isa_ok( $two, 'Mock::Quick::Class' ); is( $two->package, 'Foo', "took over Foo" ); is( Foo->blah, 'blah', "got mock" ); $two = undef; ok( !Foo->can('blah'), "Mock destroyed" ); }; tests class_store => sub { my $self = shift; can_ok( $self, 'QINTERCEPT' ); qtakeover(Foo => (blah => sub { 'blah' })); is( Foo->blah, 'blah', "Mock not auto-destroyed" ); }; describe outer_wrap => sub { qtakeover( Foo => ( outer => 'outer' )); ok( !Foo->can( 'outer' ), "No Leak" ); before_all ba => sub { qtakeover( Foo => ( ba => 'ba' )); can_ok( 'Foo', qw/outer ba/ ); }; before_each be => sub { qtakeover( Foo => ( be => 'be' )); can_ok( 'Foo', qw/outer ba be/ ); }; tests the_check => sub { qtakeover( Foo => ( inner => 'inner' )); can_ok( 'Foo', qw/outer ba be inner/ ); }; ok( !Foo->can( 'outer' ), "No Leak" ); ok( !Foo->can( 'ba' ), "No Leak" ); ok( !Foo->can( 'be' ), "No Leak" ); ok( !Foo->can( 'inner' ), "No Leak" ); }; done_testing sub { ok( !Foo->can('blah'), "Mock did not leak" ); }; Fennec-2.018/t/FennecLN/RunSpecific.ft000444001750001750 161313306515055 20044 0ustar00exodistexodist000000000000#!/usr/bin/perl use strict; use warnings; use Fennec; $ENV{FENNEC_TEST} = "only_these"; tests only_these => sub { ok( 1, "Should see this" ); }; describe foo => sub { my $check = 0; before_each check => sub { $check = 1; }; tests only_these => sub { ok( $check, "Should see this" ); }; describe foo => sub { my $check = 0; before_each check => sub { $check = 1; }; tests only_these => sub { ok( $check, "Should see this" ); }; }; describe bar => sub { before_each no => sub { ok( 0, "Should not run" ); }; tests blah => sub { ok( 0, "blah" ); }; }; }; describe bar => sub { before_each no => sub { ok( 0, "Should not run" ); }; tests blah => sub { ok( 0, "blah" ); }; }; done_testing; Fennec-2.018/t/FennecLN/Todo.ft000444001750001750 44413306515055 16520 0ustar00exodistexodist000000000000#!/usr/bin/perl package TEST::Fennec::TODO; use strict; use warnings; use Fennec; tests blah1 => ( skip => 'whatever', code => sub { ok( 0, "fail 1" ); }, ); tests blah2 => ( todo => 'whatever', code => sub { ok( 0, "fail 2" ); }, ); done_testing; Fennec-2.018/t/FennecLN/Workflow_Fennec.ft000444001750001750 51613306515055 20703 0ustar00exodistexodist000000000000package TEST::Test::Workflow; use strict; use warnings; use lib 't/lib'; use Fennec parallel => 0, test_sort => 'sort', with_tests => ['WorkflowTest']; can_ok( __PACKAGE__, qw/describe it before_each after_each before_all after_all/ ); is( @{TEST_WORKFLOW->root_layer->child}, 2, "Loaded tests from WorkflowTest" ); done_testing; Fennec-2.018/t/FennecLN/hash_warning.ft000444001750001750 125013306515055 20277 0ustar00exodistexodist000000000000#!/usr/bin/env perl package Test::HashWarning; use strict; use warnings; use Fennec; tests 'sub' => ( sub => sub { ok( 1, "sanity" ) }, ); tests 'code' => ( code => sub { ok( 1, "sanity" ) }, ); tests 'method' => ( method => sub { ok( 1, "sanity" ) }, ); tests 'sub tail' => ( foo => 'bar', sub { ok( 1, "sanity" ) }, ); tests 'sub param' => ( foo => 'bar', sub => sub { ok( 1, "sanity" ) }, baz => 'bar', ); tests 'method param' => ( foo => 'bar', method => sub { ok( 1, "sanity" ) }, baz => 'bar', ); tests 'code param' => ( foo => 'bar', code => sub { ok( 1, "sanity" ) }, baz => 'bar', ); done_testing; Fennec-2.018/t/FennecLN/import_skip.ft000444001750001750 25513306515055 20153 0ustar00exodistexodist000000000000package TEST::SKIP; use strict; use warnings; use Fennec skip_without => [qw/ Some::Fake::Class:You::Better::Not::Have /]; ok( 0, "Should not see this." ); done_testing; Fennec-2.018/t/FennecLN/inner_todo.ft000444001750001750 127513306515055 17776 0ustar00exodistexodist000000000000#!/usr/bin/env perl package Test::InnerTodo; use strict; use warnings; use Fennec; describe outer => ( todo => 'foo', code => sub { ok( 0, "outer: This should be todo" ); tests outer_tests => sub { ok( 0, "outer_test: This should be todo" ); }; describe inner => sub { ok( 0, "inner: This should be todo" ); tests inner_tests => sub { ok( 0, "inner_test: This should be todo" ); }; }; }, ); tests outside => ( todo => 'foo', code => sub { ok( 0, "outside_test: This should be todo" ); }, ); tests not_todo => sub { ok( 1, "Not todo" ); }; done_testing; Fennec-2.018/t/FennecLN/order.ft000444001750001750 53113306515055 16723 0ustar00exodistexodist000000000000#!/usr/bin/env perl package Test::Order; use strict; use warnings; use Fennec parallel => 0, test_sort => 'ordered'; my @seen; for my $num ( 1 .. 20 ) { tests "$num" => sub { push @seen => $num; is_deeply( \@seen, [1 .. $num], "Ordered through $num", ); }; } done_testing; Fennec-2.018/t/FennecLN/procs.ft000444001750001750 655513306515055 16772 0ustar00exodistexodist000000000000#!/usr/bin/env perl package Test::Procs; use strict; use warnings; use Fennec parallel => 1, test_sort => 'ordered'; use File::Temp qw/tempfile/; my ( $fh, $name ) = tempfile(); describe procs_1 => sub { my @pids = ($$); before_all setup => sub { ok( $pids[-1] == $$, "before_all happens in parent" ); push @pids => $$; }; tests a => sub { ok( $$ != $pids[-1], "New proc, even for just 1 test" ); push @pids => $$; }; after_all teardown => sub { ok( $$ == $pids[-1], "Same process as before_all" ); }; }; describe procs_2 => sub { my @pids = ($$); my $test_pid; before_all setup => sub { ok( $pids[-1] == $$, "before_all happens in parent" ); push @pids => $$; }; tests a => sub { ok( $$ != $pids[-1], "Multiple Tests, each should have a different proc" ); ok( !$test_pid, "Did not see other test" ); $test_pid = $$; }; tests b => sub { ok( $$ != $pids[-1], "Multiple Tests, each should have a different proc" ); ok( !$test_pid, "Did not see other test" ); $test_pid = $$; }; after_all teardown => sub { ok( $$ == $pids[-1], "Same process as before_all" ); }; }; describe procs_nested => sub { my @caller = caller; my @pids = ($$); my $test_pid; before_all setup => sub { print $fh "OUTER SETUP\n"; ok( $pids[-1] == $$, "before_all happens in parent" ); push @pids => $$; }; tests outer_a => sub { print $fh "OUTER TEST\n"; ok( $$ != $pids[-1], "Multiple Tests, each should have a different proc" ); ok( !$test_pid, "Did not see other test" ); $test_pid = $$; }; describe inner => sub { before_all inner_setup => sub { print $fh "INNER SETUP\n"; ok( $pids[-1] == $$, "before_all happens in parent" ); push @pids => $$; }; around_each spin_me_right_round => sub { my $self = shift; my ($run) = @_; print $fh "AROUND START\n"; $run->(); print $fh "AROUND END\n"; }; tests a => sub { print $fh "INNER TEST\n"; ok( $$ != $pids[-1], "Multiple Tests, each should have a different proc" ); ok( !$test_pid, "Did not see other test" ); $test_pid = $$; }; tests b => sub { print $fh "INNER TEST\n"; ok( $$ != $pids[-1], "Multiple Tests, each should have a different proc" ); ok( !$test_pid, "Did not see other test" ); $test_pid = $$; }; after_all inner_teardown => sub { ok( $$ == $pids[-1], "Same process as before_all" ); }; }; tests outer_b => sub { print $fh "OUTER TEST\n"; ok( $$ != $pids[-1], "Multiple Tests, each should have a different proc" ); ok( !$test_pid, "Did not see other test" ); $test_pid = $$; }; after_all teardown => sub { ok( $$ == $pids[-1], "Same process as before_all" ); }; }; done_testing( sub { close($fh); open( $fh, '<', $name ) || die $!; is_deeply( join( '' => <$fh> ), <<" EOT", "Order is correct" ); OUTER SETUP OUTER TEST OUTER TEST INNER SETUP AROUND START INNER TEST AROUND END AROUND START INNER TEST AROUND END EOT }); Fennec-2.018/t/lib000755001750001750 013306515055 14257 5ustar00exodistexodist000000000000Fennec-2.018/t/lib/FinderTest.pm000444001750001750 20413306515055 16775 0ustar00exodistexodist000000000000package FinderTest; use strict; use warnings; use Fennec; tests something => sub { ok( 1, "generic test" ); }; done_testing; Fennec-2.018/t/lib/WorkflowTest.pm000444001750001750 525013306515055 17426 0ustar00exodistexodist000000000000package WorkflowTest; use strict; use warnings; use Test::More; use Test::Workflow; our @RUN_ORDER; describe a => sub { push @RUN_ORDER => "Describe"; before_all b => sub { push @RUN_ORDER => "Before All" }; before_each c => sub { push @RUN_ORDER => "Before Each" }; it d => sub { push @RUN_ORDER => "It"; }; after_each e => sub { push @RUN_ORDER => "After Each" }; after_all f => sub { push @RUN_ORDER => "After All" }; describe aa => sub { push @RUN_ORDER => "Describe Nested"; before_all bb => sub { push @RUN_ORDER => "Before All Nested" }; before_each cc => sub { push @RUN_ORDER => "Before Each Nested" }; around_each ar => sub { my $self = shift; my ($runme) = @_; push @RUN_ORDER => "around start"; $runme->(); push @RUN_ORDER => "around end"; }; it dd => sub { push @RUN_ORDER => "It Nested"; }; it xx => sub { push @RUN_ORDER => "It Nested xx"; }; after_each ee => sub { push @RUN_ORDER => "After Each Nested" }; after_all ff => sub { push @RUN_ORDER => "After All Nested" }; }; }; cases m => sub { push @RUN_ORDER => 'm'; case a => sub { push @RUN_ORDER => 'a' }; case b => sub { push @RUN_ORDER => 'b' }; case c => sub { push @RUN_ORDER => 'c' }; tests x => sub { push @RUN_ORDER => 'x' }; tests y => sub { push @RUN_ORDER => 'y' }; tests z => sub { push @RUN_ORDER => 'z' }; }; tests verify => sub { is_deeply( \@RUN_ORDER, [ # Generators "Describe", "Describe Nested", "m", # Cases qw/a x a y a z/, qw/b x b y b z/, qw/c x c y c z/, #<<< no-tidy "Before All", "Before Each", "It", "After Each", "Before All Nested", "Before Each", "Before Each Nested", "around start", "It Nested", "around end", "After Each Nested", "After Each", "Before Each", "Before Each Nested", "around start", "It Nested xx", "around end", "After Each Nested", "After Each", "After All Nested", "After All", #>>> ], "Order is correct" ); }; 1;