File Coverage

blib/lib/Test/WriteVariants.pm
Criterion Covered Total %
statement 77 135 57.0
branch 18 56 32.1
condition 4 13 30.7
subroutine 16 25 64.0
pod 11 11 100.0
total 126 240 52.5


line stmt bran cond sub pod time code
1             package Test::WriteVariants;
2              
3             =head1 NAME
4              
5             Test::WriteVariants - Dynamic generation of tests in nested combinations of contexts
6              
7             =head1 SYNOPSIS
8              
9             use Test::WriteVariants;
10              
11             my $test_writer = Test::WriteVariants->new();
12              
13             $test_writer->write_test_variants(
14              
15             # tests we want to run in various contexts
16             input_tests => {
17             'core/10-foo' => { require => 't/core/10-foo.t' },
18             'core/20-bar' => { require => 't/core/20-bar.t' },
19             },
20              
21             # one or more providers of variant contexts
22             variant_providers => [
23             sub {
24             my ($path, $context, $tests) = @_;
25             my %variants = (
26             plain => $context->new_env_var(MY_MODULE_PUREPERL => 0),
27             pureperl => $context->new_env_var(MY_MODULE_PUREPERL => 1),
28             );
29             return %variants;
30             },
31             sub {
32             my ($path, $context, $tests) = @_;
33             my %variants = map {
34             $_ => $context->new_env_var(MY_MODULE_WIBBLE => $_),
35             } 1..3;
36             delete $variants{3} if $context->get_env_var("MY_MODULE_PUREPERL");
37             return %variants;
38             },
39             ],
40              
41             # where to generate the .t files that wrap the input_tests
42             output_dir => 't/variants',
43             );
44              
45             When run that generates the desired test variants:
46              
47             Writing t/variants/plain/1/core/10-foo.t
48             Writing t/variants/plain/1/core/20-bar.t
49             Writing t/variants/plain/2/core/10-foo.t
50             Writing t/variants/plain/2/core/20-bar.t
51             Writing t/variants/plain/3/core/10-foo.t
52             Writing t/variants/plain/3/core/20-bar.t
53             Writing t/variants/pureperl/1/core/10-foo.t
54             Writing t/variants/pureperl/1/core/20-bar.t
55             Writing t/variants/pureperl/2/core/10-foo.t
56             Writing t/variants/pureperl/2/core/20-bar.t
57              
58             Here's what t/variants/pureperl/2/core/20-bar.t looks like:
59              
60             #!perl
61             $ENV{MY_MODULE_WIBBLE} = 2;
62             END { delete $ENV{MY_MODULE_WIBBLE} } # for VMS
63             $ENV{MY_MODULE_PUREPERL} = 1;
64             END { delete $ENV{MY_MODULE_PUREPERL} } # for VMS
65             require 't/core/20-bar.t';
66              
67              
68             Here's an example that uses plugins to provide the tests and the variants:
69              
70             my $test_writer = Test::WriteVariants->new();
71              
72             # gather set of input tests that we want to run in various contexts
73             # these can come from various sources, including modules and test files
74             my $input_tests = $test_writer->find_input_test_modules(
75             search_path => [ 'DBI::TestCase' ]
76             );
77              
78             $test_writer->write_test_variants(
79              
80             # tests we want to run in various contexts
81             input_tests => $input_tests,
82              
83             # one or more providers of variant contexts
84             # (these can be code refs or plugin namespaces)
85             variant_providers => [
86             "DBI::Test::VariantDBI",
87             "DBI::Test::VariantDriver",
88             "DBI::Test::VariantDBD",
89             ],
90              
91             # where to generate the .t files that wrap the input_tests
92             output_dir => $output_dir,
93             );
94              
95             =head1 DESCRIPTION
96              
97             NOTE: This is alpha code that's still evolving - nothing is stable.
98              
99             See L (on github) for an example use.
100              
101             =cut
102              
103 2     2   50444 use strict;
  2         3  
  2         60  
104 2     2   6 use warnings;
  2         2  
  2         45  
105              
106 2     2   15 use File::Path;
  2         4  
  2         96  
107 2     2   8 use File::Basename;
  2         2  
  2         112  
108 2     2   7 use Carp qw(croak confess);
  2         2  
  2         95  
109              
110 2     2   883 use Module::Pluggable::Object;
  2         10740  
  2         53  
111              
112 2     2   652 use Test::WriteVariants::Context;
  2         4  
  2         48  
113 2     2   777 use Data::Tumbler;
  2         6564  
  2         2358  
