use strict; use warnings; package App::Cmd::Tester 0.336; # ABSTRACT: for capturing the result of running an app #pod =head1 SYNOPSIS #pod #pod use Test::More tests => 4; #pod use App::Cmd::Tester; #pod #pod use YourApp; #pod #pod my $result = test_app(YourApp => [ qw(command --opt value) ]); #pod #pod like($result->stdout, qr/expected output/, 'printed what we expected'); #pod #pod is($result->stderr, '', 'nothing sent to sderr'); #pod #pod is($result->error, undef, 'threw no exceptions'); #pod #pod my $result = test_app(YourApp => [ qw(command --opt value --quiet) ]); #pod #pod is($result->output, '', 'absolutely no output with --quiet'); #pod #pod =head1 DESCRIPTION #pod #pod One of the reasons that user-executed programs are so often poorly tested is #pod that they are hard to test. App::Cmd::Tester is one of the tools App-Cmd #pod provides to help make it easy to test App::Cmd-based programs. #pod #pod It provides one routine: test_app. #pod #pod =method test_app #pod #pod B: while C is a method, it is by default exported as a #pod subroutine into the namespace that uses App::Cmd::Tester. In other words: you #pod probably don't need to think about this as a method unless you want to subclass #pod App::Cmd::Tester. #pod #pod my $result = test_app($app_class => \@argv_contents); #pod #pod This will locally set C<@ARGV> to simulate command line arguments, and will #pod then call the C method on the given application class (or application). #pod Output to the standard output and standard error filehandles will be captured. #pod #pod C<$result> is an App::Cmd::Tester::Result object, which has methods to access #pod the following data: #pod #pod stdout - the output sent to stdout #pod stderr - the output sent to stderr #pod output - the combined output of stdout and stderr #pod error - the exception thrown by running the application, or undef #pod run_rv - the return value of the run method (generally irrelevant) #pod exit_code - the numeric exit code that would've been issued (0 is 'okay') #pod #pod The output is captured using L, which I ensure that the #pod ordering is preserved in the combined output, but I capture the output #pod of external programs. You can reverse these tradeoffs by using #pod L instead. #pod #pod =cut use Sub::Exporter::Util qw(curry_method); use Sub::Exporter -setup => { exports => { test_app => curry_method }, groups => { default => [ qw(test_app) ] }, }; our $TEST_IN_PROGRESS; BEGIN { *CORE::GLOBAL::exit = sub { my ($rc) = @_; return CORE::exit($rc) unless $TEST_IN_PROGRESS; App::Cmd::Tester::Exited->throw($rc); }; } #pod =for Pod::Coverage result_class #pod #pod =cut sub result_class { 'App::Cmd::Tester::Result' } sub test_app { my ($class, $app, $argv) = @_; local $App::Cmd::_bad = 0; $app = $app->new unless ref($app) or $app->isa('App::Cmd::Simple'); my $result = $class->_run_with_capture($app, $argv); my $error = $result->{error}; my $exit_code = defined $error ? ((0+$!)||-1) : 0; if ($error and eval { $error->isa('App::Cmd::Tester::Exited') }) { $exit_code = $$error; } $exit_code =1 if $App::Cmd::_bad && ! $exit_code; $class->result_class->new({ app => $app, exit_code => $exit_code, %$result, }); } sub _run_with_capture { my ($class, $app, $argv) = @_; require IO::TieCombine; my $hub = IO::TieCombine->new; my $stdout = tie local *STDOUT, $hub, 'stdout'; my $stderr = tie local *STDERR, $hub, 'stderr'; my $run_rv; my $ok = eval { local $TEST_IN_PROGRESS = 1; local @ARGV = @$argv; $run_rv = $app->run; 1; }; my $error = $ok ? undef : $@; return { stdout => $hub->slot_contents('stdout'), stderr => $hub->slot_contents('stderr'), output => $hub->combined_contents, error => $error, run_rv => $run_rv, }; } { package App::Cmd::Tester::Result 0.336; sub new { my ($class, $arg) = @_; bless $arg => $class; } for my $attr (qw(app stdout stderr output error run_rv exit_code)) { Sub::Install::install_sub({ code => sub { $_[0]->{$attr} }, as => $attr, }); } } { package App::Cmd::Tester::Exited 0.336; sub throw { my ($class, $code) = @_; $code = 0 unless defined $code; my $self = (bless \$code => $class); die $self; } } 1; __END__ =pod =encoding UTF-8 =head1 NAME App::Cmd::Tester - for capturing the result of running an app =head1 VERSION version 0.336 =head1 SYNOPSIS use Test::More tests => 4; use App::Cmd::Tester; use YourApp; my $result = test_app(YourApp => [ qw(command --opt value) ]); like($result->stdout, qr/expected output/, 'printed what we expected'); is($result->stderr, '', 'nothing sent to sderr'); is($result->error, undef, 'threw no exceptions'); my $result = test_app(YourApp => [ qw(command --opt value --quiet) ]); is($result->output, '', 'absolutely no output with --quiet'); =head1 DESCRIPTION One of the reasons that user-executed programs are so often poorly tested is that they are hard to test. App::Cmd::Tester is one of the tools App-Cmd provides to help make it easy to test App::Cmd-based programs. It provides one routine: test_app. =head1 PERL VERSION This library should run on perls released even a long time ago. It should work on any version of perl released in the last five years. Although it may work on older versions of perl, no guarantee is made that the minimum required version will not be increased. The version may be increased for any reason, and there is no promise that patches will be accepted to lower the minimum required perl. =head1 METHODS =head2 test_app B: while C is a method, it is by default exported as a subroutine into the namespace that uses App::Cmd::Tester. In other words: you probably don't need to think about this as a method unless you want to subclass App::Cmd::Tester. my $result = test_app($app_class => \@argv_contents); This will locally set C<@ARGV> to simulate command line arguments, and will then call the C method on the given application class (or application). Output to the standard output and standard error filehandles will be captured. C<$result> is an App::Cmd::Tester::Result object, which has methods to access the following data: stdout - the output sent to stdout stderr - the output sent to stderr output - the combined output of stdout and stderr error - the exception thrown by running the application, or undef run_rv - the return value of the run method (generally irrelevant) exit_code - the numeric exit code that would've been issued (0 is 'okay') The output is captured using L, which I ensure that the ordering is preserved in the combined output, but I capture the output of external programs. You can reverse these tradeoffs by using L instead. =for Pod::Coverage result_class =head1 AUTHOR Ricardo Signes =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2023 by Ricardo Signes. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut