File Coverage

blib/lib/Test/MockCommand/Recorder.pm
Criterion Covered Total %
statement 75 78 96.1
branch 14 22 63.6
condition n/a
subroutine 18 18 100.0
pod 10 10 100.0
total 117 128 91.4


line stmt bran cond sub pod time code
1             package Test::MockCommand::Recorder;
2 18     18   113 use strict;
  18         33  
  18         494  
3 18     18   88 use warnings;
  18         34  
  18         392  
4              
5 18     18   79 use Cwd;
  18         27  
  18         867  
6 18     18   86 use Carp qw(croak);
  18         27  
  18         568  
7 18     18   81 use Symbol;
  18         29  
  18         822  
8              
9 18     18   6479 use Test::MockCommand::Result;
  18         45  
  18         467  
10 18     18   185 use Test::MockCommand::TiedFH;
  18         33  
  18         7813  
11              
12             sub new {
13 18     18 1 55 my $class = shift;
14 18         70 return bless {}, $class;
15             }
16              
17             sub handle {
18 49     49 1 133 my $self = shift;
19 49 50       295 croak "odd number of parameters" if @_ % 2;
20 49         180 my %args = @_;
21              
22             # check arguments
23 49         305 for (qw(command function arguments caller)) {
24 196 50       460 croak "no $_ parameter" unless exists $args{$_};
25             }
26              
27             # check function is one we know
28             croak "unknown function $args{function}"
29 49 50       605 unless $args{function} =~ /^(open|readpipe|system|exec)$/;
30              
31             # do we want to handle recording this command?
32 49 50       252 return undef unless $self->matches(%args);
33              
34             # capture data before running the command
35 49         191 $args{result} = $self->pre_capture(%args);
36              
37             # emulate and record the appropriate function
38 49 100       1144 if ($args{function} eq 'open') {
    100          
    100          
    50          
39 16         331 $args{return_value} = $self->record_open(%args);
40             }
41             elsif ($args{function} eq 'readpipe') {
42 24         518 $args{return_value} = $self->record_readpipe(%args);
43             }
44             elsif ($args{function} eq 'system') {
45 8         125 $args{return_value} = $self->record_system(%args);
46             }
47             elsif ($args{function} eq 'exec') {
48 1         19 $args{return_value} = $self->record_exec(%args);
49             }
50              
51             # capture data after running the command
52 49         906 $self->post_capture(%args);
53              
54             # return the result object
55 49         557 return $args{result};
56             }
57              
58             sub matches {
59             # always record commands
60 49     49 1 274 return 1;
61             }
62              
63             sub pre_capture {
64 49     49 1 95 my $self = shift;
65              
66             # create result object
67 49         562 my $result = Test::MockCommand::Result->new(@_);
68              
69             # set the current working directory
70 49         228274 $result->cwd(Cwd::cwd());
71              
72             # TODO: check command line to see if it's a shell invocation, and if so,
73             # check if it uses an external source of input with the '<' operator
74              
75 49         875 return $result;
76             }
77              
78             sub post_capture {
79 49     49 1 206 my $self = shift;
80 49         338 my %args = @_;
81              
82 49         784 $args{result}->return_value($args{return_value});
83              
84 49         403 $args{result}->exit_code($?);
85              
86             # TODO: check command line to see if it's a shell invocation, and if so,
87             # check if it writes or appends to an external output file with the
88             # '>', '>>', '2>', etc. operators
89             }
90              
91             sub record_open {
92 16     16 1 58 my $self = shift;
93 16         180 my %all_args = @_;
94 16         112 my $args = $all_args{arguments};
95              
96             # call the real open() using our own filehandle
97 16         48 my $fh = undef;
98 16         45156 my $opened = (@{$args} < 3)
99             ? CORE::open($fh, $args->[-1])
100 16 50       32 : CORE::open($fh, $args->[1], $args->[2], splice(@{$args}, 3));
  0         0  
101 16 50       412 return 0 unless $opened;
102              
103             # make our own filehandle and tie it to the open FH and result object
104 16         337 my $our_fh = $self->create_tied_fh($fh, $all_args{result});
105              
106             # create the requested filehandle
107 16 50       89 if (defined $args->[0]) {
108             # file handle is a bareword symbol reference
109 0         0 my $sym = Symbol::qualify($args->[0], $all_args{caller}->[0]);
110              
111 18     18   120 no strict 'refs';
  18         32  
  18         3962  
112 0         0 *$sym = $our_fh;
113             }
114             else {
115 16         103 $args->[0] = $our_fh;
116             }
117              
118             # return result of open
119 16         391 return $opened;
120             }
121              
122             sub create_tied_fh {
123 16     16 1 118 my ($self, $real_open_fh, $result_object) = @_;
124 16         304 my $fh = gensym();
125 16         1543 tie *$fh, 'Test::MockCommand::TiedFH', 1, $real_open_fh, $result_object;
126 16         62 return $fh;
127             }
128              
129             sub record_system {
130 9     9 1 57 my $self = shift;
131 9         124 my %args = @_;
132 9         34 return CORE::system(@{$args{arguments}});
  9         37557  
133             }
134              
135             sub record_exec {
136 1     1 1 3 my $self = shift;
137 1         9 return $self->record_system(@_);
138             }
139              
140             sub record_readpipe {
141 24     24 1 102 my $self = shift;
142 24         310 my %args = @_;
143              
144             # readpipe() in perl >= 5.27.7 breaks when passed a dereferenced array,
145             # it essentially calls scalar() on its arguments. @{["command"]} becomes
146             # "1" rather than "command". We have to pass a single scalar value.
147             # https://rt.perl.org/Public/Bug/Display.html?id=132810
148             # https://rt.perl.org/Public/Bug/Display.html?id=4574
149 24         101237 my @out = CORE::readpipe($args{arguments}->[0]);
150              
151             # handle() will automatically split this according to $/
152             # if needed, but we always return it as a scalar
153 24         1358 return join '', @out;
154             }
155              
156             1;
157              
158             __END__