File Coverage

blib/lib/Test2/Tools/Process.pm
Criterion Covered Total %
statement 263 287 91.6
branch 60 80 75.0
condition 27 38 71.0
subroutine 58 60 96.6
pod 5 6 83.3
total 413 471 87.6


line stmt bran cond sub pod time code
1             package Test2::Tools::Process;
2              
3 9     9   1168592 use strict;
  9         48  
  9         263  
4 9     9   50 use warnings;
  9         17  
  9         209  
5 9     9   198 use 5.010;
  9         31  
6 9     9   832 use Test2::Tools::Compare ();
  9         136285  
  9         259  
7 9     9   56 use Test2::API qw( context );
  9         16  
  9         500  
8 9     9   4744 use Ref::Util qw( is_plain_arrayref is_ref is_plain_coderef is_plain_hashref );
  9         15592  
  9         730  
9 9     9   73 use Carp qw( croak carp );
  9         20  
  9         499  
10 9     9   65 use Test2::Compare::Array ();
  9         20  
  9         130  
11 9     9   47 use Test2::Compare::Wildcard ();
  9         20  
  9         111  
12 9     9   44 use Test2::Compare::Number ();
  9         24  
  9         187  
13 9     9   50 use Test2::Compare::String ();
  9         20  
  9         144  
14 9     9   60 use Test2::Compare::Custom ();
  9         29  
  9         192  
15 9     9   59 use Test2::Compare ();
  9         59  
  9         211  
16 9     9   5073 use Capture::Tiny qw( capture_stdout );
  9         249059  
  9         687  
17 9     9   88 use base qw( Exporter );
  9         30  
  9         2341  
