File Coverage

blib/lib/App/Cmd/Tester.pm
Criterion Covered Total %
statement 48 48 100.0
branch 13 14 92.8
condition 9 11 81.8
subroutine 12 12 100.0
pod 1 2 50.0
total 83 87 95.4


line stmt bran cond sub pod time code
1 13     13   1667839 use strict;
  13         26  
  13         521  
2 13     13   123 use warnings;
  13         38  
  13         1428  
3             package App::Cmd::Tester 0.340;
4              
5             # ABSTRACT: for capturing the result of running an app
6              
7             #pod =head1 SYNOPSIS
8             #pod
9             #pod use Test::More tests => 4;
10             #pod use App::Cmd::Tester;
11             #pod
12             #pod use YourApp;
13             #pod
14             #pod my $result = test_app(YourApp => [ qw(command --opt value) ]);
15             #pod
16             #pod like($result->stdout, qr/expected output/, 'printed what we expected');
17             #pod
18             #pod is($result->stderr, '', 'nothing sent to sderr');
19             #pod
20             #pod is($result->error, undef, 'threw no exceptions');
21             #pod
22             #pod my $result = test_app(YourApp => [ qw(command --opt value --quiet) ]);
23             #pod
24             #pod is($result->output, '', 'absolutely no output with --quiet');
25             #pod
26             #pod =head1 DESCRIPTION
27             #pod
28             #pod One of the reasons that user-executed programs are so often poorly tested is
29             #pod that they are hard to test. App::Cmd::Tester is one of the tools App-Cmd
30             #pod provides to help make it easy to test App::Cmd-based programs.
31             #pod
32             #pod It provides one routine: test_app.
33             #pod
34             #pod =method test_app
35             #pod
36             #pod B: while C is a method, it is by default exported as a
37             #pod subroutine into the namespace that uses App::Cmd::Tester. In other words: you
38             #pod probably don't need to think about this as a method unless you want to subclass
39             #pod App::Cmd::Tester.
40             #pod
41             #pod my $result = test_app($app_class => \@argv_contents);
42             #pod
43             #pod This will locally set C<@ARGV> to simulate command line arguments, and will
44             #pod then call the C method on the given application class (or application).
45             #pod Output to the standard output and standard error filehandles will be captured.
46             #pod
47             #pod C<$result> is an App::Cmd::Tester::Result object, which has methods to access
48             #pod the following data:
49             #pod
50             #pod stdout - the output sent to stdout
51             #pod stderr - the output sent to stderr
52             #pod output - the combined output of stdout and stderr
53             #pod error - the exception thrown by running the application, or undef
54             #pod run_rv - the return value of the run method (generally irrelevant)
55             #pod exit_code - the numeric exit code that would've been issued (0 is 'okay')
56             #pod
57             #pod The output is captured using L, which I ensure that the
58             #pod ordering is preserved in the combined output, but I capture the output
59             #pod of external programs. You can reverse these tradeoffs by using
60             #pod L instead.
61             #pod
62             #pod =cut
63              
64 13     13   7596 use Sub::Exporter::Util qw(curry_method);
  13         263029  
  13         123  
65 13         77 use Sub::Exporter -setup => {
66             exports => { test_app => curry_method },
67             groups => { default => [ qw(test_app) ] },
68 13     13   3834 };
  13         41  
69              
70             our $TEST_IN_PROGRESS;
71             BEGIN {
72             *CORE::GLOBAL::exit = sub {
73 3     3   669863 my ($rc) = @_;
74 3 100       5 return CORE::exit($rc) unless $TEST_IN_PROGRESS;
75 1         9 App::Cmd::Tester::Exited->throw($rc);
76 13     13   15678 };
77             }
78              
79             #pod =for Pod::Coverage result_class
80             #pod
81             #pod =cut
82              
83 17     17 0 199 sub result_class { 'App::Cmd::Tester::Result' }
84              
85             sub test_app {
86 17     17 1 1658620 my ($class, $app, $argv) = @_;
87              
88 17         43 local $App::Cmd::_bad = 0;
89              
90 17 100 66     328 $app = $app->new unless ref($app) or $app->isa('App::Cmd::Simple');
91              
92 17         74 my $result = $class->_run_with_capture($app, $argv);
93              
94 17         578 my $error = $result->{error};
95              
96 17 100 100     75 my $exit_code = defined $error ? ((0+$!)||-1) : 0;
97              
98 17 100 100     92 if ($error and eval { $error->isa('App::Cmd::Tester::Exited') }) {
  3         35  
99 1         3 $exit_code = $$error;
100             }
101              
102 17 100 66     117 $exit_code =1 if $App::Cmd::_bad && ! $exit_code;
103              
104 17         153 $class->result_class->new({
105             app => $app,
106             exit_code => $exit_code,
107             %$result,
108             });
109             }
110              
111             sub _run_with_capture {
112 18     18   82 my ($class, $app, $argv) = @_;
113              
114 18         4553 require IO::TieCombine;
115 18         29469 my $hub = IO::TieCombine->new;
116              
117 18         375 my $stdout = tie local *STDOUT, $hub, 'stdout';
118 18         844 my $stderr = tie local *STDERR, $hub, 'stderr';
119              
120 18         528 my $run_rv;
121              
122 18         40 my $ok = eval {
123 18         37 local $TEST_IN_PROGRESS = 1;
124 18         80 local @ARGV = @$argv;
125 18         170 $run_rv = $app->run;
126 15         796 1;
127             };
128              
129 18 100       1171 my $error = $ok ? undef : $@;
130              
131             return {
132 18         102 stdout => $hub->slot_contents('stdout'),
133             stderr => $hub->slot_contents('stderr'),
134             output => $hub->combined_contents,
135             error => $error,
136             run_rv => $run_rv,
137             };
138             }
139              
140             {
141             package App::Cmd::Tester::Result 0.340;
142              
143             sub new {
144 17     17   45 my ($class, $arg) = @_;
145 17         191 bless $arg => $class;
146             }
147              
148             for my $attr (qw(app stdout stderr output error run_rv exit_code)) {
149             Sub::Install::install_sub({
150 20     20   4917 code => sub { $_[0]->{$attr} },
151             as => $attr,
152             });
153             }
154             }
155              
156             {
157             package App::Cmd::Tester::Exited 0.340;
158              
159             sub throw {
160 1     1   4 my ($class, $code) = @_;
161 1 50       6 $code = 0 unless defined $code;
162 1         3 my $self = (bless \$code => $class);
163 1         20 die $self;
164             }
165             }
166              
167             1;
168              
169             __END__