File Coverage

blib/lib/Test2/AsyncSubtest.pm
Criterion Covered Total %
statement 223 279 79.9
branch 51 102 50.0
condition 26 55 47.2
subroutine 38 42 90.4
pod 14 17 82.3
total 352 495 71.1


line stmt bran cond sub pod time code
1             package Test2::AsyncSubtest;
2 29     29   2381669 use strict;
  29         51  
  29         660  
3 29     29   115 use warnings;
  29         35  
  29         544  
4              
5 29     29   9190 use Test2::IPC;
  29         11958  
  29         140  
6              
7             our $VERSION = '0.000018';
8              
9             our @CARP_NOT = qw/Test2::Util::HashBase/;
10              
11 29     29   1917 use Carp qw/croak cluck/;
  29         64  
  29         1195  
12 29     29   113 use Test2::Util qw/get_tid CAN_THREAD CAN_FORK/;
  29         33  
  29         1089  
13 29     29   98 use Scalar::Util qw/blessed/;
  29         32  
  29         1137  
14 29     29   99 use List::Util qw/first/;
  29         29  
  29         1244  
15              
16 29     29   13098 use Scope::Guard();
  29         9132  
  29         433  
17 29     29   123 use Test2::API();
  29         36  
  29         338  
18 29     29   234 use Test2::API::Context();
  29         32  
  29         314  
19 29     29   90 use Test2::Util::Trace();
  29         32  
  29         288  
20 29     29   14378 use Time::HiRes();
  29         33167  
  29         826  
21              
22 29     29   11031 use Test2::AsyncSubtest::Hub();
  29         51  
  29         459  
23 29     29   11256 use Test2::AsyncSubtest::Event::Attach();
  29         36  
  29         432  
24 29     29   11619 use Test2::AsyncSubtest::Event::Detach();
  29         43  
  29         789  
25              
26 29         103 use Test2::Util::HashBase qw{
27             name hub
28             trace send_to
29             events
30             finished
31             active
32             stack
33             id
34             children
35             _in_use
36             _attached pid tid
37 29     29   123 };
  29         29  