18              
19             our @EXPORT = qw( process proc_event named_signal intercept_exit intercept_exec );
20             our @CARP_NOT = qw( Test2::Tools::Process::SystemProc );
21              
22             # ABSTRACT: Unit tests for code that calls exit, exec, system or qx()
23             our $VERSION = '0.05'; # VERSION
24              
25              
26             our %handlers;
27              
28             BEGIN {
29              
30             %handlers = (
31 0         0 exit => sub (;$) { CORE::exit(@_) },
32 0         0 exec => sub { CORE::exec(@_) },
33 0         0 system => sub { CORE::system(@_) },
34 0         0 readpipe => sub (_) { CORE::readpipe(@_) },
35 9     9   536 );
36              
37 9     9   119 no warnings 'redefine';
  9         23  
  9         2161  
38 9     11   82 *CORE::GLOBAL::exit = sub (;$) { $handlers{exit}->(@_) };
  11         89  
39 9     21   92 *CORE::GLOBAL::exec = sub { $handlers{exec}->(@_) };
  21         510  
40 9     15   90 *CORE::GLOBAL::system = sub { $handlers{system}->(@_) };
  15         2698  
41 9     2   18654 *CORE::GLOBAL::readpipe = sub (_) { $handlers{readpipe}->(@_) };
  2         8  
42             }
43              
44              
45             sub process (&;@)
46             {
47 33     33 1 12206 my $sub = shift;
48 33         66 my @expected = ();
49 33         64 my $test_name = 'process ok';
50 33         51 my @events;
51 33         54 my $i = 0;
52              
53 33 100       107 if(is_plain_arrayref $_[0])
54             {
55 31         59 @expected = @{ shift() };
  31         76  
56             }
57              
58 33 100       81 $test_name = shift if defined $_[0];
59              
60             Test2::Tools::Process::ReturnMultiLevel::with_return(sub {
61 33     33   60 my($return) = @_;
62              
63 33         166 local %handlers = %handlers;
64              
65             $handlers{exit} = sub {
66 8         16 my $expected = $expected[$i++];
67              
68 8         14 my $status = shift;
69 8 100       30 $status = 0 unless defined $status;
70 8         14 $status = int($status);
71 8         30 push @events, { event_type => 'exit', exit_status => $status };
72              
73 8 100 33     252 if(defined $expected && $expected->is_exit && defined $expected->callback)
      66        
74             {
75 2         20 my $proc = Test2::Tools::Process::Proc->new($return);
76 2         57 my $ret = $expected->callback->($proc, $status);
77 2 50       25 if(exists $proc->{errno})
78             {
79 0         0 $! = $proc->{errno};
80 0         0 return 0;
81             }
82 2         11 return $ret;
83             }
84             else
85             {
86 6         53 $return->();
87             }
88 33         150 };
89              
90             $handlers{exec} = sub {
91 13         28 my $expected = $expected[$i++];
92              
93 13 100 100     59 if(@_ == 1 || @_ == 0)
94             {
95 8         27 push @events, { event_type => 'exec', command => $_[0] };
96             }
97             else
98             {
99 5         25 push @events, { event_type => 'exec', command => [@_] };
100             }
101              
102 13 100 33     457 if(defined $expected && $expected->is_exec && defined $expected->callback)
      66        
103             {
104 1         18 my $proc = Test2::Tools::Process::Proc->new($return);
105 1         19 my $ret = $expected->callback->($proc, @_);
106 1 50       25 if(exists $proc->{errno})
107             {
108 0         0 $! = $proc->{errno};
109 0         0 return 0;
110             }
111 1         10 return $ret;
112             }
113             else
114             {
115 12         109 $return->();
116             }
117 33         130 };
118              
119 33         80 foreach my $type (qw( system readpipe ))
120             {
121             $handlers{$type} = sub {
122 17         38 my $expected = $expected[$i++];
123              
124 17         23 my $event;
125 17         36 my $args = \@_;
126 17 100 100     69 if(@_ == 1 || @_ == 0)
127             {
128 11         42 push @events, $event = { event_type => 'system', command => $_[0] };
129             }
130             else
131             {
132 6         25 push @events, $event = { event_type => 'system', command => [@_] };
133             }
134              
135 17 50 33     509 if(defined $expected && $expected->is_system && defined $expected->callback)
      33        
136             {
137             my $inner = sub {
138 17         34 my($return) = @_;
139 17         65 my $proc = Test2::Tools::Process::SystemProc->new($return, $event, $type);
140 17         359 $expected->callback->($proc, @$args);
141 9         57 $event->{status} = 0;
142 9         36 $? = 0;
143 17         167 };
144 17 100       48 if($type eq 'system')
145             {
146 15         43 Test2::Tools::Process::ReturnMultiLevel::with_return($inner);
147 15 100       92 return -1 if exists $event->{errno};
148 14         219 return $?;
149             }
150             else
151             {
152 2         42 return scalar capture_stdout { Test2::Tools::Process::ReturnMultiLevel::with_return($inner) };
  2         1985  
153             }
154             }
155             else
156             {
157             local $SIG{__WARN__} = sub {
158 0         0 my($message) = @_;
159 0         0 $message =~ s/ at .*? line [0-9]+\.$//;
160 0         0 chomp $message;
161 0         0 carp($message);
162 0         0 };
163 0 0       0 my $ret = $type eq 'system' ? CORE::system(@_) : CORE::readpipe(@_);
164 0 0       0 if($? == -1)
    0          
165             {
166 0         0 $event->{errno} = $!;
167             }
168             elsif($? & 127)
169             {
170 0         0 $event->{signal} = $? & 127;
171             }
172             else
173             {
174 0         0 $event->{status} = $? >> 8;
175             }
176 0         0 return $ret;
177             }
178 66         350 };
179             }
180              
181 33         96 $sub->();
182 33         196 });
183              
184             @_ = (
185             \@events,
186 33         3620 [ map { $_->to_check } @expected ],
  41         175  
187             $test_name
188             );
189              
190 33         461 goto \&Test2::Tools::Compare::is;
191             }
192              
193              
194             {
195             my $sig;
196             sub named_signal ($)
197             {
198 71     71 1 35809 my($name) = @_;
199              
200             # build hash on demand.
201 71   66     167 $sig ||= do {
202 1         21 require Config;
203 1         5 my %sig;
204 1         115 my @num = split /\s+/, $Config::Config{sig_num};
205 1         51 foreach my $name (split /\s+/, $Config::Config{sig_name})
206             {
207 69         217 $sig{$name} = shift @num;
208             }
209 1         12 \%sig;
210             };
211              
212 71 100       405 croak "no such signal: $name" unless exists $sig->{$name};
213              
214 70         174 $sig->{$name};
215             }
216             }
217              
218              
219             sub intercept_exit (&)
220             {
221 4     4 1 11224 my $sub = shift;
222              
223 4         7 my $ret;
224              
225             Test2::Tools::Process::ReturnMultiLevel::with_return(sub {
226 4     4   11 my $return = shift;
227             local $handlers{exit} = sub {
228 3         6 $ret = shift;
229 3 100       12 $ret = 0 unless defined $ret;
230 3         7 $ret = int $ret;
231 3         8 $return->();
232 4         17 };
233 4         12 $sub->();
234 4         25 });
235              
236 4         436 $ret;
237             }
238              
239              
240             sub intercept_exec (&)
241             {
242 3     3 1 434 my $sub = shift;
243              
244 3         4 my $ret;
245              
246             Test2::Tools::Process::ReturnMultiLevel::with_return(sub {
247 3     3   9 my $return = shift;
248             local $handlers{exec} = sub {
249 2         14 $ret = \@_;
250 2         6 $return->();
251 3         11 };
252 3         12 $sub->();
253 3         19 });
254              
255 3         378 $ret;
256             }
257              
258              
259             sub proc_event ($;$$$)
260             {
261 41     41 1 186059 my $type = shift;
262 41 50       120 croak("no such process event undef") unless defined $type;
263              
264 41         108 my $check;
265             my $check2;
266 41         0 my $callback;
267              
268 41 100 100     265 $check = shift if defined $_[0] && !is_plain_coderef $_[0] && !is_plain_hashref $_[0];
      100        
269 41 100 100     141 $check2 = shift if defined $_[0] && is_plain_hashref $_[0];
270              
271 41 100       88 if(defined $_[0])
272             {
273 21 50       52 if(is_plain_coderef $_[0])
274             {
275 21         34 $callback = shift;
276             }
277             else
278             {
279 0         0 croak("callback is not a code reference");
280             }
281             }
282              
283 41         155 my @caller = caller;
284              
285 41 100       255 if($type eq 'exit')
    50          
286             {
287 9 100       21 if(defined $check)
288             {
289 6 100       15 unless(is_ref $check)
290             {
291 4         25 $check = Test2::Compare::Number->new(
292             file => $caller[1],
293             lines => [$caller[2]],
294             input => $check,
295             );
296             }
297             }
298             else
299             {
300             $check = Test2::Compare::Custom->new(
301 2 50   2   1818 code => sub { defined $_ ? 1 : 0 },
302 3         74 name => 'DEFINED',
303             operator => 'DEFINED()',
304             file => $caller[1],
305             lines => [$caller[2]],
306             );
307             }
308              
309 9         366 return Test2::Tools::Process::Exit->new(status_check => $check, callback => $callback);
310             }
311              
312             elsif($type =~ /^(exec|system)$/)
313             {
314 32 100       73 if(defined $check)
315             {
316 22 100       77 if(is_plain_arrayref $check)
    100          
317             {
318 5         57 my $array = Test2::Compare::Array->new(
319             called => \@caller,
320             );
321 5         199 foreach my $item (@$check)
322             {
323 11         190 my $wc = Test2::Compare::Wildcard->new(
324             expect => $item,
325             file => $caller[1],
326             lines => [$caller[2]],
327             );
328 11         301 $array->add_item($wc);
329             }
330 5         116 $check = $array;
331             }
332             elsif(!is_ref $check)
333             {
334 4         24 $check = Test2::Compare::String->new(
335             file => $caller[1],
336             lines => [$caller[2]],
337             input => $check,
338             );
339             }
340             }
341             else
342             {
343             $check = Test2::Compare::Custom->new(
344 8 50   8   4789 code => sub { defined $_ ? 1 : 0 },
345 10         57 name => 'DEFINED',
346             operator => 'DEFINED()',
347             file => $caller[1],
348             lines => [$caller[2]],
349             );
350             }
351              
352 32 100       436 if($type eq 'system')
353             {
354 18   100     64 $check2 ||= { status => 0 };
355             }
356              
357 32 100       75 my $class = $type eq 'exec'
358             ? 'Test2::Tools::Process::Exec'
359             : 'Test2::Tools::Process::System';
360 32         192 return $class->new( command_check => $check, result_check => $check2, callback => $callback);
361             }
362              
363 0         0 croak("no such process event $type");
364             }
365              
366             package Test2::Tools::Process::Event;
367              
368 9     9   104 use constant is_exit => 0;
  9         32  
  9         1015  
