NAME

Fennec::Manual::Developer - Developer manual for extending or enhancing Fennec.

DESCRIPTION

Guide to developing plugins or enhancements for Fennec.

CHAPTER 1 - INNER WORKINGS

Fennec starts by loading a runner, even in standalone. This runner does the following:

STARTUP PHASE

Gather process information
Take over Test::Builder if needed
Start a collector to gather results
Initialize output handlers
Create a fork controller aka threader (NOT USING I-THREADS)

Threader should not be confused with perls horrible threading.

TESTING PHASE

The following is run for each test file the runner finds. For standalone tests it occurs only once; for only the one file. The runner may do this in multiple processes, one per file.

Run test file

Creates test groups and workflow objects, as well as running any package level asserts.

Process Workflows

Workflow codeblocks are run as methods and produce additional test groups.

Run all test groups from all workflows

All the test groups defined in the file and the workflows are randomized and run as methods. The file process may run multiple groups at once in several processes.

CLEANUP PHASE

The cleanup phase tears everything down after tests have been run.

Wait on child processes
Shutdown output handles

CHAPTER 2 - NAMESPACES

Fennec

This is the primary Fennec namespace.

Fennec::Workflow

This is the namespace for workflow implementations. Things like RSPEC an Case live here.

Fennec::Util

Used for misc utility items.

Fennec::TestSet

TestSet is the class used to implement test group methods, and any workflow test grouping.

Fennec::TestFile

The base class for all Fennec test classes. Custom file types may subclass this for their own base classes.

Fennec::Parser

Namespace for Devel::Declare Magic.

Fennec::Output

This is where output objects such as results, diag, etc. live. These are the objects that get serialized and sent to the handlers in the runner.

Fennec::Handler

This is where output handlers go. These objects take the output objects and present them to the user in a useful way such as TAP.

Fennec::FileType

The namespace for filetype classes which are used to load test files.

Fennec::Collector

Namespace for collectors which funnel results to the parent thread.

Fennec::Base

This is where various base classes go.

Fennec::Assert

This is the namespace for libraries of assert functions.

CHAPTER 3 - TESTING FENNEC ITSELF

Fennec provides a module that makes it easy to test itself. Anyone who has written libraries for Test::Builder has probably used the nightmare that is Test::Builder::Tester. Let me assure you Fennec learned from this nightmare and does not repeat it.

Fennec provides Fennec::Assert::Interceptor which can override the collector and intercept result objects without sending them to the main process. This library provides the capture() function for this task. Here is a snippet from t/Fennec/Assert/Core/Simple.pm showing its use:

package TEST::Fennec::Assert::Simple;
use strict;
use warnings;
use Fennec asserts => [qw/ Core Interceptor /];

tests 'ok' => sub {
    my $output = capture {
        ok( 1, 'pass' );
        ok( 0, 'fail' );
        ok( 1 );
        ok( 0 );
    };

    is( @$output, 4, "4 results" );

    is( $output->[0]->pass, 1, "passed" );
    is( $output->[0]->name, 'pass', 'name' );

    is( $output->[1]->pass, 0, "failed" );
    is( $output->[1]->name, 'fail', 'name' );

    is( $output->[2]->pass, 1, "passed" );
    is( $output->[2]->name, 'nameless test', 'name' );

    is( $output->[3]->pass, 0, "failed" );
    is( $output->[3]->name, 'nameless test', 'name' );
};

Each item will be one of the subclasses of Fennec::Output, such as Fennec::Output::Result and Fennec::Output::Diag.

CHAPTER 4 - WRITING ASSERTION LIBRARIES

Assertion libraries are the most common testing tool type written. Fennec makes the task of writing assertion libraries very simple. Fennec provies utilities that let you focus on your assertion library instead of worrying about the framework details.

CHAPTER 4.1 - WRITING NEW LIBRARIES

An assertion library must first import Fennec::Assert, this will turn the calling package into an Exporter::Declare subclass. This will also provide several helper functions:

util

Used to export functions that will not generate result objects.

All of the following are valid:

# Export and define seperately
util 'my_util';
sub my_util { ... }

# export and declare together
util my_util => sub { ... };

# export and declare using Devel::Declare shortcuts
util my_util { ... }

# export and declare attaching Devel::Declare magic
util my_util PARSER { ... }
tester

Used to export functions that generate results. This will wrap your function with code to provide diagnostics, line numbers, package names, and other contextal information you dont want figure out yourself.

# Export and define seperately
tester 'my_assert';
sub my_assert { ... }

# export and declare together
tester my_assert => sub { ... };

# export and declare using Devel::Declare shortcuts
tester my_assert { ... }

# export and declare attaching Devel::Declare magic
tester my_assert PARSER { ... }
result

