File Coverage

blib/lib/MooX/Cmd/Tester.pm
Criterion Covered Total %
statement 69 79 87.3
branch 28 38 73.6
condition 9 17 52.9
subroutine 16 19 84.2
pod 3 3 100.0
total 125 156 80.1


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