File Coverage

blib/lib/Test/Applify.pm
Criterion Covered Total %
statement 154 154 100.0
branch 26 30 86.6
condition 15 22 68.1
subroutine 28 28 100.0
pod 14 14 100.0
total 237 248 95.5


line stmt bran cond sub pod time code
1             package Test::Applify;
2              
3 5     5   271172 use strict;
  5         58  
  5         164  
4 5     5   23 use warnings;
  5         9  
  5         125  
5 5     5   21 use Carp 'croak';
  5         8  
  5         243  
6 5     5   23 use Exporter 'import';
  5         6  
  5         136  
7 5     5   23 use File::Spec ();
  5         21  
  5         62  
8 5     5   2533 use File::Temp ();
  5         79029  
  5         127  
9 5     5   40 use Test::More ();
  5         9  
  5         5384  
10              
11             our @EXPORT_OK = ('applify_ok', 'applify_subcommands_ok');
12             our $VERSION = '0.04';
13              
14             sub app {
15 200 100   200 1 3369 @_ == 2 and $_[0]->{app} = $_[1];
16 200         517 $_[0]->{app};
17             }
18              
19             sub applify_ok {
20 12     12 1 33577 my $code = shift;
21 12         29 my $args = shift;
22 12   100     56 my $desc = shift || 'applify_ok';
23 12         67 my $self = __PACKAGE__->new();
24             my $dir = File::Temp->newdir(TEMPLATE => 'test-applify-XXXXX',
25 12   33     627 DIR => $ENV{TMPDIR} || File::Spec->tmpdir);
26 12         5240 my $fh = File::Temp->new(DIR => $dir, SUFFIX => '.pl');
27 12         4655 my $file = $fh->filename;
28 12 50 50     149 ($fh->syswrite($code) // -1) == length $code
29             or croak qq{Can't write to file "$file": $!};
30              
31 12         483 my $app = $self->_build_code($file, @$args);
32              
33             # _build_code does this?
34 12         82 $self->_test('ok', ref($app), "$desc (compilation)");
35              
36 12         3441 return $app;
37             }
38              
39             sub applify_subcommands_ok {
40 2     2 1 431 my $code = shift;
41 2   50     40 my $desc = shift || 'applify_subcommands_ok';
42              
43 2         13 my $app = applify_ok($code, [], $desc);
44 2         1126 my @apps = $app;
45 2         15 my $self = __PACKAGE__->new();
46 2 50       8 my @cmds = sort keys %{$app->_script->{subcommands}||{}};
  2         8  
47 2         49 $self->_test('ok', scalar(@cmds), 'has subcommands');
48              
49 2         657 foreach my $cmd(@cmds) {
50 4         1450 my $cmd_app = applify_ok($code, [$cmd], "$desc - $cmd");
51 4 50       2178 push @apps, $cmd_app
52             if $self->_test('is', $cmd_app->_script->subcommand, $cmd, "$desc - create");
53             }
54 2         1407 $self->_test('is', scalar(@apps), scalar(@cmds) + 1, "$desc - created all");
55              
56 2         1550 return \@apps;
57             }
58              
59             sub app_script {
60 166     166 1 1952 shift->app->_script;
61             }
62              
63             sub app_instance {
64 11     11 1 6180 my ($self, $name) = (shift, @_);
65 11 50 66     70 $name = shift if ($name and $name =~ /^\w+/); # no change to specialisation now
66 11         29 local @ARGV = @_;
67 11         34 return ($self->app_script->app)[0];
68             }
69              
70             sub can_ok {
71 6     6 1 1683 my $self = shift;
72 6         24 $self->_test('can_ok', $self->app, @_);
73 6         1758 return $self;
74             }
75              
76             sub documentation_ok {
77 9     9 1 25 my $self = shift;
78 9         17 my $like = shift;
79 9         20 my $doc = $self->app_script->documentation;
80 9         70 $self->_test('ok', $doc, 'has documentation');
81 9 100       2044 $self->_test('like', $doc, $like, "documentation not like $like") if $like;
82 9         1428 return $self;
83             }
84              
85             sub extends_ok {
86 5     5 1 2110 my $self = shift;
87 5   100     25 $self->_test('isa_ok', $self->app, $_[0], $_[1] || 'application class');
88 5         3147 return $self;
89             }
90              
91             sub help_ok {
92 9     9 1 27 my $self = shift;
93 9   66     54 my $like = shift || qr/help/;
94 9         24 local *STDOUT;
95 9         19 local *STDERR;
96 9         15 my $stdout = '';
97 9         16 my $stderr = '';
98 4     4   33 open STDOUT, '>', \$stdout;
  4         9  
  4         41  
  9         190  
99 9         2532 open STDERR, '>', \$stderr;
100 9         31 $self->app_script->print_help();
101 9         9335 $self->_test('like', $stdout, qr/^usage:/mi, 'has usage string');
102 9         5006 $self->_test('like', $stdout, qr/\-\-help/, 'has help');
103 9         4161 $self->_test('is', $stderr, '', 'no stderr');
104 9         4277 $self->_test('like', $stdout, $like, "help not like $like");
105 9         4441 return $self;
106             }
107              
108             sub is_option {
109 20     20 1 68 my $self = shift;
110 20         46 my $option = $self->app_script->_attr_to_option(shift);
111             my @opt = (
112 50         212 grep { $_ eq $option }
113 50         345 map { $self->app_script->_attr_to_option($_->{name}) }
114 20         186 @{ $self->app_script->options }
  20         45  
115             );
116 20         93 $self->_test('ok', @opt == 1, "--$option is an option");
117 20         4826 return $self;
118             }
119              
120             sub is_required_option {
121 9     9 1 44 my $self = shift;
122 9         24 my $option = $self->app_script->_attr_to_option(shift);
123             my @opt = (
124 11         84 grep { $_ eq $option}
125 11         36 map { $self->app_script->_attr_to_option($_->{name}) }
126 22         73 grep { $_->{required} }
127 9         73 @{ $self->app_script->options }
  9         19  
128             );
129 9         38 $self->_test('ok', @opt == 1, "--$option is a required option");
130 9         2108 return $self;
131             }
132              
133             sub new {
134 40     40 1 26562 my $class = shift;
135 40   100     277 my $self = bless {}, ref $class || $class || __PACKAGE__;
136 40 100       233 return $self unless my $app = shift;
137 23 100       115 $self->app(ref $app ? $app : $self->_build_code($self->_filename($app)->_filename, @_));
138 19         72 return $self;
139             }
140              
141             sub subcommand_ok {
142 3     3 1 12 my $self = shift;
143 3         5 my $exp = shift;
144 3         8 my $obs = $self->app_script->subcommand;
145 3         28 $self->_test('is', $obs, $exp, 'subcommand is correct');
146 3         1293 return $self;
147             }
148              
149             sub version_ok {
150 9     9 1 30 my $self = shift;
151 9         15 my $exp = shift;
152 9         23 my $version = $self->app_script->version;
153 9         62 $self->_test('is', $version, $exp, 'version correct');
154 9         3939 return $self;
155             }
156              
157             sub _build_code {
158 23     23   77 my ($self, $name) = (shift, shift);
159 23         43 my ($app, %seen);
160 23         179 (my $ext = $name) =~ s/(\.pl)?$/.pl/i;
161 23         94 foreach my $file (grep { not $seen{lc $_}++ }
  42         184  
162 46 100       578 grep { -e $_ and -r _ } $name, $ext) {
163             {
164 22         42 eval {
  22         43  
165             package
166             Test::Applify::Container; # do not index
167 22         862 require Applify;
168 5     5   39 no strict 'refs';
  5         11  
  5         171  
169 5     5   24 no warnings 'redefine';
  5         10  
  5         1955  
170 22         7050 my $code = Applify->can('app');
171 22         47 my $tmp; ## copy - help recovering bad code.
172 22         98 local *{"Applify\::app"} = sub (&) {
173             ## do not run the app - even if user authored incorrect code.
174 19     19   3008 ($tmp) = $code->(@_); ## force array context
175 19         35304 return $tmp;
176 22         159 };
177 22         74 local @ARGV = @_; # support subcommand
178 22         5237 $app = do $file;
179              
180 22 100 66     831 if ($@) {
    100          
    100          
181             ## script didn't compile - syntax error, missing modules, etc...
182 2         18 die $@;
183             } elsif (! defined $tmp){
184 1         9 die "coding error in $file - app must be called\n";
185             } elsif (!(ref($app) && $app->can('_script') && ref($app->_script) eq 'Applify')) {
186 1         3 $app = $tmp;
187 1         54 warn "coding error in $file - app must be the last function called\n";
188             }
189             };
190 22         247 $self->_filename($file);
191             }
192             }
193 23 100       70 die $@ if $@;
194 20 100       59 die "Applify app not created ($!)\n" if not defined $app;
195 19         98 $self->_test('ok', ref($app), "do succeeded $name");
196 19         7615 $self->_test('can_ok', $app, '_script');
197 19         6857 $self->_test('isa_ok', $app->_script, 'Applify', 'type');
198 19         6848 return $app;
199             }
200              
201             sub _filename {
202 46 100   46   153 return $_[0]->{_filename} if @_ == 1;
203 33         107 $_[0]->{_filename} = $_[1];
204 33         77 return $_[0];
205             }
206              
207             sub _test {
208 177     177   591 my ($self, $name, @args) = @_;
209 177         338 local $Test::Builder::Level = $Test::Builder::Level + 2;
210 177         1355 return !!Test::More->can($name)->(@args);
211             }
212              
213             1;
214              
215             =head1 NAME
216              
217             Test::Applify - Testing Applify scripts
218              
219             =for html Build Status
220              
221             =for html Coverage Status
222              
223             =for html CPAN version
224              
225             =head1 SYNOPSIS
226              
227             use Test::More;
228             use Test::Applify;
229              
230             my $t = Test::Applify->new('./bin/app.pl');
231             my $help = $t->help_ok;
232             like $help, qr/basic/, 'help mentions basic mode';
233             $t->documentation_ok;
234             $t->version_ok('1.0.999');
235             $t->is_option($_) for qw{mode input};
236             $t->is_required_option($_) for qw{input};
237              
238             my $app1 = $t->app_instance(qw{-input strings.txt});
239             is $app1->mode, 'basic', 'basic mode is default';
240              
241             my $app2 = $t->app_instance(qw{-mode expert -input strings.txt});
242             is $app2->mode, 'expert', 'expert mode enabled';
243             is $app2->input, 'strings.txt', 'reading strings.txt';
244              
245             use Test::Applify 'applify_ok';
246             my $inlineapp = applify_ok("use Applify; app { print 'hello world!'; 0;};");
247             my $t = Test::Applify->new($inlineapp);
248              
249             =head1 DESCRIPTION
250              
251             L is a test agent to be used with L to test
252             L scripts. To run your tests use L.
253              
254             $ prove -l -v t
255              
256             Avoid testing the Applify code for correctness, it has its own test suite.
257             Instead, test for consistency of option behaviour, defaults and requiredness,
258             the script is compiled and that attributes and methods of the script behave with
259             different inputs.
260              
261             The aim is to remove repetition of multiple blocks to retrieve instances and
262             checks for success of C.
263              
264             my $app = do 'bin/app.pl'; ## check $@ and return value
265             {
266             local @ARGV = qw{...};
267             my $instance = $app->_script->app;
268             # more tests.
269             }
270              
271             =head1 EXPORTED FUNCTIONS
272              
273             =head2 applify_ok
274              
275             use Test::Applify 'applify_ok';
276             my $inlineapp = applify_ok("use Applify; app { print 'Hello world!'; 0;};");
277             my $t = Test::Applify->new($inlineapp);
278              
279             my $helloapp = applify_ok("use Applify; app { print 'Hello $_[1]!'; 0;};",
280             \@ARGV, 'hello app');
281             my $t = Test::Applify->new($helloapp);
282              
283             Utility function that wraps L and runs the same tests as L.
284              
285             This function must be imported.
286              
287             =head2 applify_subcommands_ok
288              
289             use Test::Applify 'applify_subcommands_ok';
290             my $subcmds = applify_subcommands_ok($code);
291             foreach my $app(@$subcmds){
292             Test::Applify->new($app)->help_ok
293             ->documentation_ok
294             ->version_ok('1')
295             ->is_required_option('global_option')
296             }
297              
298             Like L, but creates each of the subcommands and return in an array
299             reference.
300              
301             =head1 METHODS
302              
303             =head2 app
304              
305             my $t = Test::Applify->new('./bin/app.pl');
306             my $app = $t->app;
307              
308             Access to the application.
309              
310             B The removal of C<.> from C<@INC> requires relative paths to start with
311             C<./>. See link for further information L
312              
313             =head2 app_script
314              
315             my $script = $t->app_script;
316             isa_ok $script, 'Applify', 'the Applify object';
317              
318             Access to the Applify object.
319              
320             =head2 app_instance
321              
322             my $safe = $t->app_instance(qw{-opt value -mode safe});
323             my $risky = $t->app_instance();
324             is $risky->mode, 'expert', 'expert mode is the default';
325              
326             =head2 can_ok
327              
328             $t->can_ok(qw{mode input});
329              
330             Test for the presence of methods that the script has.
331              
332             =head2 documentation_ok
333              
334             $t->documentation_ok;
335              
336             Test the documentation.
337              
338             =head2 extends_ok
339              
340             $t->extends_ok('Parent::Class');
341             $t->extends_ok('Parent::Class', 'object name');
342              
343             Test the inheritance.
344              
345             =head2 help_ok
346              
347             my $help = $t->help_ok;
348              
349             Test and access the help for the script.
350              
351             =head2 is_option
352              
353             $t->is_option('mode');
354             $t->is_option($_) for qw{mode input};
355              
356             Test for the presence of an option with the supplied name
357              
358             =head2 is_required_option
359              
360             $t->is_required_option('input');
361              
362             Test that the option is a required option.
363              
364             =head2 new
365              
366             my $t = Test::Applify->new('script.pl');
367             # instance for the 'list' subcommand
368             my $t = Test::Applify->new('script.pl', 'list');
369              
370             Instantiate a new test instance for the supplied script name.
371              
372             =head2 subcommand_ok
373              
374             my $subcommand = $t->subcommand_ok('list');
375              
376             Test that the subcommand computed from C<@ARGV> matches the supplied subcommand.
377              
378             =head2 version_ok
379              
380             $t->version_ok('1.0.999');
381              
382             Test that the version matches the supplied version.
383              
384             =cut