38              
39             sub CAN_REALLY_THREAD {
40 2     2 0 9047 return 0 unless CAN_THREAD;
41 0 0       0 return 0 unless eval { require threads; threads->VERSION('1.34'); 1 };
  0         0  
  0         0  
  0         0  
42 0         0 return 1;
43             }
44              
45             my @STACK;
46              
47 0 0   0 0 0 sub TOP { @STACK ? $STACK[-1] : undef }
48              
49             sub init {
50 216     216 0 128849 my $self = shift;
51              
52             croak "'name' is a required attribute"
53 216 100       827 unless $self->{+NAME};
54              
55 215   33     2176 $self->{+SEND_TO} ||= Test2::API::test2_stack()->top;
56 215   33     114567 $self->{+TRACE} ||= Test2::Util::Trace->new(frame => [caller(1)]);
57              
58 215         5672 $self->{+STACK} = [@STACK];
59 215         613 $_->{+_IN_USE}++ for reverse @STACK;
60              
61 215         433 $self->{+TID} = get_tid;
62 215         486 $self->{+PID} = $$;
63 215         878 $self->{+ID} = 1;
64 215         337 $self->{+FINISHED} = 0;
65 215         707 $self->{+ACTIVE} = 0;
66 215         384 $self->{+_IN_USE} = 0;
67 215         731 $self->{+CHILDREN} = [];
68              
69 215 50       488 unless($self->{+HUB}) {
70 215         803 my $ipc = Test2::API::test2_ipc();
71 215         2873 my $formatter = Test2::API::test2_stack->top->format;
72 215   100     4463 my $args = delete $self->{hub_init_args} || {};
73 215         2228 my $hub = Test2::AsyncSubtest::Hub->new(
74             %$args,
75             ipc => $ipc,
76             nested => 1,
77             formatter => $formatter,
78             );
79 215         2273 $self->{+HUB} = $hub;
80             }
81              
82 215         390 my $hub = $self->{+HUB};
83 215 50       480 $hub->set_ast_ids({}) unless $hub->ast_ids;
84 215         2124 $hub->listen($self->_listener);
85 215         3342 $hub->pre_filter($self->_pre_filter);
86             }
87              
88             sub _listener {
89 215     215   275 my $self = shift;
90              
91 215   50     1094 my $events = $self->{+EVENTS} ||= [];
92              
93 215     498   2636 sub { push @$events => $_[1] };
  498         18920  
94             }
95              
96             sub _pre_filter {
97 215     215   250 my $self = shift;
98              
99             sub {
100 134     134   33438 my ($hub, $e) = @_;
101 134 100       453 return $e if $hub->is_local;
102              
103 73         733 my $attached = $self->{+_ATTACHED};
104 73 50 33     1632 return $e if $attached && @$attached && $attached->[0] == $$ && $attached->[1] == get_tid;
      33        
      33        
105 0         0 $e->trace->throw("You must attach to an AsyncSubtest before you can send events to it from another process or thread");
106 0         0 return;
107 215         1666 };
108             }
109              
110             sub context {
111 167     167 1 272 my $self = shift;
112             return Test2::API::Context->new(
113             trace => $self->{+TRACE},
114 167         1737 hub => $self->{+SEND_TO},
115             );
116             }
117              
118             sub _gen_event {
119 47     47   177 my $self = shift;
120 47         297 my ($type, $id) = @_;
121              
122 47         307 my $class = "Test2::AsyncSubtest::Event::$type";
123              
124 47         2489 return $class->new(id => $id, trace => Test2::Util::Trace->new(frame => [caller(1)]));
125             }
126              
127             sub cleave {
128 185     185 1 204 my $self = shift;
129 185         322 my $id = $self->{+ID}++;
130 185         403 $self->{+HUB}->ast_ids->{$id} = 0;
131 185         1166 return $id;
132             }
133              
134             sub attach {
135 24     24 1 3251 my $self = shift;
136 24         347 my ($id) = @_;
137              
138 24 50       496 croak "An ID is required" unless $id;
139              
140             croak "ID $id is not valid"
141 24 50       935 unless defined $self->{+HUB}->ast_ids->{$id};
142              
143             croak "ID $id is already attached"
144 24 50       782 if $self->{+HUB}->ast_ids->{$id};
145              
146             croak "You must attach INSIDE the child process/thread"
147 24 50       1030 if $self->{+HUB}->is_local;
148              
149 24         1474 $self->{+_ATTACHED} = [ $$, get_tid, $id ];
150 24         378 $self->{+HUB}->send($self->_gen_event('Attach', $id));
151             }
152              
153             sub detach {
154 23     23 1 1300 my $self = shift;
155              
156 23 50 33     220 if ($self->{+PID} == $$ && $self->{+TID} == get_tid) {
157 0         0 cluck "You must detach INSIDE the child process/thread";
158 0         0 return;
159             }
160              
161 23 50       124 my $att = $self->{+_ATTACHED}
162             or croak "Not attached";
163              
164 23 50 33     326 croak "Attempt to detach from wrong child"
165             unless $att->[0] == $$ && $att->[1] == get_tid;
166              
167 23         55 my $id = $att->[2];
168              
169 23         101 $self->{+HUB}->send($self->_gen_event('Detach', $id));
170              
171 23         10323 delete $self->{+_ATTACHED};
172             }
173              
174 0     0 1 0 sub ready { return !shift->pending }
175             sub pending {
176 2     2 1 268 my $self = shift;
177 2         8 my $hub = $self->{+HUB};
178 2 50       11 return -1 unless $hub->is_local;
179              
180 2         53 $hub->cull;
181              
182 2         197 return $self->{+_IN_USE} + keys %{$self->{+HUB}->ast_ids};
  2         32  
183             }
184              
185             sub run {
186 56     56 1 456089 my $self = shift;
187 56         143 my ($code, @args) = @_;
188              
189 56 50 33     984 croak "AsyncSubtest->run() takes a codeblock as the first argument"
190             unless $code && ref($code) eq 'CODE';
191              
192 56         179 $self->start;
193              
194 56         666 my ($ok, $err, $finished);
195             T2_SUBTEST_WRAPPER: {
196 56         179 $ok = eval { $code->(@args); 1 };
  56         104  
  56         256  
  36         5126  
197 37         97 $err = $@;
198              
199             # They might have done 'BEGIN { skip_all => "whatever" }'
200 37 50 66     426 if (!$ok && $err =~ m/Label not found for "last T2_SUBTEST_WRAPPER"/) {
201 0         0 $ok = undef;
202 0         0 $err = undef;
203             }
204             else {
205 37         57 $finished = 1;
206             }
207             }
208              
209 53         9318 $self->stop;
210              
211 53         526 my $hub = $self->{+HUB};
212              
213 53 100       144 if (!$finished) {
214 16 50       254 if(my $bailed = $hub->bailed_out) {
215 0         0 my $ctx = $self->context;
216 0         0 $ctx->bail($bailed->reason);
217 0         0 return;
218             }
219 16         206 my $code = $hub->exit_code;
220 16         59 $ok = !$code;
221 16 50       106 $err = "Subtest ended with exit code $code" if $code;
222             }
223              
224 53 100       146 unless ($ok) {
225 1         19 my $e = Test2::Event::Exception->new(
226             error => $err,
227             trace => Test2::Util::Trace->new(frame => [caller(0)]),
228             );
229 1         56 $hub->send($e);
230             }
231              
232 53         194 return $hub->is_passing;
233             }
234              
235             sub start {
236 59     59 1 138 my $self = shift;
237              
238             croak "Subtest is already complete"
239 59 50       219 if $self->{+FINISHED};
240              
241 59         134 $self->{+ACTIVE}++;
242              
243 59         133 push @STACK => $self;
244 59         177 my $hub = $self->{+HUB};
245 59         440 my $stack = Test2::API::test2_stack();
246 59         1105 $stack->push($hub);
247              
248 59         557 return $hub->is_passing;
249             }
250              
251             sub stop {
252 56     56 1 151 my $self = shift;
253              
254             croak "Subtest is not active"
255 56 50       197 unless $self->{+ACTIVE}--;
256              
257 56 50 33     582 croak "AsyncSubtest stack mismatch"
258             unless @STACK && $self == $STACK[-1];
259              
260 56         105 pop @STACK;
261              
262 56         289 my $hub = $self->{+HUB};
263 56         201 my $stack = Test2::API::test2_stack();
264 56         526 $stack->pop($hub);
265 56         9144 return $hub->is_passing;
266             }
267              
268             sub finish {
269 167     167 1 19054 my $self = shift;
270 167         589 my %params = @_;
271              
272 167         1313 my $hub = $self->hub;
273              
274             croak "Subtest is already finished"
275 167 50       978 if $self->{+FINISHED}++;
276              
277 167 50       2273 croak "Subtest can only be finished in the process/thread that created it"
278             unless $hub->is_local;
279              
280             croak "Subtest is still active"
281 167 50       2355 if $self->{+ACTIVE};
282              
283 167         759 $self->wait;
284              
285 167         1615 my $todo = $params{todo};
286 167         307 my $skip = $params{skip};
287 167         234 my $empty = !@{$self->{+EVENTS}};
  167         395  
288 167         1262 my $no_asserts = !$hub->count;
289 167         572 my $collapse = $params{collapse};
290 167   100     1292 my $no_plan = $params{no_plan} || ($collapse && $no_asserts) || $skip;
291              
292 167 50 33     687 $hub->finalize($self->trace, !$no_plan)
293             unless $hub->no_ending || $hub->ended;
294              
295 167 50       19036 if ($hub->ipc) {
296 167         875 $hub->ipc->drop_hub($hub->hid);
297 167         41419 $hub->set_ipc(undef);
298             }
299              
300 167 50       1279 return $hub->is_passing if $params{silent};
301              
302 167         800 my $ctx = $self->context;
303              
304 167         4994 my $pass = 1;
305 167 100       435 if ($skip) {
306 1         3 $ctx->skip($self->{+NAME}, $skip);
307             }
308             else {
309 166 100 100     510 if ($collapse && $empty) {
310 3         14 $ctx->ok($hub->is_passing, $self->{+NAME});
311 3         481 return $hub->is_passing;
312             }
313              
314             my $e = $ctx->build_event(
315             'Subtest',
316             pass => $hub->is_passing,
317             subtest_id => $hub->id,
318             name => $self->{+NAME},
319             buffered => 1,
320 163 50       469 subevents => $self->{+EVENTS},
321             $todo ? (
322             todo => $todo,
323             effective_pass => 1,
324             ) : (),
325             );
326              
327 163         12904 $ctx->hub->send($e);
328              
329 163 100       23780 unless ($e->effective_pass) {
330 4         32 $ctx->failure_diag($e);
331              
332             $ctx->diag("Bad subtest plan, expected " . $hub->plan . " but ran " . $hub->count)
333 4 50 66     345 if $hub->plan && !$hub->check_plan && !grep {$_->causes_fail} @{$self->{+EVENTS}};
  0   33     0  
  0         0  
334             }
335              
336 163         996 $pass = $e->pass;
337             }
338              
339 164         622 $_->{+_IN_USE}-- for reverse @{$self->{+STACK}};
  164         475  
340              
341 164         755 return $pass;
342             }
343              
344             sub wait {
345 167     167 1 245 my $self = shift;
346              
347 167         314 my $hub = $self->{+HUB};
348 167         238 my $children = $self->{+CHILDREN};
349              
350 167         1205 while (@$children) {
351 138         984 $hub->cull;
352 138 50       9960 if (my $child = pop @$children) {
353 138 50       503 if (blessed($child)) {
354 0         0 $child->join;
355             }
356             else {
357 138         39219137 waitpid($child, 0);
358             }
359             }
360             else {
361 0         0 Time::HiRes::sleep('0.01');
362             }
363             }
364              
365 167         1489 $hub->cull;
366              
367             cluck "Subtest '$self->{+NAME}': All children have completed, but we still appear to be pending"
368 167 50 33     14250 if $hub->is_local && keys %{$self->{+HUB}->ast_ids};
  167         1955  
369             }
370              
371             sub fork {
372 180 50   180 1 912 croak "Forking is not supported" unless CAN_FORK;
373 180         1323 my $self = shift;
374 180         362 my $id = $self->cleave;
375 180         131084 my $pid = CORE::fork();
376              
377 180 50       2426 unless (defined $pid) {
378 0         0 delete $self->{+HUB}->ast_ids->{$id};
379 0         0 croak "Failed to fork";
380             }
381              
382 180 100       3223 if($pid) {
383 158         655 push @{$self->{+CHILDREN}} => $pid;
  158         2764  
384 158         2044 return $pid;
385             }
386              
387 22         1715 $self->attach($id);
388              
389 22         26411 return $self->_guard;
390             }
391              
392             sub run_fork {
393 180     180 1 442 my $self = shift;
394 180         286 my ($code, @args) = @_;
395              
396 180         407 my $f = $self->fork;
397 180 100       3854 return $f unless blessed($f);
398              
399 22         370 $self->run($code, @args);
400              
401 21         297 $self->detach();
402 21         234 $f->dismiss();
403 21         646 exit 0;
404             }
405              
406             sub run_thread {
407 0 0   0 1 0 croak "Threading is not supported"
408             unless CAN_REALLY_THREAD;
409              
410 0         0 my $self = shift;
411 0         0 my ($code, @args) = @_;
412              
413 0         0 my $id = $self->cleave;
414             my $thr = threads->create(sub {
415 0     0   0 $self->attach($id);
416              
417 0         0 $self->run($code, @args);
418              
419 0         0 $self->detach(get_tid);
420 0         0 return 0;
421 0         0 });
422              
423 0         0 push @{$self->{+CHILDREN}} => $thr;
  0         0  
424              
425 0         0 return $thr;
426             }
427              
428             sub _guard {
429 22     22   127 my $self = shift;
430              
431 22         140 my ($pid, $tid) = ($$, get_tid);
432              
433             return Scope::Guard->new(sub {
434 1 50 33 1   103 return unless $$ == $pid && get_tid == $tid;
435              
436 0         0 my $error = "Scope Leak";
437 0 0       0 if (my $ex = $@) {
438 0         0 chomp($ex);
439 0         0 $error .= " ($ex)";
440             }
441              
442 0         0 cluck $error;
443              
444 0         0 my $e = $self->context->build_event(
445             'Exception',
446             error => "$error\n",
447             );
448 0         0 $self->{+HUB}->send($e);
449 0         0 $self->detach();
450 0         0 exit 255;
451 22         982 });
452             }
453              
454             sub DESTROY {
455 1     1   2 my $self = shift;
456 1 50       12 return unless $self->{+NAME};
457              
458 0 0         if (my $att = $self->{+_ATTACHED}) {
459 0 0         return unless $self->{+HUB};
460 0           eval { $self->detach() };
  0            
461             }
462              
463 0 0         return if $self->{+FINISHED};
464 0 0         return unless $self->{+PID} == $$;
465 0 0         return unless $self->{+TID} == get_tid;
466              
467 0           local $@;
468 0           eval { $_->{+_IN_USE}-- for reverse @{$self->{+STACK}} };
  0            
  0            
469              
470 0           warn "Subtest $self->{+NAME} did not finish!";
471 0           exit 255;
472             }
473              
474             1;
475              
476             __END__