File Coverage

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


line stmt bran cond sub pod time code
1 10     10   501025 use strict;
  10         82  
  10         276  
2 10     10   46 use warnings;
  10         14  
  10         583  
3             package App::Cmd::Tester 0.334;
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 10     10   4516 use Sub::Exporter::Util qw(curry_method);
  10         142704  
  10         68  
65 10         29 use Sub::Exporter -setup => {
66             exports => { test_app => curry_method },
67             groups => { default => [ qw(test_app) ] },
68 10     10   2104 };
  10         20  
69              
70             our $TEST_IN_PROGRESS;
71             BEGIN {
72             *CORE::GLOBAL::exit = sub {
73 1     1   9 my ($rc) = @_;
74 1 50       3 return CORE::exit($rc) unless $TEST_IN_PROGRESS;
75 1         8 App::Cmd::Tester::Exited->throw($rc);
76 10     10   8883 };
77             }
78              
79             #pod =for Pod::Coverage result_class
80             #pod
81             #pod =cut
82              
83 14     14 0 162 sub result_class { 'App::Cmd::Tester::Result' }
84              
85             sub test_app {
86 14     14 1 2472 my ($class, $app, $argv) = @_;
87              
88 14         36 local $App::Cmd::_bad = 0;
89              
90 14 100 66     208 $app = $app->new unless ref($app) or $app->isa('App::Cmd::Simple');
91              
92 14         49 my $result = $class->_run_with_capture($app, $argv);
93              
94 14         422 my $error = $result->{error};
95              
96 14 100 100     88 my $exit_code = defined $error ? ((0+$!)||-1) : 0;
97              
98 14 100 100     65 if ($error and eval { $error->isa('App::Cmd::Tester::Exited') }) {
  3         33  
99 1         2 $exit_code = $$error;
100             }
101              
102 14 100 66     63 $exit_code =1 if $App::Cmd::_bad && ! $exit_code;
103              
104 14         123 $class->result_class->new({
105             app => $app,
106             exit_code => $exit_code,
107             %$result,
108             });
109             }
110              
111             sub _run_with_capture {
112 15     15   58 my ($class, $app, $argv) = @_;
113              
114 15         3027 require IO::TieCombine;
115 15         19689 my $hub = IO::TieCombine->new;
116              
117 15         220 my $stdout = tie local *STDOUT, $hub, 'stdout';
118 15         547 my $stderr = tie local *STDERR, $hub, 'stderr';
119              
120 15         370 my $run_rv;
121              
122 15         28 my $ok = eval {
123 15         28 local $TEST_IN_PROGRESS = 1;
124 15         49 local @ARGV = @$argv;
125 15         72 $run_rv = $app->run;
126 12         606 1;
127             };
128              
129 15 100       1021 my $error = $ok ? undef : $@;
130              
131             return {
132 15         67 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.334;
142              
143             sub new {
144 14     14   57 my ($class, $arg) = @_;
145 14         131 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 19     19   2827 code => sub { $_[0]->{$attr} },
151             as => $attr,
152             });
153             }
154             }
155              
156             {
157             package App::Cmd::Tester::Exited 0.334;
158              
159             sub throw {
160 1     1   3 my ($class, $code) = @_;
161 1 50       3 $code = 0 unless defined $code;
162 1         3 my $self = (bless \$code => $class);
163 1         25 die $self;
164             }
165             }
166              
167             1;
168              
169             __END__