File Coverage

blib/lib/Test/Applify.pm
Criterion Covered Total %
statement 177 177 100.0
branch 26 30 86.6
condition 21 28 75.0
subroutine 32 32 100.0
pod 16 16 100.0
total 272 283 96.1


line stmt bran cond sub pod time code
1             package Test::Applify;
2              
3 6     6   306996 use strict;
  6         44  
  6         129  
4 6     6   24 use warnings;
  6         7  
  6         124  
5 6     6   21 use Carp 'croak';
  6         9  
  6         245  
6 6     6   25 use Exporter 'import';
  6         8  
  6         133  
7 6     6   35 use File::Spec ();
  6         12  
  6         83  
8 6     6   3429 use File::Temp ();
  6         96053  
  6         112  
9 6     6   2167 use IO::String ();
  6         11688  
  6         101  
10 6     6   29 use Test::More ();
  6         8  
  6         7405  
11              
12             our @EXPORT_OK = ('applify_ok', 'applify_subcommands_ok');
13             our $VERSION = '0.06';
14             our $exited = 0;
15              
16             sub app {
17 224 100   224 1 2935 @_ == 2 and $_[0]->{app} = $_[1];
18 224         430 $_[0]->{app};
19             }
20              
21             sub applify_ok {
22 20     20 1 41164 my $code = shift;
23 20         39 my $args = shift;
24 20   100     76 my $desc = shift || 'applify_ok';
25 20         85 my $self = __PACKAGE__->new();
26             my $dir = File::Temp->newdir(TEMPLATE => 'test-applify-XXXXX',
27 20   33     649 DIR => $ENV{TMPDIR} || File::Spec->tmpdir);
28 20         5986 my $fh = File::Temp->new(DIR => $dir, SUFFIX => '.pl');
29 20         6141 my $file = $fh->filename;
30 20 50 50     146 ($fh->syswrite($code) // -1) == length $code
31             or croak qq{Can't write to file "$file": $!};
32              
33 20         658 my $app = $self->_build_code($file, @$args);
34              
35             # _build_code does this?
36 20         71 $self->_test('ok', ref($app), "$desc (compilation)");
37              
38 20         4413 return $app;
39             }
40              
41             sub applify_subcommands_ok {
42 2     2 1 392 my $code = shift;
43 2   50     11 my $desc = shift || 'applify_subcommands_ok';
44              
45 2         5 my $app = applify_ok($code, [], $desc);
46 2         802 my @apps = $app;
47 2         11 my $self = __PACKAGE__->new();
48 2 50       3 my @cmds = sort keys %{$app->_script->{subcommands}||{}};
  2         7  
49 2         30 $self->_test('ok', scalar(@cmds), 'has subcommands');
50              
51 2         450 foreach my $cmd(@cmds) {
52 4         910 my $cmd_app = applify_ok($code, [$cmd], "$desc - $cmd");
53 4 50       1603 push @apps, $cmd_app
54             if $self->_test('is', $cmd_app->_script->subcommand, $cmd, "$desc - create");
55             }
56 2         874 $self->_test('is', scalar(@apps), scalar(@cmds) + 1, "$desc - created all");
57              
58 2         794 return \@apps;
59             }
60              
61             sub app_script {
62 181     181 1 1752 shift->app->_script;
63             }
64              
65             sub app_instance {
66 14     14 1 7937 my ($self, $name) = (shift, @_);
67 14 50 66     80 $name = shift if ($name and $name =~ /^\w+/); # no change to specialisation now
68 14         31 local @ARGV = @_;
69 14         28 return ($self->app_script->app)[0];
70             }
71              
72             sub can_ok {
73 6     6 1 1529 my $self = shift;
74 6         16 $self->_test('can_ok', $self->app, @_);
75 6         1619 return $self;
76             }
77              
78             sub documentation_ok {
79 9     9 1 21 my $self = shift;
80 9         13 my $like = shift;
81 9         18 my $doc = $self->app_script->documentation;
82 9         57 $self->_test('ok', $doc, 'has documentation');
83 9 100       1912 $self->_test('like', $doc, $like, "documentation not like $like") if $like;
84 9         1290 return $self;
85             }
86              
87             sub extends_ok {
88 5     5 1 1112 my $self = shift;
89 5   100     20 $self->_test('isa_ok', $self->app, $_[0], $_[1] || 'application class');
90 5         1360 return $self;
91             }
92              
93             sub help_ok {
94 9     9 1 20 my $self = shift;
95 9   66     53 my $like = shift || qr/help/;
96 9         26 local (*STDOUT, *STDERR);
97 9         20 my ($stdout, $stderr) = ('', '');
98 4     4   31 open STDOUT, '>', \$stdout;
  4         7  
  4         20  
  9         137  
99 9         2778 open STDERR, '>', \$stderr;
100 9         28 $self->app_script->print_help();
101 9         9500 $self->_test('like', $stdout, qr/^usage:/mi, 'has usage string');
102 9         4146 $self->_test('like', $stdout, qr/\-\-help/, 'has help');
103 9         3707 $self->_test('is', $stderr, '', 'no stderr');
104 9         3639 $self->_test('like', $stdout, $like, "help not like $like");
105 9         3815 return $self;
106             }
107              
108             sub is_option {
109 20     20 1 58 my $self = shift;
110 20         35 my $option = $self->app_script->_attr_to_option(shift);
111             my @opt = (
112 50         182 grep { $_ eq $option }
113 50         264 map { $self->app_script->_attr_to_option($_->{name}) }
114 20         138 @{ $self->app_script->options }
  20         34  
115             );
116 20         68 $self->_test('ok', @opt == 1, "--$option is an option");
117 20         4293 return $self;
118             }
119              
120             sub is_required_option {
121 9     9 1 37 my $self = shift;
122 9         17 my $option = $self->app_script->_attr_to_option(shift);
123             my @opt = (
124 11         81 grep { $_ eq $option}
125 11         32 map { $self->app_script->_attr_to_option($_->{name}) }
126 22         62 grep { $_->{required} }
127 9         75 @{ $self->app_script->options }
  9         15  
128             );
129 9         32 $self->_test('ok', @opt == 1, "--$option is a required option");
130 9         1901 return $self;
131             }
132              
133             sub new {
134 57     57 1 27934 my $class = shift;
135 57   100     251 my $self = bless {}, ref $class || $class || __PACKAGE__;
136 57 100       189 return $self unless my $app = shift;
137 32 100       117 $self->app(ref $app ? $app : $self->_build_code($self->_filename($app)->_filename, @_));
138 28         65 return $self;
139             }
140              
141             sub run_instance_ok {
142 16     16 1 2535 my $self = shift;
143 16         20 my $instance = shift;
144              
145 16         33 $self->_test('ok', $instance, 'pass run_instance_ok return from app_instance');
146 16         3739 $self->_test('like', ref($instance), qr/^Applify\:\:/, 'application class');
147 16         4451 $self->_test('can_ok', $instance, '_script');
148              
149 16         4073 my ($stdout, $stderr) = ('', '');
150             #local (*STDOUT, *STDERR);
151             #open STDOUT, '>', \$stdout;
152             #open STDERR, '>', \$stderr;
153             # different approach to help_ok
154 16         73 tie *STDOUT, 'IO::String', \$stdout;
155 16         483 tie *STDERR, 'IO::String', \$stderr;
156              
157 16         345 $exited = 0;
158 16         19 my $retval = eval { $instance->run(@_) };
  16         35  
159              
160 16         60 untie *STDOUT;
161 16         87 untie *STDERR;
162              
163 16   100     193 return ($retval, $stdout || $@, $@ || $stderr, $exited);
      100        
164             }
165              
166             sub run_ok {
167 12     12 1 8723 my ($self, $app, @extra) = (shift);
168             {
169 12         17 local @ARGV = @_;
  12         23  
170 12         23 $app = ($self->app_script->app)[0];
171 12         2757 @extra = @ARGV;
172             }
173 12         29 $self->run_instance_ok($app, @extra);
174             }
175              
176              
177             sub subcommand_ok {
178 3     3 1 10 my $self = shift;
179 3         4 my $exp = shift;
180 3         6 my $obs = $self->app_script->subcommand;
181 3         21 $self->_test('is', $obs, $exp, 'subcommand is correct');
182 3         1128 return $self;
183             }
184              
185             sub version_ok {
186 9     9 1 25 my $self = shift;
187 9         12 my $exp = shift;
188 9         19 my $version = $self->app_script->version;
189 9         54 $self->_test('is', $version, $exp, 'version correct');
190 9         3703 return $self;
191             }
192              
193             sub _build_code {
194 31     31   64 my ($self, $name) = (shift, shift);
195 31         35 my ($app, %seen);
196 31         189 (my $ext = $name) =~ s/(\.pl)?$/.pl/i;
197 31         67 foreach my $file (grep { not $seen{lc $_}++ }
  58         214  
198 62 100       1032 grep { -e $_ and -r _ } $name, $ext) {
199             {
200             ## no critic(Modules::ProhibitMultiplePackages)
201 30         38 eval {
  30         42  
202             package
203             Test::Applify::Container; # do not index
204 30         1248 require Applify;
205 6     6   40 no strict 'refs';
  6         8  
  6         176  
206 6     6   24 no warnings 'redefine';
  6         8  
  6         2266  
207 30         10327 my $code = Applify->can('app');
208 30         40 my $tmp; ## copy - help recovering bad code.
209             # like a BEGIN block https://stackoverflow.com/a/25376064
210             # "bakes" it into do().
211             *CORE::GLOBAL::exit = sub (;$) {
212 1     1   2 $exited = 1;
213 30         189 };
214 30         94 local *{"Applify\::app"} = sub (&) {
215             ## do not run the app - even if user authored incorrect code.
216 27     27   2184 ($tmp) = $code->(@_); ## force array context
217 27         41772 return $tmp;
218 30         91 };
219 30         68 local @ARGV = @_; # support subcommand
220 30         6717 $app = do $file;
221             # no warnings 'once' and https://stackoverflow.com/a/25376064
222             #*CORE::GLOBAL::exit = *CORE::exit;
223              
224 30 100 66     857 if ($@) {
    100          
    100          
225             ## script didn't compile - syntax error, missing modules, etc...
226 2         15 die $@;
227             } elsif (! defined $tmp){
228 1         9 die "coding error in $file - app must be called\n";
229             } elsif (!(ref($app) && $app->can('_script') && ref($app->_script) eq 'Applify')) {
230 1         3 $app = $tmp;
231 1         41 warn "coding error in $file - app must be the last function called\n";
232             }
233             };
234 30         224 $self->_filename($file);
235             }
236             }
237 31 100       62 die $@ if $@;
238 28 100       69 die "Applify app not created ($!)\n" if not defined $app;
239 27         91 $self->_test('ok', ref($app), "do succeeded $name");
240 27         7247 $self->_test('can_ok', $app, '_script');
241 27         7158 $self->_test('isa_ok', $app->_script, 'Applify', 'type');
242 27         7429 return $app;
243             }
244              
245             sub _filename {
246 54 100   54   130 return $_[0]->{_filename} if @_ == 1;
247 41         96 $_[0]->{_filename} = $_[1];
248 41         67 return $_[0];
249             }
250              
251             sub _test {
252 257     257   598 my ($self, $name, @args) = @_;
253 257         379 local $Test::Builder::Level = $Test::Builder::Level + 2;
254 257         1041 return !!Test::More->can($name)->(@args);
255             }
256              
257             1;
258              
259             =head1 NAME
260              
261             Test::Applify - Testing Applify scripts
262              
263             =begin html
264              
265            
266            
267            
268             alt="Build Status">
269            
270            
271            
272            
273             alt="Coverage Status">
274            
275            
276            
277            
278             alt="Kritika Analysis Status" />
279            
280            
281            
282             CPAN version
283            
284              
285             =end html
286              
287             =head1 SYNOPSIS
288              
289             use Test::More;
290             use Test::Applify 'applify_ok';
291              
292             my $t = Test::Applify->new('./bin/app.pl');
293             $t->help_ok(qr/basic/)
294             ->documentation_ok
295             ->version_ok('1.0.999');
296             $t->is_option($_) for qw{mode input};
297             $t->is_required_option($_) for qw{input};
298              
299             my $app1 = $t->app_instance(qw{-input strings.txt});
300             is $app1->mode, 'basic', 'basic mode is default';
301              
302             my $app2 = $t->app_instance(qw{-mode expert -input strings.txt});
303             is $app2->mode, 'expert', 'expert mode enabled';
304             is $app2->input, 'strings.txt', 'reading strings.txt';
305             $t->run_instance_ok($app2);
306              
307             my $inlineapp = applify_ok("use Applify; app { print 'hello world!'; 0;};");
308             $t = Test::Applify->new($inlineapp);
309              
310             =head1 DESCRIPTION
311              
312             L is a test agent to be used with L to test
313             L scripts. To run your tests use L.
314              
315             $ prove -l -v t
316              
317             Avoid testing the Applify code for correctness, it has its own test suite.
318             Instead, test for consistency of option behaviour, defaults and requiredness,
319             the script is compiled and that attributes and methods of the script behave with
320             different inputs.
321              
322             The aim is to remove repetition of multiple blocks to retrieve instances and
323             checks for success of C.
324              
325             my $app = do 'bin/app.pl'; ## check $@ and return value
326             {
327             local @ARGV = qw{...};
328             my $instance = $app->_script->app;
329             # more tests.
330             }
331              
332             =head1 EXPORTED FUNCTIONS
333              
334             =head2 applify_ok
335              
336             use Test::Applify 'applify_ok';
337             my $inlineapp = applify_ok("use Applify; app { print 'Hello world!'; 0;};");
338             my $t = Test::Applify->new($inlineapp);
339              
340             my $helloapp = applify_ok("use Applify; app { print 'Hello $_[1]!'; 0;};",
341             \@ARGV, 'hello app');
342             my $t = Test::Applify->new($helloapp);
343              
344             Utility function that wraps L and runs the same tests as L.
345              
346             This function must be imported.
347              
348             =head2 applify_subcommands_ok
349              
350             use Test::Applify 'applify_subcommands_ok';
351             my $subcmds = applify_subcommands_ok($code);
352             foreach my $app(@$subcmds){
353             Test::Applify->new($app)->help_ok
354             ->documentation_ok
355             ->version_ok('1')
356             ->is_required_option('global_option')
357             }
358              
359             Like L, but creates each of the subcommands and return in an array
360             reference.
361              
362             =head1 METHODS
363              
364             =head2 app
365              
366             my $t = Test::Applify->new('./bin/app.pl');
367             my $app = $t->app;
368              
369             Access to the application.
370              
371             B The removal of C<.> from C<@INC> requires relative paths to start with
372             C<./>. See link for further information L
373              
374             =head2 app_script
375              
376             my $script = $t->app_script;
377             isa_ok $script, 'Applify', 'the Applify object';
378              
379             Access to the Applify object.
380              
381             =head2 app_instance
382              
383             my $safe = $t->app_instance(qw{-opt value -mode safe});
384             is $safe->mode, 'safe', 'will run in safe mode';
385             my $risky = $t->app_instance();
386             is $risky->mode, 'expert', 'expert mode is the default';
387              
388             Create an instance of the application class, which will be the contents of the
389             L script created. The array passed will be turned into C<@ARGV> as if
390             those options had been passed on the command line.
391              
392             =head2 can_ok
393              
394             $t->can_ok(qw{mode input});
395              
396             Test for the presence of methods that the script has.
397              
398             =head2 documentation_ok
399              
400             $t->documentation_ok;
401              
402             Test the documentation.
403              
404             =head2 extends_ok
405              
406             $t->extends_ok('Parent::Class');
407             $t->extends_ok('Parent::Class', 'object name');
408              
409             Test the inheritance.
410              
411             =head2 help_ok
412              
413             my $help = $t->help_ok;
414              
415             Test and access the help for the script.
416              
417             =head2 is_option
418              
419             $t->is_option('mode');
420             $t->is_option($_) for qw{mode input};
421              
422             Test for the presence of an option with the supplied name
423              
424             =head2 is_required_option
425              
426             $t->is_required_option('input');
427              
428             Test that the option is a required option.
429              
430             =head2 new
431              
432             my $t = Test::Applify->new('./script.pl');
433             # instance for the 'list' subcommand
434             my $t = Test::Applify->new('./script.pl', 'list');
435              
436             Instantiate a new test instance for the supplied script name.
437              
438             =head2 run_instance_ok
439              
440             my $t = Test::Applify->new('./script.pl');
441             my $app = $t->app_instance(qw{-mode expert});
442             is $app->mode, 'expert', 'everyone is an expert';
443             my ($retval, $stdout, $stderr, $exited) = $t->run_instance_ok($app);
444              
445             Call C on the L application class instance
446             (execute the C block). Returns a list of scalars which are:
447              
448             =over 4
449              
450             =item retval - the return value of the C block
451              
452             This is C when L called
453              
454             =item stdout - the content that was printed to C during the run
455              
456             =item stderr - L or the content that was printed to C during the run
457              
458             =item exit - whether the code exited
459              
460             =back
461              
462             =head2 run_ok
463              
464             my $t = Test::Applify->new('./script.pl');
465             my ($exit, $stdout, $stderr, $retval) = $t->run_ok(qw{-mode expert});
466              
467             Same as L, but less code.
468              
469             =head2 subcommand_ok
470              
471             my $subcommand = $t->subcommand_ok('list');
472              
473             Test that the subcommand computed from C<@ARGV> matches the supplied subcommand.
474              
475             =head2 version_ok
476              
477             $t->version_ok('1.0.999');
478              
479             Test that the version matches the supplied version.
480              
481             =cut