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   284865 use strict;
  5         64  
  5         155  
4 5     5   26 use warnings;
  5         11  
  5         218  
5 5     5   31 use Carp 'croak';
  5         7  
  5         323  
6 5     5   31 use Exporter 'import';
  5         9  
  5         127  
7 5     5   27 use File::Spec ();
  5         54  
  5         93  
8 5     5   2751 use File::Temp ();
  5         88974  
  5         135  
9 5     5   39 use Test::More ();
  5         11  
  5         5444  
10              
11             our @EXPORT_OK = ('applify_ok', 'applify_subcommands_ok');
12             our $VERSION = '0.03';
13              
14             sub app {
15 184 100   184 1 3317 @_ == 2 and $_[0]->{app} = $_[1];
16 184         532 $_[0]->{app};
17             }
18              
19             sub applify_ok {
20 12     12 1 35450 my $code = shift;
21 12         29 my $args = shift;
22 12   100     48 my $desc = shift || 'applify_ok';
23 12         64 my $self = __PACKAGE__->new();
24             my $dir = File::Temp->newdir(TEMPLATE => 'test-applify-XXXXX',
25 12   33     635 DIR => $ENV{TMPDIR} || File::Spec->tmpdir);
26 12         4767 my $fh = File::Temp->new(DIR => $dir, SUFFIX => '.pl');
27 12         4475 my $file = $fh->filename;
28 12 50 50     144 ($fh->syswrite($code) // -1) == length $code
29             or croak qq{Can't write to file "$file": $!};
30              
31 12         460 my $app = $self->_build_code($file, @$args);
32              
33             # _build_code does this?
34 12         63 $self->_test('ok', ref($app), "$desc (compilation)");
35              
36 12         2989 return $app;
37             }
38              
39             sub applify_subcommands_ok {
40 2     2 1 344 my $code = shift;
41 2   50     11 my $desc = shift || 'applify_subcommands_ok';
42              
43 2         7 my $app = applify_ok($code, [], $desc);
44 2         823 my @apps = $app;
45 2         12 my $self = __PACKAGE__->new();
46 2 50       4 my @cmds = sort keys %{$app->_script->{subcommands}||{}};
  2         8  
47 2         34 $self->_test('ok', scalar(@cmds), 'has subcommands');
48              
49 2         473 foreach my $cmd(@cmds) {
50 4         973 my $cmd_app = applify_ok($code, [$cmd], "$desc - $cmd");
51 4 50       1572 push @apps, $cmd_app
52             if $self->_test('is', $cmd_app->_script->subcommand, $cmd, "$desc - create");
53             }
54 2         971 $self->_test('is', scalar(@apps), scalar(@cmds) + 1, "$desc - created all");
55              
56 2         851 return \@apps;
57             }
58              
59             sub app_script {
60 152     152 1 2429 shift->app->_script;
61             }
62              
63             sub app_instance {
64 11     11 1 5535 my ($self, $name) = (shift, @_);
65 11 50 66     74 $name = shift if ($name and $name =~ /^\w+/); # no change to specialisation now
66 11         33 local @ARGV = @_;
67 11         33 return $self->app_script->app;
68             }
69              
70             sub can_ok {
71 6     6 1 1977 my $self = shift;
72 6         27 $self->_test('can_ok', $self->app, @_);
73 6         2180 return $self;
74             }
75              
76             sub documentation_ok {
77 9     9 1 28 my $self = shift;
78 9         17 my $like = shift;
79 9         24 my $doc = $self->app_script->documentation;
80 9         85 $self->_test('ok', $doc, 'has documentation');
81 9 100       2423 $self->_test('like', $doc, $like, "documentation not like $like") if $like;
82 9         1300 return $self;
83             }
84              
85             sub extends_ok {
86 5     5 1 1247 my $self = shift;
87 5   100     12 $self->_test('isa_ok', $self->app, $_[0], $_[1] || 'application class');
88 5         1508 return $self;
89             }
90              
91             sub help_ok {
92 9     9 1 32 my $self = shift;
93 9   66     61 my $like = shift || qr/help/;
94 9         29 local *STDOUT;
95 9         18 local *STDERR;
96 9         20 my $stdout = '';
97 9         17 my $stderr = '';
98 4     4   34 open STDOUT, '>', \$stdout;
  4         7  
  4         42  
  9         206  
99 9         2426 open STDERR, '>', \$stderr;
100 9         37 $self->app_script->print_help();
101 9         10672 $self->_test('like', $stdout, qr/^usage:/mi, 'has usage string');
102 9         5515 $self->_test('like', $stdout, qr/\-\-help/, 'has help');
103 9         4902 $self->_test('is', $stderr, '', 'no stderr');
104 9         4687 $self->_test('like', $stdout, $like, "help not like $like");
105 9         4674 return $self;
106             }
107              
108             sub is_option {
109 17     17 1 72 my $self = shift;
110 17         47 my $option = $self->app_script->_attr_to_option(shift);
111             my @opt = (
112 42         222 grep { $_ eq $option }
113 42         322 map { $self->app_script->_attr_to_option($_->{name}) }
114 17         170 @{ $self->app_script->options }
  17         43  
115             );
116 17         79 $self->_test('ok', @opt == 1, "--$option is an option");
117 17         4700 return $self;
118             }
119              
120             sub is_required_option {
121 9     9 1 51 my $self = shift;
122 9         30 my $option = $self->app_script->_attr_to_option(shift);
123             my @opt = (
124 11         107 grep { $_ eq $option}
125 11         51 map { $self->app_script->_attr_to_option($_->{name}) }
126 22         89 grep { $_->{required} }
127 9         91 @{ $self->app_script->options }
  9         24  
128             );
129 9         50 $self->_test('ok', @opt == 1, "--$option is a required option");
130 9         2447 return $self;
131             }
132              
133             sub new {
134 38     38 1 28057 my $class = shift;
135 38   100     247 my $self = bless {}, ref $class || $class || __PACKAGE__;
136 38 100       204 return $self unless my $app = shift;
137 21 100       112 $self->app(ref $app ? $app : $self->_build_code($self->_filename($app)->_filename));
138 17         63 return $self;
139             }
140              
141             sub subcommand_ok {
142 3     3 1 19 my $self = shift;
143 3         7 my $exp = shift;
144 3         11 my $obs = $self->app_script->subcommand;
145 3         42 $self->_test('is', $obs, $exp, 'subcommand is correct');
146 3         1590 return $self;
147             }
148              
149             sub version_ok {
150 9     9 1 39 my $self = shift;
151 9         20 my $exp = shift;
152 9         24 my $version = $self->app_script->version;
153 9         83 $self->_test('is', $version, $exp, 'version correct');
154 9         4664 return $self;
155             }
156              
157             sub _build_code {
158 21     21   69 my ($self, $name) = (shift, shift);
159 21         38 my ($app, %seen);
160 21         168 (my $ext = $name) =~ s/(\.pl)?$/.pl/i;
161 21         67 foreach my $file (grep { not $seen{lc $_}++ }
  38         171  
162 42 100       612 grep { -e $_ and -r _ } $name, $ext) {
163             {
164 20         71 eval {
  20         40  
165             package
166             Test::Applify::Container; # do not index
167 20         976 require Applify;
168 5     5   36 no strict 'refs';
  5         8  
  5         169  
169 5     5   24 no warnings 'redefine';
  5         30  
  5         1764  
170 20         7568 my $code = Applify->can('app');
171 20         41 my $tmp; ## copy - help recovering bad code.
172 20         86 local *{"Applify\::app"} = sub (&) {
173             ## do not run the app - even if user authored incorrect code.
174 17     17   1986 ($tmp) = $code->(@_); ## force array context
175 17         33098 return $tmp;
176 20         121 };
177 20         68 local @ARGV = @_; # support subcommand
178 20         4764 $app = do $file;
179              
180 20 100 66     1043 if ($@) {
    100          
    100          
181             ## script didn't compile - syntax error, missing modules, etc...
182 2         21 die $@;
183             } elsif (! defined $tmp){
184 1         18 die "coding error in $file - app must be called\n";
185             } elsif (!(ref($app) && $app->can('_script') && ref($app->_script) eq 'Applify')) {
186 1         2 $app = $tmp;
187 1         48 warn "coding error in $file - app must be the last function called\n";
188             }
189             };
190 20         204 $self->_filename($file);
191             }
192             }
193 21 100       244 die $@ if $@;
194 18 100       70 die "Applify app not created ($!)\n" if not defined $app;
195 17         92 $self->_test('ok', ref($app), "do succeeded $name");
196 17         5982 $self->_test('can_ok', $app, '_script');
197 17         5240 $self->_test('isa_ok', $app->_script, 'Applify', 'type');
198 17         5881 return $app;
199             }
200              
201             sub _filename {
202 40 100   40   143 return $_[0]->{_filename} if @_ == 1;
203 29         98 $_[0]->{_filename} = $_[1];
204 29         77 return $_[0];
205             }
206              
207             sub _test {
208 168     168   519 my ($self, $name, @args) = @_;
209 168         319 local $Test::Builder::Level = $Test::Builder::Level + 2;
210 168         910 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              
368             Instantiate a new test instance for the supplied script name.
369              
370             =head2 subcommand_ok
371              
372             my $subcommand = $t->subcommand_ok('list');
373              
374             Test that the subcommand computed from C<@ARGV> matches the supplied subcommand.
375              
376             =head2 version_ok
377              
378             $t->version_ok('1.0.999');
379              
380             Test that the version matches the supplied version.
381              
382             =cut