369 9     9   77 use constant is_exec => 0;
  9         29  
  9         556  
370 9     9   64 use constant is_system => 0;
  9         23  
  9         430  
371 9     9   5063 use Class::Tiny qw( callback );
  9         17161  
  9         48  
372              
373             package Test2::Tools::Process::Exit;
374              
375 9     9   2202 use constant is_exit => 1;
  9         21  
  9         537  
376 9     9   59 use base qw( Test2::Tools::Process::Event );
  9         19  
  9         3450  
377 9     9   76 use Class::Tiny qw( status_check );
  9         19  
  9         71  
378              
379             sub to_check
380             {
381 9     9   21 my($self) = @_;
382 9         182 { event_type => 'exit', exit_status => $self->status_check };
383             }
384              
385             package Test2::Tools::Process::Exec;
386              
387 9     9   2482 use constant is_exec => 1;
  9         30  
  9         546  
388 9     9   77 use base qw( Test2::Tools::Process::Event );
  9         39  
  9         2729  
389 9     9   72 use Class::Tiny qw( command_check );
  9         21  
  9         36  
390              
391             sub to_check
392             {
393 14     14   34 my($self) = @_;
394 14         330 { event_type => 'exec', command => $self->command_check };
395             }
396              
397             package Test2::Tools::Process::System;
398              
399 9     9   2293 use constant is_system => 1;
  9         22  
  9         645  
