sync with main trunk
[bioperl-live.git] / Bio / Root / Test.pm
blobf705c5ca68abc4cfdf478ecb6857afbd765a27a6
1 # $Id$
3 # BioPerl module for Bio::Root::Test
5 # Cared for by Sendu Bala <bix@sendu.me.uk>
7 # Copyright Sendu Bala
9 # You may distribute this module under the same terms as perl itself
11 # POD documentation - main docs before the code
13 =head1 NAME
15 Bio::Root::Test - A common base for all Bioperl test scripts.
17 =head1 SYNOPSIS
19 use lib '.'; # (for core package tests only)
20 use Bio::Root::Test;
22 test_begin(-tests => 20,
23 -requires_modules => [qw(IO::String XML::Parser)],
24 -requires_networking => 1);
26 my $do_network_tests = test_network();
27 my $output_debugging = test_debug();
29 # carry out tests with Test::More, Test::Exception and Test::Warn syntax
31 SKIP: {
32 # these tests need version 2.6 of Optional::Module to work
33 test_skip(-tests => 10, -requires_module => 'Optional::Module 2.6');
34 use_ok('Optional::Module');
36 # 9 other optional tests that need Optional::Module
39 SKIP: {
40 test_skip(-tests => 10, -requires_networking => 1);
42 # 10 optional tests that require internet access (only makes sense in the
43 # context of a script that doesn't use -requires_networking in the call to
44 # &test_begin)
47 # in unix terms, we want to test with a file t/data/input_file.txt
48 my $input_file = test_input_file('input_file.txt');
50 # we want the name of a file we can write to, that will be automatically
51 # deleted when the test script finishes
52 my $output_file = test_output_file();
54 # we want the name of a directory we can store files in, that will be
55 # automatically deleted when the test script finishes
56 my $output_dir = test_output_dir();
58 =head1 DESCRIPTION
60 This provides a common base for all BioPerl test scripts. It safely handles the
61 loading of Test::More, Test::Exception and Test::Warn (actually, a subclass
62 compatible with Bioperl warnings) prior to tests being run. It also presents an
63 interface to common needs such as skipping all tests if required modules aren't
64 present or if network tests haven't been enabled. See test_begin().
66 In the same way, it allows you to skip just a subset of tests for those same
67 reasons, in addition to requiring certain executables and environment variables.
68 See test_skip().
70 It also has two further methods that let you decide if network tests should be
71 run, and if debugging information should be printed. See test_network() and
72 test_debug().
74 Finally, it presents a consistent way of getting the path to input and output
75 files. See test_input_file(), test_output_file() and test_output_dir().
77 =head1 FEEDBACK
79 =head2 Mailing Lists
81 User feedback is an integral part of the evolution of this and other
82 Bioperl modules. Send your comments and suggestions preferably to
83 the Bioperl mailing list. Your participation is much appreciated.
85 bioperl-l@bioperl.org - General discussion
86 http://bioperl.org/wiki/Mailing_lists - About the mailing lists
88 =head2 Reporting Bugs
90 Report bugs to the Bioperl bug tracking system to help us keep track
91 of the bugs and their resolution. Bug reports can be submitted via
92 the web:
94 http://bugzilla.open-bio.org/
96 =head1 AUTHOR - Sendu Bala
98 Email bix@sendu.me.uk
100 =head1 APPENDIX
102 The rest of the documentation details each of the object methods.
103 Internal methods are usually preceded with a _
105 =cut
107 package Bio::Root::Test;
109 use strict;
110 use warnings;
112 use File::Temp qw(tempdir);
113 use File::Spec;
114 use Exporter qw(import);
116 BEGIN {
117 # For prototyping reasons, we have to load Test::More's methods now, even
118 # though theoretically in future the user may use a different Test framework
120 # We want to load Test::More, Test::Exception and Test::Warn. Preferably the
121 # users own versions, but if they don't have them, the ones in t/lib.
122 # However, this module is in t/lib so t/lib is already in @INC so Test::* in
123 # t/lib will be used first, which we don't want: get rid of t/lib in @INC
124 no lib 't/lib';
125 eval { require Test::More;
126 require Test::Exception;
127 require Test::Warn; };
128 if ($@) {
129 eval "use lib 't/lib';";
131 eval "use Test::More;
132 use Test::Exception;";
133 die "$@\n" if $@;
135 # now that the users' Test::Warn has been loaded if they had it, we can
136 # use Bio::Root::TestWarn
137 eval "use Bio::Root::Test::Warn;";
138 die "$@\n" if $@;
141 # re-export Test::More, Test::Exception and Test::Warn methods and export our own
142 our @EXPORT = qw(ok use_ok require_ok
143 is isnt like unlike is_deeply
144 cmp_ok
145 skip todo todo_skip
146 pass fail
147 eq_array eq_hash eq_set
148 $TODO
149 plan
150 can_ok isa_ok
151 diag
152 BAIL_OUT
154 dies_ok
155 lives_ok
156 throws_ok
157 lives_and
159 warning_is
160 warnings_are
161 warning_like
162 warnings_like
164 test_begin
165 test_skip
166 test_output_file
167 test_output_dir
168 test_input_file
169 test_network
170 test_debug
171 float_is
174 our $GLOBAL_FRAMEWORK = 'Test::More';
175 our @TEMP_FILES;
177 =head2 test_begin
179 Title : test_begin
180 Usage : test_begin(-tests => 20);
181 Function: Begin your test script, setting up the plan (skip all tests, or run
182 them all)
183 Returns : True if tests should be run.
184 Args : -tests => int (REQUIRED, the number of tests that will
185 be run)
186 -requires_modules => [] (array ref of module names that are
187 required; if any don't load, all tests
188 will be skipped. To specify a required
189 version of a module, include the version
190 number after the module name, separated
191 by a space)
192 -requires_module => str (as above, but for just one module)
193 -requires_networking => 1|0 (default 0, if true all tests will be
194 skipped if network tests haven't been
195 enabled in Build.PL)
196 -excludes_os => str (default none, if OS suppied, all tests
197 will skip if running on that OS (eg.
198 'mswin'))
199 -framework => str (default 'Test::More', the Test module
200 to load. NB: experimental, avoid using)
202 Note, supplying -tests => 0 is possible, allowing you to skip all
203 tests in the case that a test script is testing deprecated modules
204 that have yet to be removed from the distribution
206 =cut
208 sub test_begin {
209 my ($skip_all, $tests, $framework) = _skip(@_);
210 $GLOBAL_FRAMEWORK = $framework;
212 if ($framework eq 'Test::More') {
213 # ideally we'd delay loading Test::More until this point, but see BEGIN
214 # block
216 if ($skip_all) {
217 eval "plan skip_all => '$skip_all';";
219 elsif ($tests == 0) {
220 eval "plan skip_all => 'All tests are being skipped, probably because the module(s) being tested here are now deprecated';";
222 else {
223 eval "plan tests => $tests;";
226 return 1;
228 # go ahead and add support for other frameworks here
229 else {
230 die "Only Test::More is supported at the current time\n";
233 return 0;
236 =head2 test_skip
238 Title : test_skip
239 Usage : SKIP: {
240 test_skip(-tests => 10,
241 -requires_module => 'Optional::Module 2.01');
243 # 10 tests that need v2.01 of Optional::Module
245 Function: Skip a subset of tests for one of several common reasons: missing one
246 or more optional modules, network tests haven't been enabled, a
247 required binary isn't present, or an environmental variable isn't set
248 Returns : n/a
249 Args : -tests => int (REQUIRED, the number of tests that are
250 to be skipped in the event one of the
251 following options isn't satisfied)
252 -requires_modules => [] (array ref of module names that are
253 required; if any don't load, the desired
254 number of tests will be skipped. To
255 specify a required version of a module,
256 include the version number after the
257 module name, separated by a space)
258 -requires_module => str (as above, but for just one module)
259 -requires_executable => Bio::Tools::Run::WrapperBase instance
260 (checks WrapperBase::executable for the
261 presence of a binary, skips if absent)
262 -requires_env => str (checks %ENV for a specific env. variable,
263 skips if absent)
264 -excludes_os => str (default none, if OS suppied, desired num
265 of tests will skip if running on that OS
266 (eg. 'mswin'))
267 -requires_networking => 1 (if true the desired number of tests will
268 be skipped if network tests haven't been
269 enabled in Build.PL)
271 =cut
273 sub test_skip {
274 my ($skip, $tests, $framework) = _skip(@_);
275 $tests || die "-tests must be a number greater than 0";
277 if ($framework eq 'Test::More') {
278 if ($skip) {
279 eval "skip('$skip', $tests);";
282 # go ahead and add support for other frameworks here
283 else {
284 die "Only Test::More is supported at the current time\n";
288 =head2 test_output_file
290 Title : test_output_file
291 Usage : my $output_file = test_output_file();
292 Function: Get the full path of a file suitable for writing to.
293 When your test script ends, the file will be automatically deleted.
294 Returns : string (file path)
295 Args : none
297 =cut
299 sub test_output_file {
300 die "test_output_file takes no args\n" if @_;
302 my $tmp = File::Temp->new();
303 push(@TEMP_FILES, $tmp);
305 return $tmp->filename;
308 =head2 test_output_dir
310 Title : test_output_dir
311 Usage : my $output_dir = test_output_dir();
312 Function: Get the full path of a directory suitable for storing temporary files
314 When your test script ends, the directory and its contents will be
315 automatically deleted.
316 Returns : string (path)
317 Args : none
319 =cut
321 sub test_output_dir {
322 die "test_output_dir takes no args\n" if @_;
324 return tempdir(CLEANUP => 1);
327 =head2 test_input_file
329 Title : test_input_file
330 Usage : my $input_file = test_input_file();
331 Function: Get the path of a desired input file stored in the standard location
332 (currently t/data), but correct for all platforms.
333 Returns : string (file path)
334 Args : list of strings (ie. at least the input filename, preceded by the
335 names of any subdirectories within t/data)
336 eg. for the file t/data/in.file pass 'in.file', for the file
337 t/data/subdir/in.file, pass ('subdir', 'in.file')
339 =cut
341 sub test_input_file {
342 return File::Spec->catfile('t', 'data', @_);
345 =head2 test_network
347 Title : test_network
348 Usage : my $do_network_tests = test_network();
349 Function: Ask if network tests should be run.
350 Returns : boolean
351 Args : none
353 =cut
355 sub test_network {
356 require Module::Build;
357 my $build = Module::Build->current();
358 return $build->notes('network');
361 =head2 test_debug
363 Title : test_debug
364 Usage : my $output_debugging = test_debug();
365 Function: Ask if debugging information should be output.
366 Returns : boolean
367 Args : none
369 =cut
371 sub test_debug {
372 return $ENV{'BIOPERLDEBUG'} || 0;
375 =head2 float_is
377 Title : float_is
378 Usage : float_is($val1, $val2);
379 Function: test two floating point values for equality
380 Returns : Boolean based on test (can use in combination with diag)
381 Args : two scalar values (floating point numbers) (required via prototype)
382 test message (optional)
384 =cut
386 sub float_is ($$;$) {
387 my ($val1, $val2, $message) = @_;
388 # catch any potential undefined values and directly compare
389 if (!defined $val1 || !defined $val2) {
390 is($val1, $val2 ,$message);
391 } else {
392 is(sprintf("%g",$val1), sprintf("%g",$val2),$message);
396 # decide if should skip and generate skip message
397 sub _skip {
398 my %args = @_;
400 # handle input strictly
401 my $tests = $args{'-tests'};
402 (defined $tests && $tests =~ /^\d+$/) || die "-tests must be supplied and be an int\n";
403 delete $args{'-tests'};
405 my $req_mods = $args{'-requires_modules'};
406 delete $args{'-requires_modules'};
407 my @req_mods;
408 if ($req_mods) {
409 ref($req_mods) eq 'ARRAY' || die "-requires_modules takes an array ref\n";
410 @req_mods = @{$req_mods};
412 my $req_mod = $args{'-requires_module'};
413 delete $args{'-requires_module'};
414 if ($req_mod) {
415 ref($req_mod) && die "-requires_module takes a string\n";
416 push(@req_mods, $req_mod);
419 my $req_net = $args{'-requires_networking'};
420 delete $args{'-requires_networking'};
422 my $req_env = $args{'-requires_env'};
423 delete $args{'-requires_env'};
425 # strip any leading $ in case someone passes $FOO instead of 'FOO'
426 $req_env =~ s{^\$}{} if $req_env;
428 my $req_exe = $args{'-requires_executable'};
429 delete $args{'-requires_executable'};
431 if ($req_exe && (!ref($req_exe) || !$req_exe->isa('Bio::Tools::Run::WrapperBase'))) {
432 die "-requires_exe takes an argument of type Bio::Tools::Run::WrapperBase";
435 my $os = $args{'-excludes_os'};
436 delete $args{'-excludes_os'};
438 my $framework = $args{'-framework'} || $GLOBAL_FRAMEWORK;
439 delete $args{'-framework'};
441 # catch user mistakes
442 while (my ($key, $val) = each %args) {
443 die "unknown argument '$key' supplied, did you mistake 'required...' for 'requires...'?\n";
446 # test user requirments and return
447 if ($os) {
448 if ($^O =~ /$os/i) {
449 return ('Not compatible with your Operating System', $tests, $framework);
453 foreach my $mod (@req_mods) {
454 my $skip = _check_module($mod);
455 if ($skip) {
456 return ($skip, $tests, $framework);
460 if ($req_net && ! test_network()) {
461 return ('Network tests have not been requested', $tests, $framework);
464 if ($req_exe && !$req_exe->executable) {
465 my $msg = 'Required executable for '.ref($req_exe).' is not present';
466 diag($msg);
467 return ($msg, $tests, $framework);
470 if ($req_env && !exists $ENV{$req_env}) {
471 my $msg = 'Required environment variable $'.$req_env. ' is not set';
472 diag($msg);
473 return ($msg, $tests, $framework);
476 return ('', $tests, $framework);
479 sub _check_module {
480 my $mod = shift;
482 my $desired_version;
483 if ($mod =~ /(\S+)\s+(\S+)/) {
484 $mod = $1;
485 $desired_version = $2;
488 eval "require $mod;";
490 if ($@) {
491 return "The optional module $mod (or dependencies thereof) was not installed";
493 elsif ($desired_version) {
494 no strict 'refs';
495 unless (defined ${"${mod}::VERSION"}) {
496 return "The optional module $mod didn't have a version, but we want v$desired_version";
498 elsif (${"${mod}::VERSION"} < $desired_version) {
499 return "The optional module $mod was out of date (wanted v$desired_version)";
503 return;