File Coverage

blib/lib/Fennec/Lite.pm
Criterion Covered Total %
statement 185 190 97.3
branch 39 50 78.0
condition 10 23 43.4
subroutine 42 42 100.0
pod 12 15 80.0
total 288 320 90.0


line stmt bran cond sub pod time code
1             package Fennec::Lite;
2 3     3   2041 use strict;
  3         6  
  3         95  
3 3     3   15 use warnings;
  3         5  
  3         78  
4              
5 3     3   16 use Carp qw/ croak /;
  3         10  
  3         224  
6 3     3   17 use List::Util qw/ shuffle /;
  3         6  
  3         426  
7 3     3   15 use B;
  3         6  
  3         2613  
8              
9             our $VERSION = '0.004';
10              
11             our %MODULE_LOADERS = (
12             'Test::More' => sub {
13             my ( $self ) = @_;
14             my $into = $self->test_class;
15             require Test::More;
16              
17             my $plan = $self->plan || (Test::More->can('done_testing') ? '' : 'no_plan');
18             eval "package $into; Test::More->import(" . ($plan ? 'tests => $plan' : '') . "); 1"
19             || die $@;
20             },
21             );
22              
23 2     2 1 5 sub import_hook {}
24 4     4 1 12 sub module_loaders { \%MODULE_LOADERS }
25 2     2 1 11 sub must_load {qw/ Test::More /}
26 2     2 1 11 sub may_load {qw/
27             Test::Warn
28             Test::Exception
29             /};
30              
31             fennec_accessors(qw/
32             tests
33             test_class
34             seed
35             random
36             testing
37             alias
38             alias_to
39             plan
40             TB
41             /);
42              
43             sub import {
44 2     2   32 my $class = shift;
45 2         6 my %specs = @_;
46 2         5 my $caller = caller;
47              
48 2 50       12 $specs{random} = 1 unless defined $specs{random};
49              
50 2         10 my $instance = $class->new( %specs, test_class => $caller );
51              
52 2         12 $instance->_import_must_loads();
53 2         11 $instance->_import_way_loads();
54 2         12 $instance->_export_shortcuts();
55 2         9 $instance->_export_aliases();
56 2         9 $instance->_export_functions();
57 2         8 $instance->import_hook();
58              
59 2         2402 1;
60             }
61              
62             sub new {
63 4     4 0 8685 my $class = shift;
64 4         599 my @ltime = localtime(time);
65 4   33     128 my $self = bless ({
66             tests => [],
67             seed => $ENV{FENNEC_SEED} || join( '', @ltime[5,4,3] ),
68             @_,
69             }, $class );
70 4         17 $self->init();
71 4         21 return $self;
72             }
73              
74             sub init {
75 4     4 1 6 my $self = shift;
76 4         2707 require Test::Builder;
77 4         27738 $self->TB( Test::Builder->new());
78             }
79              
80             sub _import_loads {
81 4     4   9 my $self = shift;
82 4         12 my ( $no_die_on_fail, @load ) = @_;
83 4         17 my $handlers = $self->module_loaders;
84              
85 4         12 for my $package ( @load ) {
86 6 100       29 if ($handlers->{$package}) {
87 2         9 $handlers->{$package}->( $self );
88 2         14 next;
89             }
90 4         23 my ($ret, $error) = load_package_into( $package, $self->test_class );
91 4 50       29 next if $ret;
92 0 0 0     0 die $error unless $no_die_on_fail && $error =~ m/Can't locate [\w\d_\/\.]+\.pm in \@INC/;
93             }
94             }
95              
96             sub load_package_into {
97 4     4 0 9 my ( $load, $into ) = @_;
98 4         7 local $@;
99 2     2   2126 my $ret = eval "package $into; use $load; 1;";
  2     2   29432  
  2         132  
  2         2176  
  2         3577  
  2         11  
  4         280  
100 4 50       27 return $ret ? ( $ret ) : ( $ret, $@ );
101             }
102              
103             sub _import_must_loads {
104 2     2   5 my $self = shift;
105 2         11 $self->_import_loads( 0, $self->must_load );
106             }
107              
108             sub _import_way_loads {
109 2     2   4 my $self = shift;
110 2         11 $self->_import_loads( 1, $self->may_load );
111             }
112              
113             sub _export_shortcuts {
114 2     2   4 my $self = shift;
115 2   100     10 my $package = $self->testing || return;
116 1         5 my $into = $self->test_class;
117              
118 3     3   21 no strict 'refs';
  3         5  
  3         507  
119 1     1   6 *{"$into\::CLASS"} = sub { $package };
  1         7  
  1         1081  
120 1         2 *{"$into\::CLASS"} = \$package;
  1         5  
121             }
122              
123             sub _export_aliases {
124 2     2   6 my $self = shift;
125 2         8 my $caller = $self->test_class;
126              
127 2 100       27 if ( my $aliases = $self->alias ) {
128 1 50       6 $aliases = [ $aliases ] unless ref $aliases;
129 1         3 for my $class ( @$aliases ) {
130 1 50       59 eval "require $class; 1" || die $@;
131 3     3   17 no strict 'refs';
  3         4  
  3         446  
132 1         3 my $name = $class;
133 1         8 $name =~ s/^.*:([^:]+)$/$1/;
134 1     1   5 *{"$caller\::$name"} = sub { $class };
  1         9  
  1         5  
135             }
136             }
137              
138 2 100       50 if ( my $alias_map = $self->alias_to ) {
139 1         4 for my $name ( keys %$alias_map ) {
140 1         3 my $class = $alias_map->{ $name };
141 3     3   16 no strict 'refs';
  3         6  
  3         364  
142 1     1   4 *{"$caller\::$name"} = sub { $class };
  1         8  
  1         6  
143             }
144             }
145             }
146              
147             sub _export_functions {
148 2     2   4 my $self = shift;
149 2         8 my $into = $self->test_class;
150              
151 3     3   15 no strict 'refs';
  3         5  
  3         3369  
152 2     14   10 *{"$into\::tests"} = sub { $self->_add_tests( @_ ) };
  2         11  
  14         119  
153 2     4   9 *{"$into\::run_tests"} = sub { $self->run_tests( @_ ) };
  2         10  
  4         45  
154 2     1   7 *{"$into\::fennec"} = sub { return $self };
  2         15  
  1         7  
155              
156 2         7 *{"$into\::fennec_accessors"} = \&fennec_accessors;
  2         11  
157             }
158              
159             sub add_tests {
160 13     13 1 87 my $self = shift;
161 13         33 $self->_add_tests( @_ );
162             }
163              
164             sub _add_tests {
165 27     27   34 my $self = shift;
166 27         150 ( undef, undef, my $end_line ) = caller(1);
167 27         58 my $name = shift;
168 27 100       110 my %proto = ( @_ == 1 )
169             ? ( method => $_[0] )
170             : @_;
171              
172 27 50       78 $proto{ name } = $name if $name;
173 27   66     82 $proto{ method } ||= $proto{ code } || $proto{ sub };
      66        
174 27         50 $proto{ end_line } = $end_line;
175 27         246 $proto{ start_line } = B::svref_2object( $proto{ method })->START->line;
176              
177 27 50       79 croak "You must name your test group"
178             unless $proto{name};
179              
180 27 50       56 croak "You must provide a coderef as one of the following params 'method', 'code', or 'sub'."
181             unless $proto{method};
182              
183 27         36 push @{$self->tests} => \%proto;
  27         57  
184             }
185              
186             sub run_tests {
187 8     8 1 39 my $self = shift;
188 8         21 my %params = @_;
189 8         21 my $tests = $self->tests;
190 8         16 my $pass = 1;
191 8         17 my $item = $ENV{FENNEC_ITEM};
192              
193 8         22 my $invocant_class = $self->test_class;
194 8 100       82 my $invocant = $invocant_class->can( 'new' )
195             ? $invocant_class->new( %params )
196             : bless( \%params, $invocant_class );
197              
198             # Seed before randomizing tests, for reproducibility
199 8         49 srand( $self->seed );
200 8 100       26 $tests = [ shuffle @$tests ]
201             if $self->random;
202              
203 8         21 for my $test ( @$tests ) {
204 27         71 my $method = $test->{method};
205 27         74 my $name = $test->{name};
206              
207 27 100       59 if ( $item ) {
208 12 100       87 if ( $item =~ m/^\d+$/ ) {
209 6 100       25 next unless $test->{start_line} <= ($item + 1);
210 4 100       15 next unless $test->{end_line} >= $item;
211             }
212             else {
213 6 100       23 next unless $name eq $item;
214             }
215             }
216              
217 19 100       54 if ( $test->{ skip }) {
    100          
218 2   33     14 $pass &&= $self->run_skip_group( $invocant, $test );
219             }
220             elsif( $test->{ todo }) {
221 2   33     447 $pass &&= $self->run_todo_group( $invocant, $test );
222             }
223             else {
224 15   33     67 $pass &&= $self->run_test_group( $invocant, $test );
225             }
226             }
227              
228 8         28 $self->tests([]);
229 8         112 return $pass;
230             }
231              
232             sub run_skip_group {
233 2     2 0 4 my $self = shift;
234 2         3 my ( $invocant, $test ) = @_;
235 2         5 my $name = $test->{ name };
236 2         6 $self->TB->note( "Skipping: $name" );
237 2         194 $self->TB->skip( $test->{skip} );
238 2         361 1;
239             }
240              
241             sub run_todo_group {
242 2     2 1 5 my $self = shift;
243 2         4 my ( $invocant, $test ) = @_;
244 2         7 $self->TB->todo_start( $test->{todo} );
245 2         88 my $out = $self->run_test_eval( $invocant, $test );
246 2         6 $self->TB->todo_end();
247 2         27 return $out;
248             }
249              
250             sub run_test_group {
251 15     15 1 21 my $self = shift;
252 15         21 my ( $invocant, $test ) = @_;
253 15         42 $self->run_test_eval( $invocant, $test );
254             }
255              
256             sub run_test_eval {
257 17     17 1 26 my $self = shift;
258 17         23 my ( $invocant, $test ) = @_;
259              
260             # Seed again before running test, for reproducibility
261 17         32 srand( $self->seed );
262 17         30 my $ret = eval { $test->{method}->( $invocant ); 1 };
  17         50  
  15         5793  
263 17 100       118 return $ret ? $ret : $self->test_eval_error( $ret, $@, $test );
264             }
265              
266             sub test_eval_error {
267 2     2 1 4 my $self = shift;
268 2         5 my ( $ret, $error, $test ) = @_;
269              
270 2 50       18 return !$ret if $test->{ should_fail };
271              
272 0         0 my $name = $test->{name};
273 0         0 $self->TB->ok( $ret, "Test Group '$name' died (it should not)" );
274 0         0 $self->TB->diag( $error );
275              
276 0         0 return $ret;
277             }
278              
279             sub fennec_accessors {
280 5     5 1 37 my $caller = caller;
281 5         13 for my $name ( @_ ) {
282             my $sub = sub {
283 119     119   957 my $self = shift;
284 119 100       392 ( $self->{ $name }) = @_ if @_;
285 119         508 return $self->{ $name };
286 31         151 };
287 3     3   45 no strict 'refs';
  3         15  
  3         6672  
288 31         37 *{"$caller\::$name"} = $sub;
  31         152  
289             }
290             }
291              
292             1;
293              
294             =head1 NAME
295              
296             Fennec::Lite - Minimalist Fennec, the commonly used bits.
297              
298             =head1 DESCRIPTION
299              
300             L<Fennec> does a ton, but it may be hard to adopt it all at once. It also is a
301             large project, and has not yet been fully split into component projects.
302             Fennec::Lite takes a minimalist approach to do for Fennec what Mouse does for
303             Moose.
304              
305             Fennec::Lite is a single module file with no non-core dependencies. It can
306             easily be used by any project, either directly, or by copying it into your
307             project. The file itself is less than 300 lines of code at the time of this
308             writing, that includes whitespace.
309              
310             This module does not cover any of the more advanced features such as result
311             capturing or SPEC workflows. This module only covers test grouping and group
312             randomization. You can also use the FENNEC_ITEM variable with a group name or
313             line number to run a specific test group only. Test::Builder is used under the
314             hood for TAP output.
315              
316             =head1 SYNOPSIS
317              
318             =head2 SIMPLE
319              
320             #!/usr/bin/perl
321             use strict;
322             use warnings;
323              
324             # Brings in Test::More for us.
325             use Fennec::Lite;
326              
327             tests good => sub {
328             ok( 1, "A good test" );
329             };
330              
331             # You most call run_tests() after declaring your tests.
332             run_tests();
333             done_testing();
334              
335             =head2 ADVANCED
336              
337             #!/usr/bin/perl
338             use strict;
339             use warnings;
340              
341             use Fennec::Lite
342             plan => 8,
343             random => 1,
344             testing => 'My::Class',
345             alias => [
346             'My::Class::ThingA'
347             ],
348             alias_to => {
349             TB => 'My::Class::ThingB',
350             };
351              
352             # Quickly create get/set accessors
353             fennec_accessors qw/ construction_string /;
354              
355             # Create a constructor for our test class.
356             sub new {
357             my $class = shift;
358             my $string = @_;
359             return bless({ construction_string => $string }, $class );
360             }
361              
362             tests good => sub {
363             # Get $self. Created with new()
364             my $self = shift;
365             $self->isa_ok( __PACKAGE__ );
366             is(
367             $self->construction_string,
368             "This is the construction string",
369             "Constructed properly"
370             );
371             ok( 1, "A good test" );
372             };
373              
374             tests "todo group" => (
375             todo => "This will fail",
376             code => sub { ok( 0, "false value" )},
377             );
378              
379             tests "skip group" => (
380             skip => "This will fail badly",
381             sub => sub { die "oops" },
382             );
383              
384             run_tests( "This is the construction string" );
385              
386             =head2 Pure OO Interface
387              
388             #!/usr/bin/perl
389             use strict;
390             use warnings;
391              
392             use Fennec::Lite ();
393             use Test::More;
394              
395             my $fennec = Fennec::Lite->new( test_class => __PACKAGE__ );
396              
397             $fennec->add_tests( "test name" => sub {
398             ok( ... );
399             });
400              
401             $fennec->run_tests;
402              
403             done_testing();
404              
405             =head1 IMPORTED FOR YOU
406              
407             When you use Fennec::Lite, L<Test::More> is automatically imported for you. In
408             addition L<Test::Warn> and L<Test::Exception> will also be loaded, but only if
409             they are installed.
410              
411             =head1 IMPORT ARGUMENTS
412              
413             use Fennec::Lite %ARGS
414              
415             =over 4
416              
417             =item plan => 'no_plan' || $count
418              
419             Plan to pass into Test::More.
420              
421             =item random => $bool
422              
423             True by default. When true test groups will be run in random order.
424              
425             =item testing => $CLASS_NAME
426              
427             Declare what class you ore testing. Provides $CLASS and CLASS(), both of which
428             are simply the name of the class being tested.
429              
430             =item alias => @PACKAGES
431              
432             Create alias functions your the given package. An alias is a function that
433             returns the package name. The aliases will be named after the last part of the
434             package name.
435              
436             =item alias_to => { $ALIAS => $PACKAGE, ... }
437              
438             Define aliases, keys are alias names, values are tho package names they should
439             return.
440              
441             =back
442              
443             =head1 RUNNING IN RANDOM ORDER
444              
445             By default test groups will be run in a random order. The random seed is the
446             current date (YYYYMMDD). This is used so that the order does not change on the
447             day you are editing your code. However the ardor will change daily allowing for
448             automated testing to find order dependent failures.
449              
450             You can manually set the random seed to reproduce a failure. The FENNEC_SEED
451             environment variable will be used as the seed when it is present.
452              
453             $ FENNEC_SEED="20100915" prove -I lib -v t/*.t
454              
455             =head1 RUNNING SPECIFIC GROUPS
456              
457             You can use the FENNEC_ITEM variable with a group name or line number to run a
458             specific test group only.
459              
460             $ FENNEC_ITEM="22" prove -I lib -v t/MyTest.t
461             $ FENNEC_ITEM="Test Group A" prove -I lib -v t/MyTest.t
462              
463             This can easily be integrated into an editor such as vim or emacs.
464              
465             =head1 EXPORTED FUNCTIONS
466              
467             =over 4
468              
469             =item tests $name => $coderef,
470              
471             =item tests $name => ( code => $coderef, todo => $reason )
472              
473             =item tests $name => ( code => $coderef, skip => $reason )
474              
475             =item tests $name => ( sub => $coderef )
476              
477             =item tests $name => ( method => $coderef )
478              
479             Declare a test group. The first argument must always be the test group name. In
480             the 2 part form the second argument must be a coderef. In the multi-part form
481             you may optionally declare the group as todo, or as a skip. A coderef must
482             always be provided, in multi-part form you may use the code, method, or sub
483             params for this purpose, they are all the same.
484              
485             =item run_tests( %params )
486              
487             Instantiate an instance of the test class, passing %params to the constructor.
488             If no constructor is present a default is used. All tests that have been added
489             will be run. All tests will be cleared, you may continue to declare tests and
490             call run_tests again to run the new tests.
491              
492             =item fennec()
493              
494             Returns the instance of Fennec::Lite created when you imported it. This is the
495             instance that tests() and run_tests() act upon.
496              
497             =item fennec_accessors( @NAMES )
498              
499             Quickly generate get/set accessors for your test class. You could alternatively
500             do it manually or use L<Moose>.
501              
502             =back
503              
504             =head1 PURE OO INTERFACE METHODS
505              
506             =over 4
507              
508             =item $tests_ref = $fennec->tests()
509              
510             Get a reference to the array of tests that have been added since the last run.
511              
512             =item $classname = $fennec->test_class( $classname )
513              
514             Get/Set the class name that will be used to create test objects that will act
515             as the invocant on all test methods.
516              
517             =item $seed = $fennec->seed( $newseed )
518              
519             Get/Set the random seed that will be used to re-seed srand() before randomizing
520             tests, as well as before each test.
521              
522             =item $bool = $fennec->random( $bool )
523              
524             Turn random on/off.
525              
526             =item $fennec->add_tests( $name => sub { ... })
527              
528             =item $fennec->add_tests( $name, %args, method => sub { ... })
529              
530             Add a test group.
531              
532             =item $fennec->run_tests( %test_class_construction_args )
533              
534             Run the test groups
535              
536             =item $bool = $fennec->run_skip_test( \%test )
537              
538             Run a skip test (really just returns true)
539              
540             =item $bool = $fennec->run_todo_group( \%test )
541              
542             Run a group as TODO
543              
544             =item $bool = $fennec->run_test_group( \%test )
545              
546             Run a test group.
547              
548             =item ( $bool, $error ) = $fennec->run_test_eval( \%test )
549              
550             Does the actual test running in an eval to capture errors.
551              
552             =item $fennec->test_eval_error( $bool, $error, \%test )
553              
554             Handle a test eval error.
555              
556             =back
557              
558             =head1 Extending Fennec::Lite
559              
560             In the tradition of the Fennec project, Fennec::Lite is designed to be
561             extensible. You can even easily subclass/edit Fennec::Lite to work with
562             alternatives to Test::Builder.
563              
564             =head2 METHODS TO OVERRIDE
565              
566             =over 4
567              
568             =item $fennec->init()
569              
570             Called by new prior to returning the newly constructed object. In Fennec::Lite
571             this loads L<Test::Builder> and puts a reference to it in the TB() accessor. If
572             you do want to replace L<Test::Builder> in your subclass you may do so by
573             overriding init().
574              
575             =item $fennec->run_skip_test( \%test )
576              
577             Calls Test::Builder->skip( $reason ), then returns true. Override this if you
578             replace Test::Builder in your subclass.
579              
580             =item $fennec->run_todo_group( \%test )
581              
582             Calls run_test_eval() in a TODO environment. Currently uses L<Test::Builder> to
583             start/stop TODO mode around the test. Override this if you wish to replace
584             Test::Builder.
585              
586             =item $fennec->test_eval_error( $bool, $error, \%test )
587              
588             Handle an exception thrown in a test group method. Currently calls
589             Test::Bulder->ok( 0, GROUP_NAME ).
590              
591             =item @list = must_load()
592              
593             Returns a list of modules that MUST be loaded into tho calling class (unless
594             used in OO form). This is currently only L<Test::More>.
595              
596             =item @list = may_load()
597              
598             Returns a list of modules that should be loaded only if they are installed.
599              
600             =item $name_to_code_ref = module_loaders()
601              
602             Returns a hashref containing package => sub { ... }. Use this if you need to
603             load modules in a custom way, currently Test::More has a special loader in here
604             to account for plans.
605              
606             =item $fennec->import_hook()
607              
608             Called on the instance that was created by import(), runs at the very end of
609             the import process. Currently does nothing.
610              
611             =back
612              
613             =head1 FENNEC PROJECT
614              
615             This module is part of the Fennec project. See L<Fennec> for more details.
616             Fennec is a project to develop an extensible and powerful testing framework.
617             Together the tools that make up the Fennec framework provide a potent testing
618             environment.
619              
620             The tools provided by Fennec are also useful on their own. Sometimes a tool
621             created for Fennec is useful outside the greater framework. Such tools are
622             turned into their own projects. This is one such project.
623              
624             =over 2
625              
626             =item L<Fennec> - The core framework
627              
628             The primary Fennec project that ties them all together.
629              
630             =back
631              
632             =head1 AUTHORS
633              
634             Chad Granum L<exodist7@gmail.com>
635              
636             =head1 COPYRIGHT
637              
638             Copyright (C) 2010 Chad Granum
639              
640             Fennec-Lite is free software; Standard perl license.
641              
642             Fennec-Lite is distributed in the hope that it will be useful, but WITHOUT ANY
643             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
644             FOR A PARTICULAR PURPOSE. See the license for more details.