File Coverage

blib/lib/Test2/IPC/Driver/Files.pm
Criterion Covered Total %
statement 238 256 92.9
branch 102 136 75.0
condition 28 38 73.6
subroutine 29 30 96.6
pod 8 15 53.3
total 405 475 85.2


line stmt bran cond sub pod time code
1             package Test2::IPC::Driver::Files;
2 30     30   3014 use strict;
  30         95  
  30         941  
3 30     30   176 use warnings;
  30         60  
  30         1715  
4              
5             our $VERSION = '1.302180';
6              
7 30     30   14339 BEGIN { require Test2::IPC::Driver; our @ISA = qw(Test2::IPC::Driver) }
  30         1358  
8              
9 30     30   219 use Test2::Util::HashBase qw{tempdir event_ids read_ids timeouts tid pid globals};
  30         71  
  30         138  
10              
11 30     30   203 use Scalar::Util qw/blessed/;
  30         68  
  30         1581  
12 30     30   22089 use File::Temp();
  30         528570  
  30         824  
13 30     30   19988 use Storable();
  30         99680  
  30         924  
14 30     30   231 use File::Spec();
  30         76  
  30         483  
15 30     30   160 use POSIX();
  30         72  
  30         702  
16              
17 30     30   165 use Test2::Util qw/try get_tid pkg_to_file IS_WIN32 ipc_separator do_rename do_unlink/;
  30         62  
  30         2484  
18 30     30   192 use Test2::API qw/test2_ipc_set_pending/;
  30         66  
  30         103483  