Use this function in your asserts to generate a result object. It will build the result object, add context, and send the result to the collector.

In most cases you simply provide a name and a boolean:

result(
    pass   => $BOOL,
    name   => $NAME,
);

Here are all options:

result(
    # Required:
    pass => $BOOL,

    # Optional:
    name   => $NAME,
    stderr => \@ERRORS,
    stdout => \@NOTES,

    # Filled in automatically, listed here so you can override
    line           => $LINE_NO,
    file           => $FILE_NAME,
    todo           => $REASON,
    skip           => $REASON,
    testset_name   => $NAME_OF_TEST_GROUP,
    workflow_stack => \@NAMES,
);

Only specify file and line if Fennec gets it wrong. Deducing these is not something you want to figure out yourself.

diag

Messages that should always be displayed.

diag( $message1, $message2, @other_messages )
note

Messages that should be displayed in verbose mode.

note( $message1, $message2, @other_messages )
test_caller

Returnes a hash with line and file as keys. Used to find context for results.

my %context = test_caller();
tb_wrapper
Used to improve Test::Builder asserts. See Chapter 4.2 for more details

COMPLETE EXAMPLE

package Fennec::Assert::MyAssert;
use strict;
use warnings;
use Fennec::Assert;

tester my_ok {
    my ( $ok, $name ) = @_;
    result(
        pass => $ok ? 1 : 0,
        name => $name || 'nameless test',
    );
}

util log_time {
    diag( "Time: " . time());
}

1;

CHAPTER 4.2 - WRAPPING TEST::BUILDER BASED TOOLS

Wrapping a Test::Builder library is exceedingly simple:

use Fennec::Assert
import the functions you wish to wrap
use tb_wrapper() to wrap each function
export the resulting coderefs using tester() or util()

EXAMPLE

package Fennec::Assert::TBCore::More;
use strict;
use warnings;

use Fennec::Assert;
use Fennec::Output::Result;
require Test::More;

our @LIST = qw/ ok is isnt like unlike cmp_ok can_ok isa_ok new_ok pass fail
                use_ok require_ok is_deeply /;

for my $name ( @LIST ) {
    no strict 'refs';
    next unless Test::More->can( $name );
    tester( $name => tb_wrapper( \&{ 'Test::More::' . $name }));
}

1;

CHAPTER 5 - WRITING CUSTOM OUTPUT HANDLERS

An output handler must subclass Fennec::Handler. The baseclass provides a generic constructor which you may override if you wish. You can also hook into construction by writing an init method. Handlers are constructed with no arguments.

The primary method to implement is handle( $output ), here is a complete list of methods that you may want to implement:

handle( $output )

Every output object generated is passed to each handler via this method. Every item passed in will be a subclass of Fennec::Output. You should familiarize yourself with the 3 main output types, Fennec::Output::Result, Fennec::Output::Diag, Fennec::Output::Note. You should handle these three types as well as handle unknown types generically.

finish()

This is called with no arguments when testing is complete and no more results will be recieved. This is your last chance to do anything.

start()

This is called when Fennec starts testing.

starting_file( $filename )

This will be called whenever a process starts running a new test file.

fennec_error( @errors )

This is called when fennec encounters an error not related to testing.

EXAMPLE

This is an example handler that ignores everything but errors, which it writes to a file.

package Fennec::Handler::MyHandler;
use strict;
use warnings;

use base 'Fennec::Handler';

use Fennec::Util::Accessors;
Accessors qw/ fh count files /;

sub start {
    my $self = shift;
    open( my $fh, ">", "failures.log" ) || die( "Error opening failure log: $!" );
    $self->fh( $fh );
    $self->count( 0 );
    $self->files( {} );
}

sub handle {
    my $self = shift;
    my ( $item ) = @_;
    return unless $item
           && $item->isa( 'Fennec::Output::Result' )
           && !$item->pass;

    $self->count( $self->count + 1 );
    my $file = $item->file;
    $file =~ s|.*(perl_lib/)|$1|g;
    $self->files->{ $file }++;

    my $fh = $self->fh;
    print $fh <<EOT
===========================================================
File: @{[ $item->file ]}
Test: @{[ $item->name() ]}
Workflow Stack: @{[ join( ', ', @{ $item->workflow_stack() || [] })]}
Err:
@{[ join( "\n", @{ $item->stderr || [] })]}
EOT
}

sub finish {
    my $self = shift;
    my $fh = $self->fh;
    print $fh "=======END========\n";
    print $fh "$_: " . $self->files->{$_} . "\n" for keys %{ $self->files };
    print $fh "\nTotal Errors: " . $self->count . "\n";
    close( $fh );
}

sub fennec_error {
    my $self = shift;
    my $fh = $self->fh;
    print $fh "=====FENNEC ERRORS====\n", @_;
}