114              
115             our $VERSION = '0.012';
116              
117             =head1 METHODS
118              
119             =head2 new
120              
121             $test_writer = Test::WriteVariants->new(%attributes);
122              
123             Instanciates a Test::WriteVariants instance and sets the specified attributes, if any.
124              
125             =cut
126              
127             sub new {
128 1     1 1 665 my ($class, %args) = @_;
129              
130 1         3 my $self = bless {} => $class;
131              
132 1         3 for my $attribute (qw(allow_dir_overwrite allow_file_overwrite)) {
133 2 50       7 next unless exists $args{$attribute};
134 0         0 $self->$attribute(delete $args{$attribute});
135             }
136 1 50       3 confess "Unknown $class arguments: @{[ keys %args ]}"
  0         0  
137             if %args;
138              
139 1         2 return $self;
140             }
141              
142              
143             =head2 allow_dir_overwrite
144              
145             $test_writer->allow_dir_overwrite($bool);
146             $bool = $test_writer->allow_dir_overwrite;
147              
148             If the output directory already exists when tumble() is called it'll
149             throw an exception (and warn if it wasn't created during the run).
150             Setting allow_dir_overwrite true disables this safety check.
151              
152             =cut
153              
154             sub allow_dir_overwrite {
155 0     0 1 0 my $self = shift;
156 0 0       0 $self->{allow_dir_overwrite} = shift if @_;
157 0         0 return $self->{allow_dir_overwrite};
158             }
159              
160              
161             =head2 allow_file_overwrite
162              
163             $test_writer->allow_file_overwrite($bool);
164             $bool = $test_writer->allow_file_overwrite;
165              
166             If the test file that's about to be written already exists
167             then write_output_files() will throw an exception.
168             Setting allow_file_overwrite true disables this safety check.
169              
170             =cut
171              
172             sub allow_file_overwrite {
173 0     0 1 0 my $self = shift;
174 0 0       0 $self->{allow_file_overwrite} = shift if @_;
175 0         0 return $self->{allow_file_overwrite};
176             }
177              
178              
179             =head2 write_test_variants
180              
181             $test_writer->write_test_variants(
182             input_tests => \%input_tests,
183             variant_providers => \@variant_providers,
184             output_dir => $output_dir,
185             );
186              
187             Instanciates a L. Sets its C to call:
188              
189             $self->write_output_files($path, $context, $payload, $output_dir)
190              
191             and sets its C to call:
192              
193             $context->new($context, $item);
194              
195             and then calls its C method:
196              
197             $tumbler->tumble(
198             $self->normalize_providers($variant_providers),
199             [],
200             Test::WriteVariants::Context->new(),
201             $input_tests,
202             );
203              
204             =cut
205              
206             sub write_test_variants {
207 1     1 1 22 my ($self, %args) = @_;
208              
209 1 50       7 my $input_tests = delete $args{input_tests}
210             or croak "input_tests not specified";
211 1 50       3 my $variant_providers = delete $args{variant_providers}
212             or croak "variant_providers not specified";
213 1 50       4 my $output_dir = delete $args{output_dir}
214             or croak "output_dir not specified";
215 1 50       4 croak "write_test_variants: unknown arguments: @{[ keys %args ]}"
  0         0  
216             if keys %args;
217              
218 1 50 33     12 croak "write_test_variants: $output_dir already exists"
219             if -d $output_dir and not $self->allow_dir_overwrite;
220              
221             my $tumbler = Data::Tumbler->new(
222             consumer => sub {
223 4     4   22 my ($path, $context, $payload) = @_;
224             # payload is a clone of input_tests possibly modified by providers
225 4         7 $self->write_output_files($path, $context, $payload, $output_dir);
226             },
227             add_context => sub {
228 6     6   211 my ($context, $item) = @_;
229 6         12 return $context->new($context, $item);
230             },
231 1         19 );
232              
233 1         45 $tumbler->tumble(
234             $self->normalize_providers($variant_providers),
235             [],
236             Test::WriteVariants::Context->new(),
237             $input_tests, # payload
238             );
239              
240 1 0 33     20 warn "No tests written to $output_dir!\n"
241             if not -d $output_dir and not $self->allow_dir_overwrite;
242              
243 1         14 return;
244             }
245              
246              
247              
248             # ------
249              
250             # XXX also implement a find_input_test_files - that finds .t files
251              
252             =head2 find_input_test_modules
253              
254             $input_tests = $test_writer->find_input_test_modules(
255             );
256              
257             =cut
258              
259             sub find_input_test_modules {
260 0     0 1 0 my ($self, %args) = @_;
261              
262 0 0       0 my $namespaces = delete $args{search_path}
263             or croak "search_path not specified";
264 0         0 my $search_dirs = delete $args{search_dirs};
265 0         0 my $test_prefix = delete $args{test_prefix};
266 0   0     0 my $input_tests = delete $args{input_tests} || {};
267 0 0       0 croak "find_input_test_modules: unknown arguments: @{[ keys %args ]}"
  0         0  
268             if keys %args;
269              
270 0         0 my $edit_test_name;
271 0 0       0 if (defined $test_prefix) {
272 0         0 my $namespaces_regex = join "|", map { quotemeta($_) } @$namespaces;
  0         0  
273 0         0 my $namespaces_qr = qr/^($namespaces_regex)::/;
274 0     0   0 $edit_test_name = sub { s/$namespaces_qr/$test_prefix/ };
  0         0  
275             }
276              
277 0         0 my @test_case_modules = Module::Pluggable::Object->new(
278             require => 0,
279             search_path => $namespaces,
280             search_dirs => $search_dirs,
281             )->plugins;
282              
283             #warn "find_input_test_modules @$namespaces: @test_case_modules";
284              
285 0         0 for my $module_name (@test_case_modules) {
286 0         0 $self->add_test_module($input_tests, $module_name, $edit_test_name);
287             }
288              
289 0         0 return $input_tests;
290             }
291              
292              
293             =head2 find_input_test_files
294              
295             Not yet implemented - will file .t files.
296              
297             =cut
298              
299              
300             =head2 add_test
301              
302             $test_writer->add_test(
303             $input_tests, # the \%input_tests to add the test module to
304             $test_name, # the key to use in \%input_tests
305             $test_spec # the details of the test file
306             );
307              
308             Adds the $test_spec to %$input_tests keys by $test_name. In other words:
309              
310             $input_tests->{ $test_name } = $test_spec;
311              
312             An exception will be thrown if a test with $test_name already exists
313             in %$input_tests.
314              
315             This is a low-level interface that's not usually called directly.
316             See L.
317              
318             =cut
319              
320             sub add_test {
321 0     0 1 0 my ($self, $input_tests, $test_name, $test_spec) = @_;
322              
323 0 0       0 confess "Can't add test $test_name because a test with that name exists"
324             if $input_tests->{ $test_name };
325              
326 0         0 $input_tests->{ $test_name } = $test_spec;
327 0         0 return;
328             }
329              
330              
331             =head2 add_test_module
332              
333             $test_writer->add_test_module(
334             $input_tests, # the \%input_tests to add the test module to
335             $module_name, # the package name of the test module
336             $edit_test_name # a code ref to edit the test module name in $_
337             );
338              
339             =cut
340              
341             sub add_test_module {
342 0     0 1 0 my ($self, $input_tests, $module_name, $edit_test_name) = @_;
343              
344             # map module name, without the namespace prefix, to a dir path
345 0         0 local $_ = $module_name;
346 0 0       0 $edit_test_name->() if $edit_test_name;
347 0         0 s{[^\w:]+}{_}g;
348 0         0 s{::}{/}g;
349              
350 0         0 $self->add_test($input_tests, $_, {
351             class => $module_name,
352             method => 'run_tests',
353             });
354              
355 0         0 return;
356             }
357              
358              
359             =head2 normalize_providers
360              
361             $providers = $test_writer->normalize_providers($providers);
362              
363             Given a reference to an array of providers, returns a reference to a new array.
364             Any code references in the original array are passed through unchanged.
365              
366             Any other value is treated as a package name and passed to
367             L as a namespace C to find plugins.
368             An exception is thrown if no plugins are found.
369              
370             The corresponding element of the original $providers array is replaced with a
371             new provider code reference which calls the C, C,
372             and C methods, if present, for each plugin namespace in turn.
373              
374             Normal L provider subroutines are called with these arguments:
375              
376             ($path, $context, $tests)
377              
378             and the return value is expected to be a hash. Whereas the plugin provider
379             methods are called with these arguments:
380              
381             ($test_writer, $path, $context, $tests, $variants)
382              
383             and the return value is ignored. The $variants argument is a reference to a
384             hash that will be returned to Data::Tumbler and which should be edited by the
385             plugin provider method. This allows a plugin to see, and change, the variants
386             requested by any other plugins that have already been run for this provider.
387              
388             =cut
389              
390             sub normalize_providers {
391 1     1 1 2 my ($self, $input_providers) = @_;
392 1         3 my @providers = @$input_providers;
393              
394             # if a provider is a namespace name instead of a code ref
395             # then replace it with a code ref that uses Module::Pluggable
396             # to load and run the provider classes in that namespace
397              
398 1         3 for my $provider (@providers) {
399 2 50       5 next if ref $provider eq 'CODE';
400              
401             my @test_variant_modules = Module::Pluggable::Object->new(
402             search_path => [ $provider ],
403             # for sanity:
404             require => 1,
405 0     0   0 on_require_error => sub { croak "@_" },
406 0     0   0 on_instantiate_error => sub { croak "@_" },
407 0         0 )->plugins;
408 0         0 @test_variant_modules = sort @test_variant_modules;
409              
410 0 0       0 croak "No variant providers found in $provider\:: namespace"
411             unless @test_variant_modules;
412              
413 0         0 warn sprintf "Variant providers in %s: %s\n", $provider, join(", ", map {
414 0         0 (my $n=$_) =~ s/^${provider}:://; $n
  0         0  
415             } @test_variant_modules);
416              
417             $provider = sub {
418 0     0   0 my ($path, $context, $tests) = @_;
419              
420 0         0 my %variants;
421             # loop over several methods as a basic way of letting plugins
422             # hook in either early or late if they need to
423 0         0 for my $method (qw(provider_initial provider provider_final)) {
424 0         0 for my $test_variant_module (@test_variant_modules) {
425 0 0       0 next unless $test_variant_module->can($method);
426             #warn "$test_variant_module $method...\n";
427 0         0 my $fqsn = "$test_variant_module\::$method";
428 0         0 $self->$fqsn($path, $context, $tests, \%variants);
429             #warn "$test_variant_module $method: @{[ keys %variants ]}\n";
430             }
431             }
432              
433 0         0 return %variants;
434 0         0 };
435             }
436              
437 1         11 return \@providers;
438             }
439              
440              
441             =head2 write_output_files
442              
443             $test_writer->write_output_files($path, $context, $input_tests, $output_dir);
444              
445             Writes test files for each test in %$input_tests, for the given $path and $context,
446             into the $output_dir.
447              
448             The $output_dir, @$path, and key of %$input_tests are concatenated to form a
449             file name. A ".t" is added if not already present.
450              
451             Calls L to get the content of the test file, and then
452             calls L to write it.
453              
454             =cut
455              
456             sub write_output_files {
457 4     4 1 5 my ($self, $path, $context, $input_tests, $output_dir) = @_;
458              
459 4         10 my $base_dir_path = join "/", $output_dir, @$path;
460              
461 4         9 for my $testname (sort keys %$input_tests) {
462 8         11 my $test_spec = $input_tests->{$testname};
463              
464             # note that $testname can include a subdirectory path
465 8 50       23 $testname .= ".t" unless $testname =~ m/\.t$/;
466 8         14 my $full_path = "$base_dir_path/$testname";
467              
468 8         52 warn "Writing $full_path\n";
469             #warn "test_spec: @{[ %$test_spec ]}";
470              
471 8         18 my $test_script = $self->get_test_file_body($context, $test_spec);
472              
473 8         12 $self->write_file($full_path, $test_script);
474             }
475              
476 4         11 return;
477             }
478              
479              
480             =head2 write_file
481              
482             $test_writer->write_file($filepath, $content);
483              
484             Throws an exception if $filepath already exists and L is
485             not true.
486              
487             Creates $filepath and writes $content to it.
488             Creates any directories that are needed.
489             Throws an exception on error.
490              
491             =cut
492              
493             sub write_file {
494 8     8 1 6 my ($self, $filepath, $content) = @_;
495              
496 8 50 33     102 croak "$filepath already exists!\n"
497             if -e $filepath and not $self->allow_file_overwrite;
498              
499 8         228 my $full_dir_path = dirname($filepath);
500 8 100       545 mkpath($full_dir_path, 0)
501             unless -d $full_dir_path;
502              
503 8 50       357 open my $fh, ">", $filepath
504             or croak "Can't write to $filepath: $!";
505 8         22 print $fh $content;
506 8 50       167 close $fh
507             or croak "Error writing to $filepath: $!";
508              
509 8         34 return;
510             }
511              
512              
513             =head2 get_test_file_body
514              
515             $test_body = $test_writer->get_test_file_body($context, $test_spec);
516              
517             XXX This should probably be a method call on an object
518             instanciated by the find_input_test_* methods.
519              
520             =cut
521              
522             sub get_test_file_body {
523 8     8 1 8 my ($self, $context, $test_spec) = @_;
524              
525 8         6 my @body;
526              
527 8   50     27 push @body, $test_spec->{prologue} || qq{#!perl\n\n};
528              
529 8         17 push @body, $context->get_code;
530 8         8 push @body, "\n";
531              
532 8 50       11 push @body, "use lib '$test_spec->{lib}';\n\n"
533             if $test_spec->{lib};
534              
535 8 50       13 push @body, "require '$test_spec->{require}';\n\n"
536             if $test_spec->{require};
537              
538 8 50       11 if (my $class = $test_spec->{class}) {
539 0         0 push @body, "require $class;\n\n";
540 0         0 my $method = $test_spec->{method};
541 0 0       0 push @body, "$class->$method;\n\n" if $method;
542             }
543              
544 8 50       11 push @body, "$test_spec->{code}\n\n"
545             if $test_spec->{code};
546              
547 8         12 return join "", @body;
548             }
549              
550              
551              
552             1;
553              
554             __END__