400 9     9   62 use base qw( Test2::Tools::Process::Event );
  9         19  
  9         2692  
401 9     9   69 use Class::Tiny qw( command_check result_check );
  9         33  
  9         40  
402              
403             sub to_check
404             {
405 18     18   35 my($self) = @_;
406 18         334 { event_type => 'system', command => $self->command_check, %{ $self->result_check } };
  18         342  
407             }
408              
409             package Test2::Tools::Process::Proc;
410              
411             sub new
412             {
413 3     3   9 my($class, $return) = @_;
414 3         10 bless {
415             return => $return,
416             }, $class;
417             }
418              
419 0     0   0 sub terminate { shift->{return}->() }
420              
421             sub errno
422             {
423 0     0   0 my($self, $errno) = @_;
424 0         0 $self->{errno} = $errno;
425             }
426              
427             package Test2::Tools::Process::SystemProc;
428              
429             sub new
430             {
431 17     17   51 my($class, $return, $result, $type) = @_;
432 17         77 bless {
433             return => $return,
434             result => $result,
435             type => $type,
436             }, $class;
437             }
438              
439 2     2   1155 sub type { shift->{type} }
440              
441             sub exit
442             {
443 5     5   7291 my($self, $status) = @_;
444 5 100       17 $status = 0 unless defined $status;
445 5         31 $status = int $status;
446 5         22 $self->{result}->{status} = $status;
447 5         12 $? = $status << 8;
448 5         13 $self->{return}->();
449             }
450              
451             sub signal
452             {
453 3     3   568 my($self, $signal) = @_;
454 3 50       8 $signal = 0 unless defined $signal;
455 3 100       14 if($signal =~ /^[A-Z]/i)
456             {
457 2         8 $signal = Test2::Tools::Process::named_signal($signal);
458             }
459             else
460             {
461 1         3 $signal = int $signal;
462             }
463 2         5 $self->{result}->{signal} = $signal;
464 2         5 $? = $signal;
465 2         5 $self->{return}->();
466             }
467              
468             sub errno
469             {
470 1     1   13 my($self, $errno) = @_;
471 1 50       6 $errno = 0 unless defined $errno;
472 1         2 $errno = int $errno;
473 1         9 $self->{result}->{errno} = $! = $errno;
474 1         4 $self->{return}->();
475             }
476              
477             package Test2::Tools::Process::ReturnMultiLevel;
478              
479             # this is forked from Return::MultiLevel (XS implementation only)
480             # we can remove this when it gets a maintainer again.
481              
482 9     9   12626 use Scope::Upper;
  9         8713  
  9         398  
483 9     9   65 use Carp ();
  9         20  
  9         169  
484 9     9   46 use base qw( Exporter );
  9         19  
  9         2265  
485             our @EXPORT_OK = qw( with_return );
486              
487             $INC{'Test2/Tools/Process/ReturnMultiLevel.pm'} = __FILE__;
488              
489             sub with_return (&)
490             {
491 92     92 0 10056 my ($f) = @_;
492 92         264 my $ctx = Scope::Upper::HERE();
493             my @canary =
494             !$ENV{RETURN_MULTILEVEL_DEBUG}
495 92 50       320 ? '-'
496             : Carp::longmess "Original call to with_return"
497             ;
498              
499 92         167 local $canary[0];
500             $f->(sub {
501 51 0   51   215 $canary[0]
    50          
502             and confess
503             $canary[0] eq '-'
504             ? ""
505             : "Captured stack:\n$canary[0]\n",
506             "Attempt to re-enter dead call frame"
507             ;
508 51         747 Scope::Upper::unwind(@_, $ctx);
509             })
510 92         338 }
511              
512             1;
513              
514             __END__