1;

CHAPTER 6 - WRITING CUSTOM COLLECTORS

The collector is an abstracted IPC interface. All collectors must subclass Fennec::Collector. There are 2 methods to implement:

write( $output )

This method takes a single output object. This method needs to pass the object to the main thread.

@outputs = cull()

This should return a list of output objects collected from the child processes.

You may also override these, but you must remember to call SUPER.

start()

Called in the parent process to initialize the handlers.

finish()

Called in the parent process to shutdown the handlers.

You should familiarize yourself with Fennec::Collector, and avoid overriding any other methods.

CHAPTER 7 - WRITING CUSTOM FILE TYPES

You must implement the following methods.

$bool = $class->valid_file( $filename )

Return true if the given filename is a valid file of the type you expect.

$package = $obj->load_file()

Must load the test file. This must also make sure that a package name is generated for the file. When finished the method must return the name of the package for the test file. This package must also be a subclass of Fennec::TestFile.

@list = $obj->paths()

A list of paths, relative to the project root, in which to search for test files.

TRUNCATED SYNOPSIS

package Fennec::FileType::MyType;
use strict;
use warnings;

use base 'Fennec::FileType';

sub valid_file {
    my $class = shift;
    my ( $file ) = @_;
    ...
    return $bool;
}

sub load_file {
    my $self = shift;
    my $file = $self->filename;
    ...
    return $package;
}

sub paths {qw( t/ ... )}

1;

CHAPTER 8 - WRITING CUSTOM WORKFLOWS

CHAPTER 8.1 - WHY WRITE A WORKFLOW

In fennec a workflow is a way to structure tests. There are many popular testing frameworks that owe their popularity to how they structure their tests. A notable example is Ruby's RSPEC. Structured tests can simplify your job as a tester.

Within a workflow you can define child workflows, and groups of tests. Ultimately your workflow will return a list of testset objects. You can be as creative as you want in designing your workflow. You can also subclass Fennec::TestSet or use Fennec::TestSet::SubSet to wrap groups with buildup or teardown functions.

Using the Fennec::Workflow system it was a minimal effort to write an RSPEC like workflow. Less than 100 lines of code in SPEC.pm at the time this was written. If you do not like how Fennec implements SPEC you can probably implement it your own quite easily.

CHAPTER 8.2 - OVERVIEW

When a test file is loaded Fennec::Runner creates a root workflow, this root workflow is an Fennec::Workflow object. Every testset or workflow defined in the test file is a blessed method. The blessed methods are passed to a parent workflow. Those defined outside of a workflow are passed to a 'root' workflow.

A test classes root workflow is stored inthe classes meta object. Calling $test_class->fennec_meta() will return the meta object for a testclass. The meta object has 2 methods for workflows: $meta->root_workflow() which always returns the root workflow, and $meta->workflow() which returns the highest workflow on the stack, also known as the 'current' workflow.

Any testsets or workflows defined inside the method of a parent workflow are passed to that parent workflow. Each workflow object must inherit or implement the add_item() method which takes a workflow or testset. Testsets are retrieved from the workflows after they have been traversed to the deepest level.

Example:

package MyTest;
use Fennec workflows => [ 'MyWorkflow' ];

my_workflow parent => sub {
    tests 'parent tests' => {
        ...
    };
    my_workflow nested => sub {
        tests 'child tests' => {
            ...
        };
    };
};

1;

When Fennec runs it will load the file, it will create a root workflow, the workflow sub named 'parent' will be blessed and passed to the root workflow as a child workflow. The workflow sub 'nested' will be blessed as a workflow and passed to 'parent'. While this is going on the testsets will also be blessed and passed into their parent workflows.

CHAPTER 8.3 - CUSTOM WORKFLOW SYNOPSIS

package Fennec::Workflow::MyWorkflow;
use strict;
use warnings;

use Fennec::Workflow qw/:subclass/;

build_hook { ... };

build_with 'my_workflow';

sub testsets {
    my $self = shift;
    ...
    return @testsets;
}

sub add_item {
    my $self = shift;
    my ($item) = @_;

    if ( we_handle( $item )) { ... }
    else { $self->SUPER::add_item( $item )}
}

1;

CHAPTER 8.4 - METHODS THAT CAN BE OVERRIDEN

Overriding these is optional, not overriding them will result in a workflow that acts just like the root workflow.

@testsets = $wf->testsets()

testsets() should return an array with 0 or more testset objects, or objects that subclass testset. It is also responsible for returning the testsets from child workflows. If this method does not get testsets from child workflows they will not be run (which will generate a warning).

$wf->build()

The inherited method sets $wf as the current workflow, it then runs the method that was blessed into as the workflow object. Overriding this is not recommended, but may be necessary for some complicated workflows.

You should always call $self->built(1) at the end of your custom build() method. Not doing this may cause problems with items being added to a workflow AFTER build().

$wf->build_children()

This should rarely need to be overriden, calls $child->build() on all child workflows. If you override add_items to add items other than workflows and testsets, or to disallow adding items at all then you will probably want to override this to reflect the change.

$wf->add_item( $item )

Add $item to the workflow. The inherited method will add workflows or testsets, it will throw an exception for anything else. You can access the added items via $self->_workflows() and $self->_testsets().

If you override this method you should die with a useful message if $self->built() is true.

CHAPTER 8.5 - HELPFUL FUNCTIONS

build_with( $name )
build_with( $name, $class )

Exports a method ($name) that when called will create an instance of $class, or the class in which build_with() was called. The exported method will take a name and codeblock as arguments. After being build the instance will be added to the current in which it was defined.

build_hook { ... }
build_hook( sub { ... })

Add code that should be run just after building the workflows and just before running the tests.

CHAPTER 8.6 - OTHER METHODS TO KNOW

import()

Fennec::Workflow has a complicated import() method, in order to simplify it all classes that sublcass Fennec::Workflow have a new import() method exported to their package. It is important that you do not try to override import(), or that you are at least aware that you cannot call $wf->SUPER::import() and get the expected behavior. Defining your own import() method will also throw a redefine warning.

@wfs = $wf->workflows()

Returns a list of all the workflows added as children.

@testsets = $wf->testsets()

Returns a list of all the testsets in this workflow, and all of its children.

$tf = $wf->testfile()

Returns the Fennec::TestFile object currently being run.

$pwf = $wf->parent()

Returns the parent workflow object to which this one is a child, the root workflow will return the TestFile object.

$testsets = $wf->_testsets()
$wf->_testsets( \@testsets )

Get/Set the list of testsets, if you override add_item() and never caller SUPER::add_item() then you will need to manually add TestSets to the arrayref returned by _testsets().

$workflows = $wf->_workflows()
$wf->_workflows( \@workflows )

Get/Set the list of workflows, if you override add_item() and never caller SUPER::add_item() then you will need to manually add Workflows to the arrayref returned by _workflows().

run_tests()

In a normal Fennec run this will only be called on the root Workflow object. Overriding this in your subclass will have NO EFFECT.

$wf->add_items( @items )

Calls $wf->add_item() for each item in @items.

CHAPTER 9 - RUNNER PLUGINS

It is also possible to create plugins that extend or modify the runner itself. There are a few ways in which you can hook into the runner.

CHAPTER 9.1 - CONFIGURATION OPTIONS

Fennec::Runner can export the add_config() function. This function allows you to add configuration items to the runner. These items can be listed in t/Fennec.t along with the usual ones. This will create the config option itself, as well as an accessor on the runner singleton by which it can be accessed.

add_config $CONFIG_NAME
add_config $CONFIG_NAME => $default
add_config $CONFIG_NAME => ( %OPTIONS )

Options:

default => $VALUE
default => sub { my ($data) = @_; ... }

Specify the default value when none is specified. Can either be scalar value (including refs) or a coderef. The only argument provided to the coderef is the $data hash. The $data hash contains all the options that have been calculated so far for the runner.

You absolutely should not modify the $data hash in coderefs passed to this parameter. The only fields guaranteed to be set are those listed in the depends option.

env_override => $VALUE

Set to false in order to disable environment overrides to this parameter. When not specified it defaults to true. When true (numeric) this parameter can be overriden using the FENNEC_[NAME] envireonment variable.

You can also specify a string name that will be used instead of FENNEC_[NAME].

modify => sub { my ($value, $data) = @_; ... }

This hook can be used to modify the value the user provides. It can also modify the default value that you provide. The first argument is the unaltered value. The second argument is the $data hash. See the section for the 'default' option for more info about $data.

depends => \@LIST

Arrayref of config options that must be calculated before this one.

CHAPTER 9.2 - TEST HOOKS

Test hooks are run after the root workflow has been instantiated, but before it has been processed. Test hooks are coderefs that receive the runner and test object as arguments.

Fennec::Runner can export the add_test_hook() function. This function allows you to add test hooks to the runner.

add_test_hook( sub { my( $runner, $test ) = @_; ... })

CHAPTER 9.3 - FINISH HOOKS

These are run just before the threader and collector are stopped. These are coderefs that recieve no arguments. Fennec::Runner can export the add_finish_hook() function. This function allows you to add finnish hooks to the runner.

add_finish_hook(sub {...})

AUTHORS

Chad Granum exodist7@gmail.com

COPYRIGHT

Copyright (C) 2010 Chad Granum

Fennec is free software; Standard perl licence.

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.