File Coverage

blib/lib/Test/Script/Run.pm
Criterion Covered Total %
statement 159 178 89.3
branch 42 64 65.6
condition 9 14 64.2
subroutine 30 30 100.0
pod 10 10 100.0
total 250 296 84.4


line stmt bran cond sub pod time code
1             package Test::Script::Run;
2              
3 2     2   82124 use warnings;
  2         5  
  2         70  
4 2     2   12 use strict;
  2         4  
  2         72  
5 2     2   12 use Test::More;
  2         8  
  2         11  
6 2     2   2800 use Test::Exception;
  2         9214  
  2         9  
7 2     2   2858 use IPC::Run3;
  2         122596  
  2         253  
8 2     2   24 use File::Basename;
  2         5  
  2         191  
9 2     2   15 use File::Spec;
  2         4  
  2         78  
10              
11             our $VERSION = '0.08';
12 2     2   11 use base 'Exporter';
  2         4  
  2         6268  
13             our @EXPORT =
14             qw/run_ok run_not_ok run_script run_output_matches run_output_matches_unordered/;
15             our @EXPORT_OK = qw/is_script_output last_script_stdout last_script_stderr
16             last_script_exit_code get_perl_cmd/;
17             our %EXPORT_TAGS = ( all => [ @EXPORT, @EXPORT_OK ] );
18             my (
19             $last_script_stdout, $last_script_stderr,
20             $last_script_exit_code,
21             );
22              
23             our @BIN_DIRS = ('bin','sbin','script', '.');
24              
25             =head1 NAME
26              
27             Test::Script::Run - test scripts with run
28              
29             =head1 SYNOPSIS
30              
31             use Test::Script::Run;
32             # customized names of bin dirs, default is qw/bin sbin script ./;
33             @Test::Script::Run::BIN_DIRS = qw/bin/;
34             run_ok( 'app_name', [ app's args ], 'you_app runs ok' );
35             my ( $return, $stdout, $stderr ) = run_script( 'app_name', [ app's args ] );
36             run_output_matches(
37             'app_name', [app's args],
38             [ 'out line 1', 'out line 2' ],
39             [ 'err line 1', 'err line 2' ],
40             'run_output_matches'
41             );
42             run_output_matches_unordered(
43             'app_name', [ app's args ],
44             [ 'out line 2', 'out line 1' ],
45             [ 'err line 2', 'err line 1' ],
46             'run_output_matches_unordered'
47             );
48              
49             =head1 DESCRIPTION
50              
51             This module exports some subs to help test and run scripts in your dist's
52             script directory( bin, sbin, script, etc ), if the script path is not absolute.
53              
54             Nearly all the essential code is stolen from Prophet::Test, we think subs like
55             those should live below C namespace, that's why we packed them and
56             created this module.
57              
58             =head1 FUNCTIONS
59              
60             =head2 run_script($script, $args, $stdout, $stderr)
61              
62             Runs the script $script as a perl script, setting the @INC to the same as
63             our caller.
64              
65             $script is the name of the script to be run (such as 'prophet'). $args is a
66             reference to an array of arguments to pass to the script. $stdout and $stderr
67             are both optional; if passed in, they will be passed to L's run3
68             subroutine as its $stdout and $stderr args. Otherwise, this subroutine will
69             create scalar references to pass to run3 instead (which are treated as strings
70             for STDOUT/STDERR to be written to).
71              
72             Returns run3's return value and, if no $stdout and $stderr were passed in, the
73             STDOUT and STDERR of the script that was run.
74              
75             =cut
76              
77             sub run_script {
78 15     15 1 4561 my $script = shift;
79 15   100     113 my $args = shift || [];
80 15         35 my ( $stdout, $stderr ) = @_;
81 15         23 my ( $new_stdout, $new_stderr, $return_stdouterr );
82 15 50 66     120 if ( !ref($stdout) && !ref($stderr) ) {
83 10         28 ( $stdout, $stderr, $return_stdouterr ) =
84             ( \$new_stdout, \$new_stderr, 1 );
85             }
86 15         170 my @cmd = get_perl_cmd($script);
87              
88 15 100       44 if (@cmd) {
89 13         96 my $ret = run3 [ @cmd, @$args ], undef, $stdout, $stderr;
90 13         4969234 $last_script_exit_code = $? >> 8;
91 13 100       131 if ( ref $stdout eq 'SCALAR' ) {
92 9         34 $last_script_stdout = $$stdout;
93             }
94              
95 13 100       53 if ( ref $stderr eq 'SCALAR' ) {
96 9         33 $last_script_stderr = $$stderr;
97             }
98              
99 13 100       330 return $return_stdouterr
100             ? ( $ret, $last_script_stdout, $last_script_stderr )
101             : $ret;
102             }
103             else {
104             # usually people use 127 to show error about the command can't be found
105 2         4 $last_script_exit_code = 127;
106 2         6 return;
107             }
108             }
109              
110             =head2 run_ok($script, $args, $msg)
111              
112             Runs the script, checking that it didn't error out.
113              
114             $script is the name of the script to be run (e.g. 'prophet'). $args
115             is an optional reference to an array of arguments to pass to the
116             script when it is run. $msg is an optional message to print with
117             the test. If $args is not specified, you can still pass in
118             a $msg.
119              
120             Returns nothing of interest.
121              
122             =cut
123              
124             sub run_ok {
125 3     3 1 1961 return _run_ok( '==', @_ );
126             }
127              
128             =head2 run_not_ok($script, $args, $msg)
129              
130             opposite of run_ok
131              
132             =cut
133              
134             sub run_not_ok {
135 3     3 1 8819 return _run_ok( '!=', @_ );
136             }
137              
138             sub _run_ok {
139 6   50 6   42 my $cmp = shift || '=='; # the exit code
140 6         13 my $script = shift;
141 6         9 my $args;
142 6 50       21 $args = shift if ( ref $_[0] eq 'ARRAY' );
143 6 50       21 my $msg = (@_) ? shift : '';
144              
145 6         15 local $Test::Builder::Level = $Test::Builder::Level + 1;
146              
147             lives_and {
148 6     6   102 local $Test::Builder::Level = $Test::Builder::Level + 1;
149 6         19 my ( $ret, $stdout, $stderr ) = run_script( $script, $args );
150 6         77 cmp_ok( $last_script_exit_code, $cmp, 0, _generate_test_name($msg, $script, @$args) );
151 6         84 };
152             }
153              
154             # _updir( $path )
155             #
156             # Strips off the filename in the given path and returns the absolute
157             # path of the remaining directory.
158              
159             sub _updir {
160 34     34   385 my $path = shift;
161 34         1832 my ( $file, $dir, undef ) = fileparse( File::Spec->rel2abs($path) );
162 34         133 return $dir;
163             }
164              
165             our $RUNCNT;
166              
167              
168             =head2 get_perl_cmd($script, @ARGS)
169              
170             Returns a list suitable for passing to C, C, etc. If you pass
171             C<$script> then we will search upwards for it in C<@BIN_DIRS>
172              
173             =cut
174              
175             sub get_perl_cmd {
176 15     15 1 54 my $script = shift;
177 15         23 my $base_dir;
178              
179 15 50       49 if (defined $script) {
180 15         24 my $fail = 0;
181 15 100       758 if ( File::Spec->file_name_is_absolute($script) ) {
182 2 50       104 unless ( -f $script ) {
183 0         0 warn "couldn't find the script $script";
184 0         0 $fail = 1;
185             }
186             }
187             else {
188 13         95 my ( $tmp, $i ) = ( _updir($0), 0 );
189 13         23 my $found;
190             LOOP:
191 13         49 while ( $i++ < 10 ) {
192 31         130 for my $bin ( @BIN_DIRS ) {
193 93 100       2367 if ( -f File::Spec->catfile( $tmp, $bin, $script ) ) {
194 11         104 $script = File::Spec->catfile( $tmp, $bin, $script );
195 11         23 $found = 1;
196 11         42 last LOOP;
197             }
198             }
199 20         38 $tmp = _updir($tmp);
200             }
201              
202 13 100       52 unless ( $found ) {
203 2         61 warn "couldn't find the script $script";
204 2         5 $fail = 1;
205             }
206             }
207 15 100       59 return if $fail;
208             }
209              
210             # We grep out references because of INC-hooks like Jifty::ClassLoader
211 13         37 my @cmd = ( $^X, ( map { "-I$_" } grep {!ref($_)} @INC ) );
  156         330  
  156         269  
212              
213 13 50       65 push @cmd, '-MDevel::Cover' if $INC{'Devel/Cover.pm'};
214 13 50       56 if ( $INC{'Devel/DProf.pm'} ) {
215 0         0 push @cmd, '-d:DProf';
216 0         0 $ENV{'PERL_DPROF_OUT_FILE_NAME'} = 'tmon.out.' . $$ . '.' . $RUNCNT++;
217             }
218              
219 13 50       31 if (defined $script) {
220 13         23 push @cmd, $script;
221 13         24 push @cmd, @_;
222             }
223              
224 13         81 return @cmd;
225             }
226              
227             # back-compat
228             *_get_perl_cmd = \&get_perl_cmd;
229              
230             =head2 is_script_output($scriptname \@args, \@stdout_match, \@stderr_match, $msg)
231              
232             Runs $scriptname, checking to see that its output matches.
233              
234             $args is an array reference of args to pass to the script. $stdout_match and
235             $stderr_match are references to arrays of expected lines. $msg is a string
236             message to display with the test. $stderr_match and $msg are optional. (As is
237             $stdout_match if for some reason you expect your script to have no output at
238             all. But that would be silly, wouldn't it?)
239              
240             Allows regex matches as well as string equality (lines in $stdout_match and
241             $stderr_match may be Regexp objects).
242              
243             =cut
244              
245             sub is_script_output {
246 4     4 1 22 my ( $script, $args, $exp_stdout, $exp_stderr, $msg ) = @_;
247 4         10 local $Test::Builder::Level = $Test::Builder::Level + 1;
248              
249 4         11 my $stdout_err = [];
250 4   50     20 $exp_stderr ||= [];
251              
252 4         27 my $ret = run_script(
253             $script, $args,
254             _mk_cmp_closure( 'stdout', $exp_stdout, $stdout_err ), # stdout
255             _mk_cmp_closure( 'stderr', $exp_stderr, $stdout_err ), # stderr
256             );
257              
258 4         94 _check_cmp_closure_output( $script, $msg, $args, $exp_stdout, $stdout_err );
259             }
260              
261             # =head2 _mk_cmp_closure($expected, $error)
262             # $expected is a reference to an array of expected output lines, and
263             # $error is an array reference for storing error messages.
264             #
265             # Returns a subroutine that takes a line of output and compares it
266             # to the next line in $expected. You can, for example, pass this
267             # subroutine to L::run3 and it will compare the output
268             # of the script being run to the expected output. After the script
269             # is done running, errors will be in $error.
270             #
271             # If a line in $expected is a Regexp reference (made with e.g.
272             # qr/foo/), the subroutine will check for a regexp match rather
273             # than string equality.
274              
275             sub _mk_cmp_closure {
276 8     8   17 my ( $type, $exp, $err ) = @_;
277              
278 8 100       19 if ( $type eq 'stderr' ) {
279 4         7 $last_script_stderr = '';
280 4         5 my $line = 0;
281             return sub {
282 6     6   152 my $output = shift;
283 6         10 ++$line;
284 6         14 $last_script_stderr .= $output;
285 6         16 __mk_cmp_closure()->( $exp, $err, $line, $output );
286             }
287 4         32 }
288             else {
289 4         8 $last_script_stdout = '';
290 4         9 my $line = 0;
291             return sub {
292 6     6   2419336 my $output = shift;
293 6         32 ++$line;
294 6         45 $last_script_stdout .= $output;
295 6         107 __mk_cmp_closure()->( $exp, $err, $line, $output );
296             }
297 4         39 }
298             }
299              
300             sub __mk_cmp_closure {
301             sub {
302 12     12   32 my ( $exp, $err, $line, $output ) = @_;
303 12         33 chomp $output;
304 12 50       46 unless (@$exp) {
305 0         0 push @$err, "$line: got $output";
306 0         0 return;
307             }
308 12         55 my $item = shift @$exp;
309 12 50       209 push @$err, "$line: got ($output), expect ($item)\n"
    50          
310             unless ref($item) eq 'Regexp'
311             ? ( $output =~ m/$item/ )
312             : ( $output eq $item );
313             }
314 12     12   185 }
315              
316             # XXX note that this sub doesn't check to make sure we got
317             # all the errors we were expecting (there can be more lines
318             # in the expected stderr than the received stderr as long
319             # as they match up until the end of the received stderr --
320             # the same isn't true of stdout)
321             sub _check_cmp_closure_output {
322 4     4   19 my ( $script, $msg, $args, $exp_stdout, $stdout_err ) = @_;
323              
324 4         29 for my $line (@$exp_stdout) {
325 0 0       0 next if !defined $line;
326 0         0 push @$stdout_err, "got nothing, expected: $line";
327             }
328              
329 4         37 my $test_name = _generate_test_name( $msg, $script, @$args );
330 4         67 is( scalar(@$stdout_err), 0, $test_name );
331              
332 4 50       3691 if (@$stdout_err) {
333 0         0 diag( "Different in line: " . join( "\n", @$stdout_err ) );
334             }
335             }
336              
337             =head2 run_output_matches($script, $args, $exp_stdout, $exp_stderr, $msg)
338              
339             A wrapper around L that also checks to make sure
340             the test runs without throwing an exception.
341              
342             =cut
343              
344             sub run_output_matches {
345 1     1 1 13 my ( $script, $args, $expected, $stderr, $msg ) = @_;
346 1         7 local $Test::Builder::Level = $Test::Builder::Level + 1;
347             lives_and {
348 1     1   32 local $Test::Builder::Level = $Test::Builder::Level + 5;
349 1         7 is_script_output( $script, $args, $expected, $stderr, $msg );
350 1         21 };
351             }
352              
353             =head2 run_output_matches_unordered($script, $args, $exp_stdout, $exp_stderr, $msg)
354              
355             This subroutine has exactly the same functionality as run_output_matches, but
356             doesn't impose a line ordering when comparing the expected and received
357             outputs.
358              
359             =cut
360              
361             sub run_output_matches_unordered {
362 1     1 1 8 my ( $cmd, $args, $stdout, $stderr, $msg ) = @_;
363 1   50     9 $stderr ||= [];
364              
365 1         10 my ( $val, $out, $err ) = run_script( $cmd, $args );
366              
367 1         16 local $Test::Builder::Level = $Test::Builder::Level + 1;
368              
369             # Check if each line matches a line in the expected output and
370             # delete that line if we have a match. If no match is found,
371             # add an error.
372 1         7 my $errors = [];
373 1         14 my @lines = split /\n/, $out;
374 1         11 OUTPUT: while ( my $line = shift @lines ) {
375 2         19 for my $exp_line (@$stdout) {
376 3 50       27 if (
    100          
377             (
378             ref($exp_line) eq 'Regexp'
379             ? ( $line =~ m/$exp_line/ )
380             : ( $line eq $exp_line )
381             )
382             )
383             {
384              
385             # remove the found element from the array of expected output
386 2         15 $stdout = [ grep { $_ ne $exp_line } @$stdout ];
  3         16  
387 2         13 next OUTPUT;
388             }
389             }
390              
391             # we didn't find a match
392 0         0 push @$errors, "couldn't find match for ($line)\n";
393             }
394              
395             # do the same for STDERR
396 1         8 @lines = split /\n/, $err;
397 1         6 ERROR: while ( my $line = shift @lines ) {
398 2         13 for my $exp_line (@$stderr) {
399 3 50       18 if (
    100          
400             (
401             ref($exp_line) eq 'Regexp'
402             ? ( $line =~ m/$exp_line/ )
403             : ( $line eq $exp_line )
404             )
405             )
406             {
407              
408             # remove the found element from the array of expected output
409 2         10 $stderr = [ grep { $_ ne $exp_line } @$stderr ];
  3         11  
410 2         11 next ERROR;
411             }
412             }
413              
414             # we didn't find a match
415 0         0 push @$errors, "couldn't find match for ($line)\n";
416             }
417              
418             # add any expected lines that we didn't find to the errors
419 1         7 for my $exp_line ( @$stdout, @$stderr ) {
420 0         0 push @$errors, "got nothing, expected: $exp_line";
421             }
422              
423 1         8 my $test_name = _generate_test_name( $msg, $cmd, @$args );
424 1         19 is( scalar(@$errors), 0, $test_name );
425              
426 1 50       1279 if (@$errors) {
427 0         0 diag( "Errors: " . join( "\n", @$errors ) );
428             }
429             }
430              
431             sub _is_windows {
432 11     11   246 return $^O =~ /MSWin/;
433             }
434              
435             sub _generate_test_name {
436 11     11   85 my $msg = shift;
437 11         23 my $script = shift;
438 11         27 my @args = @_;
439 11         17 my $args;
440 11 50       33 if ( _is_windows() ) {
441 0         0 eval { require Win32::ShellQuote };
  0         0  
442 0 0       0 if ($@) {
443 0         0 $args = join ' ', @_;
444             }
445             else {
446 0         0 $args = Win32::ShellQuote::quote_system_string(@_);
447             }
448             }
449             else {
450 11         20 eval { require String::ShellQuote };
  11         2016  
451 11 50       15126 if ($@) {
452 0         0 $args = join ' ', @_;
453             }
454             else {
455 11         98 $args = String::ShellQuote::shell_quote(@_);
456             }
457             }
458 11 50 66     787 return join( ' ', $msg ? "$msg:" : (), $script, defined $args && length $args ? $args : () );
    100          
459             }
460              
461             =head2 last_script_stdout
462              
463             return last script's stdout
464              
465             =cut
466              
467 4     4 1 939 sub last_script_stdout { $last_script_stdout }
468              
469             =head2 last_script_stderr
470              
471             return last script's stderr
472              
473             =cut
474              
475 4     4 1 27 sub last_script_stderr { $last_script_stderr }
476              
477             =head2 last_script_exit_code
478              
479             return last script's exit code
480              
481             =cut
482              
483 5     5 1 730 sub last_script_exit_code { $last_script_exit_code }
484              
485              
486             1;
487              
488             __END__