19              
20 57     57 1 310 sub is_viable { 1 }
21              
22             sub init {
23 83     83 0 177 my $self = shift;
24              
25             my $tmpdir = File::Temp::tempdir(
26 83   33     1064 $ENV{T2_TEMPDIR_TEMPLATE} || "test2" . ipc_separator . $$ . ipc_separator . "XXXXXX",
27             CLEANUP => 0,
28             TMPDIR => 1,
29             );
30              
31 83 50       40007 $self->abort_trace("Could not get a temp dir") unless $tmpdir;
32              
33 83         642 $self->{+TEMPDIR} = File::Spec->canonpath($tmpdir);
34              
35             print STDERR "\nIPC Temp Dir: $tmpdir\n\n"
36 83 100       309 if $ENV{T2_KEEP_TEMPDIR};
37              
38 83         262 $self->{+EVENT_IDS} = {};
39 83         194 $self->{+READ_IDS} = {};
40 83         186 $self->{+TIMEOUTS} = {};
41              
42 83         257 $self->{+TID} = get_tid();
43 83         303 $self->{+PID} = $$;
44              
45 83         186 $self->{+GLOBALS} = {};
46              
47 83         245 return $self;
48             }
49              
50             sub hub_file {
51 187     187 0 354 my $self = shift;
52 187         347 my ($hid) = @_;
53 187         415 my $tdir = $self->{+TEMPDIR};
54 187         3107 return File::Spec->catfile($tdir, "HUB" . ipc_separator . $hid);
55             }
56              
57             sub event_file {
58 38     38 0 114 my $self = shift;
59 38         152 my ($hid, $e) = @_;
60              
61 38         118 my $tempdir = $self->{+TEMPDIR};
62 38 100       427 my $type = blessed($e) or $self->abort("'$e' is not a blessed object!");
63              
64 37 100       575 $self->abort("'$e' is not an event object!")
65             unless $type->isa('Test2::Event');
66              
67 36         118 my $tid = get_tid();
68 36         410 my $eid = $self->{+EVENT_IDS}->{$hid}->{$$}->{$tid} += 1;
69              
70 36         319 my @type = split '::', $type;
71 36         240 my $name = join(ipc_separator, $hid, $$, $tid, $eid, @type);
72              
73 36         483 return File::Spec->catfile($tempdir, $name);
74             }
75              
76             sub add_hub {
77 92     92 1 212 my $self = shift;
78 92         207 my ($hid) = @_;
79              
80 92         322 my $hfile = $self->hub_file($hid);
81              
82 92 100       2245 $self->abort_trace("File for hub '$hid' already exists")
83             if -e $hfile;
84              
85 91 50       5437 open(my $fh, '>', $hfile) or $self->abort_trace("Could not create hub file '$hid': $!");
86 91         1588 print $fh "$$\n" . get_tid() . "\n";
87 91         3837 close($fh);
88             }
89              
90             sub drop_hub {
91 56     56 1 169 my $self = shift;
92 56         127 my ($hid) = @_;
93              
94 56         133 my $tdir = $self->{+TEMPDIR};
95 56         189 my $hfile = $self->hub_file($hid);
96              
97 56 100       1031 $self->abort_trace("File for hub '$hid' does not exist")
98             unless -e $hfile;
99              
100 55 50       2000 open(my $fh, '<', $hfile) or $self->abort_trace("Could not open hub file '$hid': $!");
101 55         1516 my ($pid, $tid) = <$fh>;
102 55         579 close($fh);
103              
104 55 50       377 $self->abort_trace("A hub file can only be closed by the process that started it\nExpected $pid, got $$")
105             unless $pid == $$;
106              
107 55 50       189 $self->abort_trace("A hub file can only be closed by the thread that started it\nExpected $tid, got " . get_tid())
108             unless get_tid() == $tid;
109              
110 55 100       190 if ($ENV{T2_KEEP_TEMPDIR}) {
111 1         14 my ($ok, $err) = do_rename($hfile, File::Spec->canonpath("$hfile.complete"));
112 1 50       10 $self->abort_trace("Could not rename file '$hfile' -> '$hfile.complete': $err") unless $ok
113             }
114             else {
115 54         288 my ($ok, $err) = do_unlink($hfile);
116 54 50       280 $self->abort_trace("Could not remove file for hub '$hid': $err") unless $ok
117             }
118              
119 55 50       1576 opendir(my $dh, $tdir) or $self->abort_trace("Could not open temp dir!");
120              
121 55         184 my %bad;
122 55         982 for my $file (readdir($dh)) {
123 169 100       565 next if $file =~ m{\.complete$};
124 167 100       1620 next unless $file =~ m{^$hid};
125              
126 1 50 0     3 eval { $bad{$file} = $self->read_event_file(File::Spec->catfile($tdir, $file)); 1 } or $bad{$file} = $@ || "Unknown error reading file";
  1         17  
  1         6  
127             }
128 55         636 closedir($dh);
129              
130 55 100       1870 return unless keys %bad;
131              
132 1         3 my $data;
133 1         2 my $ok = eval {
134 1         769 require JSON::PP;
135 1     1   14731 local *UNIVERSAL::TO_JSON = sub { +{ %{$_[0]} } };
  1         168  
  1         6  
136 1         6 my $json = JSON::PP->new->ascii->pretty->canonical->allow_unknown->allow_blessed->convert_blessed;
137 1         213 $data = $json->encode(\%bad);
138 1         148 1;
139             };
140 1   33     10 $ok ||= eval {
141 0         0 require Data::Dumper;
142 0         0 local $Data::Dumper::Sortkeys = 1;
143 0         0 $data = Data::Dumper::Dumper(\%bad);
144 0         0 1;
145             };
146              
147 1 50       5 $data = "Could not dump data... sorry." unless defined $data;
148              
149 1         13 $self->abort_trace("Not all files from hub '$hid' have been collected!\nHere is the leftover data:\n========================\n$data\n===================\n");
150             }
151              
152             sub try_sig_mask {
153 36     36 1 114 my $self = shift;
154 36         107 my ($code) = @_;
155              
156 36         95 my ($old, $blocked);
157 36         102 unless(IS_WIN32) {
158 36         759 my $to_block = POSIX::SigSet->new(
159             POSIX::SIGINT(),
160             POSIX::SIGALRM(),
161             POSIX::SIGHUP(),
162             POSIX::SIGTERM(),
163             POSIX::SIGUSR1(),
164             POSIX::SIGUSR2(),
165             );
166 36         171 $old = POSIX::SigSet->new;
167 36         895 $blocked = POSIX::sigprocmask(POSIX::SIG_BLOCK(), $to_block, $old);
168             # Silently go on if we failed to log signals, not much we can do.
169             }
170              
171 36         473 my ($ok, $err) = &try($code);
172              
173             # If our block was successful we want to restore the old mask.
174 36 50       612 POSIX::sigprocmask(POSIX::SIG_SETMASK(), $old, POSIX::SigSet->new()) if defined $blocked;
175              
176 36         222 return ($ok, $err);
177             }
178              
179             sub send {
180 39     39 1 337 my $self = shift;
181 39         211 my ($hid, $e, $global) = @_;
182              
183 39         216 my $tempdir = $self->{+TEMPDIR};
184 39         326 my $hfile = $self->hub_file($hid);
185 39 100       322 my $dest = $global ? 'GLOBAL' : $hid;
186              
187 39 100 100     751 $self->abort(<<" EOT") unless $global || -f $hfile;
188             hub '$hid' is not available, failed to send event!
189              
190             There was an attempt to send an event to a hub in a parent process or thread,
191             but that hub appears to be gone. This can happen if you fork, or start a new
192             thread from inside subtest, and the parent finishes the subtest before the
193             child returns.
194              
195             This can also happen if the parent process is done testing before the child
196             finishes. Test2 normally waits automatically in the root process, but will not
197             do so if Test::Builder is loaded for legacy reasons.
198             EOT
199              
200 38         289 my $file = $self->event_file($dest, $e);
201 36         217 my $ready = File::Spec->canonpath("$file.ready");
202              
203 36 100       196 if ($global) {
204 21         59 my $name = $ready;
205 21         248 $name =~ s{^.*(GLOBAL)}{GLOBAL};
206 21         169 $self->{+GLOBALS}->{$hid}->{$name}++;
207             }
208              
209             # Write and rename the file.
210 36         149 my ($ren_ok, $ren_err);
211             my ($ok, $err) = $self->try_sig_mask(sub {
212 36     36   726 Storable::store($e, $file);
213 35         10970 ($ren_ok, $ren_err) = do_rename("$file", $ready);
214 36         565 });
215              
216 36 100       337 if ($ok) {
217 35 50       128 $self->abort("Could not rename file '$file' -> '$ready': $ren_err") unless $ren_ok;
218 35         314 test2_ipc_set_pending($file);
219             }
220             else {
221 1         4 my $src_file = __FILE__;
222 1         25 $err =~ s{ at \Q$src_file\E.*$}{};
223 1         4 chomp($err);
224 1         3 my $tid = get_tid();
225 1         9 my $trace = $e->trace->debug;
226 1         7 my $type = blessed($e);
227              
228 1         11 $self->abort(<<" EOT");
229              
230             *******************************************************************************
231             There was an error writing an event:
232             Destination: $dest
233             Origin PID: $$
234             Origin TID: $tid
235             Event Type: $type
236             Event Trace: $trace
237             File Name: $file
238             Ready Name: $ready
239             Error: $err
240             *******************************************************************************
241              
242             EOT
243             }
244              
245 35         136 return 1;
246             }
247              
248             sub driver_abort {
249 0     0 1 0 my $self = shift;
250 0         0 my ($msg) = @_;
251              
252 0         0 local ($@, $!, $?, $^E);
253 0 0       0 eval {
254 0         0 my $abort = File::Spec->catfile($self->{+TEMPDIR}, "ABORT");
255 0 0       0 open(my $fh, '>>', $abort) or die "Could not open abort file: $!";
256 0         0 print $fh $msg, "\n";
257 0 0       0 close($fh) or die "Could not close abort file: $!";
258 0         0 1;
259             } or warn $@;
260             }
261              
262             sub cull {
263 743     743 1 1400 my $self = shift;
264 743         1559 my ($hid) = @_;
265              
266 743         1471 my $tempdir = $self->{+TEMPDIR};
267              
268 743 50       27046 opendir(my $dh, $tempdir) or $self->abort("could not open IPC temp dir ($tempdir)!");
269              
270 743         2908 my $read = $self->{+READ_IDS};
271 743         1275 my $timeouts = $self->{+TIMEOUTS};
272              
273 743         1297 my @out;
274 743         14323 for my $info (sort cmp_events map { $self->should_read_event($hid, $_) } readdir($dh)) {
  2648         6933  
275 26 100       110 unless ($info->{global}) {
276 13   100     126 my $next = $self->{+READ_IDS}->{$info->{hid}}->{$info->{pid}}->{$info->{tid}} ||= 1;
277              
278 13   33     123 $timeouts->{$info->{file}} ||= time;
279              
280 13 50       49 if ($next != $info->{eid}) {
281             # Wait up to N seconds for missing events
282 0 0       0 next unless 5 < time - $timeouts->{$info->{file}};
283 0         0 $self->abort("Missing event HID: $info->{hid}, PID: $info->{pid}, TID: $info->{tid}, EID: $info->{eid}.");
284             }
285              
286 13         57 $self->{+READ_IDS}->{$info->{hid}}->{$info->{pid}}->{$info->{tid}} = $info->{eid} + 1;
287             }
288              
289 26         81 my $full = $info->{full_path};
290 26         116 my $obj = $self->read_event_file($full);
291 26         79 push @out => $obj;
292              
293             # Do not remove global events
294 26 100       97 next if $info->{global};
295              
296 13 100       56 if ($ENV{T2_KEEP_TEMPDIR}) {
297 1         9 my $complete = File::Spec->canonpath("$full.complete");
298 1         4 my ($ok, $err) = do_rename($full, $complete);
299 1 50       9 $self->abort("Could not rename IPC file '$full', '$complete': $err") unless $ok;
300             }
301             else {
302 12         78 my ($ok, $err) = do_unlink("$full");
303 12 50       89 $self->abort("Could not unlink IPC file '$full': $err") unless $ok;
304             }
305             }
306              
307 743         8845 closedir($dh);
308 743         6926 return @out;
309             }
310              
311             sub parse_event_filename {
312 130     130 0 261 my $self = shift;
313 130         263 my ($file) = @_;
314              
315             # The || is to force 0 in false
316 130 100 100     665 my $complete = substr($file, -9, 9) eq '.complete' || 0 and substr($file, -9, 9, "");
317 130 100 100     650 my $ready = substr($file, -6, 6) eq '.ready' || 0 and substr($file, -6, 6, "");
318              
319 130         569 my @parts = split ipc_separator, $file;
320 130 100       543 my ($global, $hid) = $parts[0] eq 'GLOBAL' ? (1, shift @parts) : (0, join ipc_separator, splice(@parts, 0, 4));
321 130         403 my ($pid, $tid, $eid) = splice(@parts, 0, 3);
322 130         357 my $type = join '::' => @parts;
323              
324             return {
325 130         1176 file => $file,
326             ready => $ready,
327             complete => $complete,
328             global => $global,
329             type => $type,
330             hid => $hid,
331             pid => $pid,
332             tid => $tid,
333             eid => $eid,
334             };
335             }
336              
337             sub should_read_event {
338 2661     2661 0 3950 my $self = shift;
339 2661         5229 my ($hid, $file) = @_;
340              
341 2661 100       8113 return if substr($file, 0, 1) eq '.';
342 1175 100       4038 return if substr($file, 0, 3) eq 'HUB';
343 123 50       332 CORE::exit(255) if $file eq 'ABORT';
344              
345 123         338 my $parsed = $self->parse_event_filename($file);
346              
347 123 100       359 return if $parsed->{complete};
348 120 100       280 return unless $parsed->{ready};
349 116 100 100     332 return unless $parsed->{global} || $parsed->{hid} eq $hid;
350              
351 115 100 100     811 return if $parsed->{global} && $self->{+GLOBALS}->{$hid}->{$file}++;
352              
353             # Untaint the path.
354 30         475 my $full = File::Spec->catfile($self->{+TEMPDIR}, $file);
355 30 50       260 ($full) = ($full =~ m/^(.*)$/gs) if ${^TAINT};
356              
357 30         129 $parsed->{full_path} = $full;
358              
359 30         175 return $parsed;
360             }
361              
362             sub cmp_events {
363             # Globals first
364 46 100 100 46 0 208 return -1 if $a->{global} && !$b->{global};
365 38 100 100     124 return 1 if $b->{global} && !$a->{global};
366              
367             return $a->{pid} <=> $b->{pid}
368             || $a->{tid} <=> $b->{tid}
369 35   66     221 || $a->{eid} <=> $b->{eid};
370             }
371              
372             sub read_event_file {
373 31     31 0 168 my $self = shift;
374 31         75 my ($file) = @_;
375              
376 31         211 my $obj = Storable::retrieve($file);
377 31 100       4136 $self->abort("Got an unblessed object: '$obj'")
378             unless blessed($obj);
379              
380 30 100       248 unless ($obj->isa('Test2::Event')) {
381 2         12 my $pkg = blessed($obj);
382 2         9 my $mod_file = pkg_to_file($pkg);
383 2     2   12 my ($ok, $err) = try { require $mod_file };
  2         298  
384              
385 2 100       19 $self->abort("Event has unknown type ($pkg), tried to load '$mod_file' but failed: $err")
386             unless $ok;
387              
388 1 50       18 $self->abort("'$obj' is not a 'Test2::Event' object")
389             unless $obj->isa('Test2::Event');
390             }
391              
392 28         88 return $obj;
393             }
394              
395             sub waiting {
396 18     18 1 63 my $self = shift;
397 18         168 require Test2::Event::Waiting;
398 18         690 $self->send(
399             GLOBAL => Test2::Event::Waiting->new(
400             trace => Test2::EventFacet::Trace->new(frame => [caller()]),
401             ),
402             'GLOBAL'
403             );
404 18         200 return;
405             }
406              
407             sub DESTROY {
408 58     58   240 my $self = shift;
409              
410 58 100       300 return unless defined $self->pid;
411 57 100       207 return unless defined $self->tid;
412              
413 56 100       203 return unless $$ == $self->pid;
414 54 100       150 return unless get_tid() == $self->tid;
415              
416 53         144 my $tempdir = $self->{+TEMPDIR};
417              
418 53         93 my $aborted = 0;
419 53         707 my $abort_file = File::Spec->catfile($self->{+TEMPDIR}, "ABORT");
420 53 50       1373 if (-e $abort_file) {
421 0         0 $aborted = 1;
422 0         0 my ($ok, $err) = do_unlink($abort_file);
423 0 0       0 warn $err unless $ok;
424             }
425              
426 53 50       1677 opendir(my $dh, $tempdir) or $self->abort("Could not open temp dir! ($tempdir)");
427 53         819 while(my $file = readdir($dh)) {
428 112 100       1145 next if $file =~ m/^\.+$/;
429 8 100       40 next if $file =~ m/\.complete$/;
430 6         78 my $full = File::Spec->catfile($tempdir, $file);
431              
432 6         18 my $sep = ipc_separator;
433 6 100 66     117 if ($aborted || $file =~ m/^(GLOBAL|HUB$sep)/) {
434 5         24 $full =~ m/^(.*)$/;
435 5         18 $full = $1; # Untaint it
436 5 100       20 next if $ENV{T2_KEEP_TEMPDIR};
437 4         16 my ($ok, $err) = do_unlink($full);
438 4 50       24 $self->abort("Could not unlink IPC file '$full': $err") unless $ok;
439 4         26 next;
440             }
441              
442 1         7 $self->abort("Leftover files in the directory ($full)!\n");
443             }
444 52         565 closedir($dh);
445              
446 52 100       260 if ($ENV{T2_KEEP_TEMPDIR}) {
447 1         11 print STDERR "# Not removing temp dir: $tempdir\n";
448 1         12 return;
449             }
450              
451 51         642 my $abort = File::Spec->catfile($self->{+TEMPDIR}, "ABORT");
452 51 50       642 unlink($abort) if -e $abort;
453 51 50       3309 rmdir($tempdir) or warn "Could not remove IPC temp dir ($tempdir)";
454             }
455              
456             1;
457              
458             __END__