File Coverage

blib/lib/MooX/Cmd/Tester.pm
Criterion Covered Total %
statement 69 79 87.3
branch 26 36 72.2
condition 9 17 52.9
subroutine 16 19 84.2
pod 3 3 100.0
total 123 154 79.8


line stmt bran cond sub pod time code
1             package MooX::Cmd::Tester;
2              
3 4     4   109163 use strict;
  4         7  
  4         150  
4 4     4   18 use warnings;
  4         6  
  4         220  
5              
6             our $VERSION = "0.015";
7              
8             require Exporter;
9 4     4   20 use Test::More import => ['!pass'];
  4         7  
  4         26  
10 4     4   3831 use Package::Stash;
  4         26921  
  4         148  
11 4     4   2346 use Capture::Tiny qw(:all);
  4         133241  
  4         748  
12              
13 4     4   33 use parent qw(Test::Builder::Module Exporter);
  4         6  
  4         31  
14              
15             our @EXPORT = qw(test_cmd test_cmd_ok);
16             our @EXPORT_OK = qw(test_cmd test_cmd_ok);
17              
18             our $TEST_IN_PROGRESS;
19             my $CLASS = __PACKAGE__;
20              
21             BEGIN
22             {
23             *CORE::GLOBAL::exit = sub {
24 0 0   0   0 return CORE::exit(@_) unless $TEST_IN_PROGRESS;
25 0         0 MooX::Cmd::Tester::Exited->throw( $_[0] );
26 4     4   5183 };
27             }
28              
29 19     19 1 534 sub result_class { 'MooX::Cmd::Tester::Result' }
30              
31             sub test_cmd
32             {
33 19     19 1 4835 my ( $app, $argv ) = @_;
34              
35 19         80 my $result = _run_with_capture( $app, $argv );
36 19 100 50     194 my $exit_code = defined $result->{error} ? ( ( 0 + $! ) || -1 ) : 0;
37              
38             $result->{error}
39 0         0 and eval { $result->{error}->isa('MooX::Cmd::Tester::Exited') }
  3         80  
40 19 50 66     122 and $exit_code = ${ $result->{error} };
41              
42 19         103 result_class->new(
43             {
44             exit_code => $exit_code,
45             %$result,
46             }
47             );
48             }
49              
50             sub test_cmd_ok
51             {
52 16     16 1 9888 my $rv = test_cmd(@_);
53              
54 16         92 my $test_ident = $rv->app . " => [ " . join( " ", @{ $_[1] } ) . " ]";
  16         92  
55 16 50       79 ok( !$rv->error, "Everythink ok running cmd $test_ident" ) or diag( $rv->error );
56             # no error and cmd means, we're reasonable successful so far
57 16 100 33     10208 $rv
      33        
      66        
58             and !$rv->error
59             and $rv->cmd
60             and $rv->cmd->command_name
61             and ok( $rv->cmd->command_commands->{ $rv->cmd->command_name }, "found command at $test_ident" );
62              
63 16         5086 $rv;
64             }
65              
66             sub _capture_merged(&)
67             {
68 19     19   55 my $code = shift;
69 19         30 my ( $stdout, $stderr, $merged, $ok );
70 19 50       106 if ( $^O eq 'MSWin32' )
71             {
72 0     0   0 ( $stdout, $stderr, $ok ) = tee { $code->(); };
  0         0  
73 0         0 $merged = $stdout . $stderr;
74             }
75             else
76             {
77             ($merged) = tee_merged
78             {
79 19     19   361508 ( $stdout, $stderr, $ok ) = tee { $code->() };
  19         349461  
80 19         843 };
81             }
82 19         94878 ( $stdout, $stderr, $merged, $ok );
83             }
84              
85             sub _run_with_capture
86             {
87 19     19   44 my ( $app, $argv ) = @_;
88              
89 19         38 my ( $execute_rv, $cmd, $cmd_name );
90              
91             my ( $stdout, $stderr, $merged, $ok ) = _capture_merged
92             {
93 19     19   168 eval {
94 19         209 local $TEST_IN_PROGRESS = 1;
95 19         153 local @ARGV = @$argv;
96              
97 19         806 my $tb = $CLASS->builder();
98              
99 19 100       1526 $cmd = ref $app ? $app : $app->new_with_cmd;
100 16 100       79 ref $app and $app = ref $app;
101 16         117 my $test_ident = "$app => [ " . join( " ", @$argv ) . " ]";
102 16         372 ok( $cmd->isa($app), "got a '$app' from new_with_cmd" );
103 17         242 @$argv
104             and defined( $cmd_name = $cmd->command_name )
105 16 100 66     12552 and ok( ( grep { $_ =~ m/$cmd_name/ } @$argv ), "proper cmd name from $test_ident" );
106 16         4222 ok( scalar @{ $cmd->command_chain } <= 1 + scalar @$argv, "\$#argv vs. command chain length testing $test_ident" );
  16         195  
107 16 100       6462 @$argv and ok( $cmd->command_chain_end == $cmd->command_chain->[-1], "command_chain_end ok" );
108              
109 16 100       4514 unless ( $execute_rv = $cmd->execute_return )
110             {
111 9         20 my ( $command_execute_from_new, $command_execute_method_name );
112 9         67 my $cce = $cmd->can("command_chain_end");
113 9 50       76 $cce and $cce = $cce->($cmd);
114 9 50       208 $cce and $command_execute_from_new = $cce->can("command_execute_from_new");
115 9 100       46 $command_execute_from_new and $command_execute_from_new = $command_execute_from_new->($cce);
116 9 50       349 $command_execute_from_new or $command_execute_method_name = $cce->can('command_execute_method_name');
117 9 100       48 $command_execute_method_name
118             and $execute_rv = [ $cce->can( $command_execute_method_name->($cce) )->($cce) ];
119             }
120 16         1086 1;
121             };
122 19         275 };
123              
124 19 100       778 my $error = $ok ? undef : $@;
125              
126             return {
127 19         403 app => $app,
128             cmd => $cmd,
129             stdout => $stdout,
130             stderr => $stderr,
131             output => $merged,
132             error => $error,
133             execute_rv => $execute_rv,
134             };
135             }
136              
137             {
138             package # no-index
139             MooX::Cmd::Tester::Result;
140              
141             sub new
142             {
143 19     19   55 my ( $class, $arg ) = @_;
144 19         432 bless $arg => $class;
145             }
146             }
147              
148             my $res = Package::Stash->new("MooX::Cmd::Tester::Result");
149             for my $attr (qw(app cmd stdout stderr output error execute_rv exit_code))
150             {
151 125     125   3766 $res->add_symbol( '&' . $attr, sub { $_[0]->{$attr} } );
152             }
153              
154             {
155             package # no-index
156             MooX::Cmd::Tester::Exited;
157              
158             sub throw
159             {
160 0     0     my ( $class, $code ) = @_;
161 0 0         defined $code or $code = 0;
162 0           my $self = ( bless \$code => $class );
163 0           die $self;
164             }
165             }
166              
167             =head1 NAME
168              
169             MooX::Cmd::Tester - MooX cli app commands tester
170              
171             =head1 SYNOPSIS
172              
173             use MooX::Cmd::Tester;
174             use Test::More;
175              
176             use MyFoo;
177              
178             # basic tests as instance check, initialization check etc. is done there
179             my $rv = test_cmd( MyFoo => [ command(s) option(s) ] );
180              
181             like( $rv->stdout, qr/operation successful/, "Command performed" );
182             like( $rv->stderr, qr/patient dead/, "Deal with expected command error" );
183              
184             is_deeply( $rv->execute_rv, \@expected_return_values, "got what I deserve?" );
185              
186             cmp_ok( $rv->exit_code, "==", 0, "Command successful" );
187              
188             =head1 DESCRIPTION
189              
190             The test coverage of most CLI apps is somewhere between poor and wretched.
191             With the same approach as L comes MooX::Cmd::Tester to
192             ease writing tests for CLI apps.
193              
194             =head1 FUNCTIONS
195              
196             =head2 test_cmd
197              
198             my $rv = test_cmd( MyApp => \@argv );
199              
200             test_cmd invokes the app with given argv as if would be invoked from
201             command line and captures the output, the return values and exit code.
202              
203             Some minor tests are done to prove whether class matches, execute succeeds,
204             command_name and command_chain are not totally scrambled.
205              
206             It returns an object with following attributes/accessors:
207              
208             =head3 app
209              
210             Name of package of App
211              
212             =head3 cmd
213              
214             Name of executed (1st level) command
215              
216             =head3 stdout
217              
218             Content of stdout
219              
220             =head3 stderr
221              
222             Content of stderr
223              
224             =head3 output
225              
226             Content of merged stdout and stderr
227              
228             =head3 error
229              
230             the exception thrown by running the application (if any)
231              
232             =head3 execute_rv
233              
234             return values from execute
235              
236             =head3 exit_code
237              
238             0 on success, $! when error occurred and $! available, -1 otherwise
239              
240             =head2 test_cmd_ok
241              
242             my $rv = test_cmd_ok( MyApp => \@argv );
243              
244             Runs C and expects it being successful - command_name must be in
245             command_commands, etc.
246              
247             Returns the same object C returns.
248              
249             If an error occurred, no additional test is done (behavior as C).
250              
251             =head2 result_class
252              
253             Builder for result class to use. Returns C by
254             default.
255              
256             =head1 ACKNOWLEDGEMENTS
257              
258             MooX::Cmd::Tester is I by L from Ricardo Signes.
259             In fact, I reused the entire design and adopt it to the requirements of
260             MooX::Cmd.
261              
262             =head1 LICENSE AND COPYRIGHT
263              
264             Copyright 2013-2015 Jens Rehsack.
265              
266             This program is free software; you can redistribute it and/or modify it
267             under the terms of either: the GNU General Public License as published
268             by the Free Software Foundation; or the Artistic License.
269              
270             See L for more information.
271              
272             =cut
273              
274             1;