File Coverage

blib/lib/Test2/IPC/Driver/Files.pm
Criterion Covered Total %
statement 228 246 92.6
branch 101 134 75.3
condition 28 38 73.6
subroutine 28 29 96.5
pod 7 14 50.0
total 392 461 85.0


line stmt bran cond sub pod time code
1             package Test2::IPC::Driver::Files;
2 30     30   2904 use strict;
  30         66  
  30         917  
3 30     30   157 use warnings;
  30         84  
  30         1719  
4              
5             our $VERSION = '1.302181';
6              
7 30     30   14487 BEGIN { require Test2::IPC::Driver; our @ISA = qw(Test2::IPC::Driver) }
  30         1418  
8              
9 30     30   219 use Test2::Util::HashBase qw{tempdir event_ids read_ids timeouts tid pid globals};
  30         190  
  30         142  
10              
11 30     30   215 use Scalar::Util qw/blessed/;
  30         99  
  30         1555  
12 30     30   22635 use File::Temp();
  30         526580  
  30         767  
13 30     30   20008 use Storable();
  30         97874  
  30         1024  
14 30     30   216 use File::Spec();
  30         66  
  30         445  
15 30     30   148 use POSIX();
  30         67  
  30         736  
16              
17 30     30   174 use Test2::Util qw/try get_tid pkg_to_file IS_WIN32 ipc_separator do_rename do_unlink try_sig_mask/;
  30         89  
  30         2599  
18 30     30   204 use Test2::API qw/test2_ipc_set_pending/;
  30         87  
  30         101181  
19              
20 57     57 1 381 sub is_viable { 1 }
21              
22             sub init {
23 83     83 0 176 my $self = shift;
24              
25             my $tmpdir = File::Temp::tempdir(
26 83   33     945 $ENV{T2_TEMPDIR_TEMPLATE} || "test2" . ipc_separator . $$ . ipc_separator . "XXXXXX",
27             CLEANUP => 0,
28             TMPDIR => 1,
29             );
30              
31 83 50       37699 $self->abort_trace("Could not get a temp dir") unless $tmpdir;
32              
33 83         705 $self->{+TEMPDIR} = File::Spec->canonpath($tmpdir);
34              
35             print STDERR "\nIPC Temp Dir: $tmpdir\n\n"
36 83 100       299 if $ENV{T2_KEEP_TEMPDIR};
37              
38 83         260 $self->{+EVENT_IDS} = {};
39 83         225 $self->{+READ_IDS} = {};
40 83         177 $self->{+TIMEOUTS} = {};
41              
42 83         208 $self->{+TID} = get_tid();
43 83         289 $self->{+PID} = $$;
44              
45 83         171 $self->{+GLOBALS} = {};
46              
47 83         251 return $self;
48             }
49              
50             sub hub_file {
51 187     187 0 334 my $self = shift;
52 187         350 my ($hid) = @_;
53 187         407 my $tdir = $self->{+TEMPDIR};
54 187         3190 return File::Spec->catfile($tdir, "HUB" . ipc_separator . $hid);
55             }
56              
57             sub event_file {
58 38     38 0 121 my $self = shift;
59 38         186 my ($hid, $e) = @_;
60              
61 38         177 my $tempdir = $self->{+TEMPDIR};
62 38 100       356 my $type = blessed($e) or $self->abort("'$e' is not a blessed object!");
63              
64 37 100       580 $self->abort("'$e' is not an event object!")
65             unless $type->isa('Test2::Event');
66              
67 36         103 my $tid = get_tid();
68 36         380 my $eid = $self->{+EVENT_IDS}->{$hid}->{$$}->{$tid} += 1;
69              
70 36         258 my @type = split '::', $type;
71 36         250 my $name = join(ipc_separator, $hid, $$, $tid, $eid, @type);
72              
73 36         430 return File::Spec->catfile($tempdir, $name);
74             }
75              
76             sub add_hub {
77 92     92 1 234 my $self = shift;
78 92         213 my ($hid) = @_;
79              
80 92         266 my $hfile = $self->hub_file($hid);
81              
82 92 100       2298 $self->abort_trace("File for hub '$hid' already exists")
83             if -e $hfile;
84              
85 91 50       5539 open(my $fh, '>', $hfile) or $self->abort_trace("Could not create hub file '$hid': $!");
86 91         1592 print $fh "$$\n" . get_tid() . "\n";
87 91         3908 close($fh);
88             }
89              
90             sub drop_hub {
91 56     56 1 164 my $self = shift;
92 56         140 my ($hid) = @_;
93              
94 56         185 my $tdir = $self->{+TEMPDIR};
95 56         175 my $hfile = $self->hub_file($hid);
96              
97 56 100       1070 $self->abort_trace("File for hub '$hid' does not exist")
98             unless -e $hfile;
99              
100 55 50       1970 open(my $fh, '<', $hfile) or $self->abort_trace("Could not open hub file '$hid': $!");
101 55         1565 my ($pid, $tid) = <$fh>;
102 55         572 close($fh);
103              
104 55 50       340 $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       202 $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       208 if ($ENV{T2_KEEP_TEMPDIR}) {
111 1         11 my ($ok, $err) = do_rename($hfile, File::Spec->canonpath("$hfile.complete"));
112 1 50       7 $self->abort_trace("Could not rename file '$hfile' -> '$hfile.complete': $err") unless $ok
113             }
114             else {
115 54         302 my ($ok, $err) = do_unlink($hfile);
116 54 50       283 $self->abort_trace("Could not remove file for hub '$hid': $err") unless $ok
117             }
118              
119 55 50       1597 opendir(my $dh, $tdir) or $self->abort_trace("Could not open temp dir!");
120              
121 55         166 my %bad;
122 55         973 for my $file (readdir($dh)) {
123 169 100       529 next if $file =~ m{\.complete$};
124 167 100       1641 next unless $file =~ m{^$hid};
125              
126 1 50 0     4 eval { $bad{$file} = $self->read_event_file(File::Spec->catfile($tdir, $file)); 1 } or $bad{$file} = $@ || "Unknown error reading file";
  1         14  
  1         6  
127             }
128 55         600 closedir($dh);
129              
130 55 100       1932 return unless keys %bad;
131              
132 1         4 my $data;
133 1         2 my $ok = eval {
134 1         681 require JSON::PP;
135 1     1   14053 local *UNIVERSAL::TO_JSON = sub { +{ %{$_[0]} } };
  1         153  
  1         5  
136 1         5 my $json = JSON::PP->new->ascii->pretty->canonical->allow_unknown->allow_blessed->convert_blessed;
137 1         182 $data = $json->encode(\%bad);
138 1         128 1;
139             };
140 1   33     4 $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       4 $data = "Could not dump data... sorry." unless defined $data;
148              
149 1         9 $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 send {
153 39     39 1 324 my $self = shift;
154 39         276 my ($hid, $e, $global) = @_;
155              
156 39         225 my $tempdir = $self->{+TEMPDIR};
157 39         311 my $hfile = $self->hub_file($hid);
158 39 100       308 my $dest = $global ? 'GLOBAL' : $hid;
159              
160 39 100 100     845 $self->abort(<<" EOT") unless $global || -f $hfile;
161             hub '$hid' is not available, failed to send event!
162              
163             There was an attempt to send an event to a hub in a parent process or thread,
164             but that hub appears to be gone. This can happen if you fork, or start a new
165             thread from inside subtest, and the parent finishes the subtest before the
166             child returns.
167              
168             This can also happen if the parent process is done testing before the child
169             finishes. Test2 normally waits automatically in the root process, but will not
170             do so if Test::Builder is loaded for legacy reasons.
171             EOT
172              
173 38         225 my $file = $self->event_file($dest, $e);
174 36         228 my $ready = File::Spec->canonpath("$file.ready");
175              
176 36 100       225 if ($global) {
177 21         78 my $name = $ready;
178 21         275 $name =~ s{^.*(GLOBAL)}{GLOBAL};
179 21         118 $self->{+GLOBALS}->{$hid}->{$name}++;
180             }
181              
182             # Write and rename the file.
183 36         139 my ($ren_ok, $ren_err);
184             my ($ok, $err) = try_sig_mask(sub {
185 36     36   464 Storable::store($e, $file);
186 35         11084 ($ren_ok, $ren_err) = do_rename("$file", $ready);
187 36         668 });
188              
189 36 100       337 if ($ok) {
190 35 50       165 $self->abort("Could not rename file '$file' -> '$ready': $ren_err") unless $ren_ok;
191 35         321 test2_ipc_set_pending($file);
192             }
193             else {
194 1         3 my $src_file = __FILE__;
195 1         25 $err =~ s{ at \Q$src_file\E.*$}{};
196 1         3 chomp($err);
197 1         2 my $tid = get_tid();
198 1         8 my $trace = $e->trace->debug;
199 1         4 my $type = blessed($e);
200              
201 1         9 $self->abort(<<" EOT");
202              
203             *******************************************************************************
204             There was an error writing an event:
205             Destination: $dest
206             Origin PID: $$
207             Origin TID: $tid
208             Event Type: $type
209             Event Trace: $trace
210             File Name: $file
211             Ready Name: $ready
212             Error: $err
213             *******************************************************************************
214              
215             EOT
216             }
217              
218 35         126 return 1;
219             }
220              
221             sub driver_abort {
222 0     0 1 0 my $self = shift;
223 0         0 my ($msg) = @_;
224              
225 0         0 local ($@, $!, $?, $^E);
226 0 0       0 eval {
227 0         0 my $abort = File::Spec->catfile($self->{+TEMPDIR}, "ABORT");
228 0 0       0 open(my $fh, '>>', $abort) or die "Could not open abort file: $!";
229 0         0 print $fh $msg, "\n";
230 0 0       0 close($fh) or die "Could not close abort file: $!";
231 0         0 1;
232             } or warn $@;
233             }
234              
235             sub cull {
236 743     743 1 1282 my $self = shift;
237 743         1405 my ($hid) = @_;
238              
239 743         1523 my $tempdir = $self->{+TEMPDIR};
240              
241 743 50       25259 opendir(my $dh, $tempdir) or $self->abort("could not open IPC temp dir ($tempdir)!");
242              
243 743         2812 my $read = $self->{+READ_IDS};
244 743         1232 my $timeouts = $self->{+TIMEOUTS};
245              
246 743         1288 my @out;
247 743         14404 for my $info (sort cmp_events map { $self->should_read_event($hid, $_) } readdir($dh)) {
  2648         6476  
248 26 100       107 unless ($info->{global}) {
249 13   100     137 my $next = $self->{+READ_IDS}->{$info->{hid}}->{$info->{pid}}->{$info->{tid}} ||= 1;
250              
251 13   33     141 $timeouts->{$info->{file}} ||= time;
252              
253 13 50       43 if ($next != $info->{eid}) {
254             # Wait up to N seconds for missing events
255 0 0       0 next unless 5 < time - $timeouts->{$info->{file}};
256 0         0 $self->abort("Missing event HID: $info->{hid}, PID: $info->{pid}, TID: $info->{tid}, EID: $info->{eid}.");
257             }
258              
259 13         60 $self->{+READ_IDS}->{$info->{hid}}->{$info->{pid}}->{$info->{tid}} = $info->{eid} + 1;
260             }
261              
262 26         93 my $full = $info->{full_path};
263 26         124 my $obj = $self->read_event_file($full);
264 26         92 push @out => $obj;
265              
266             # Do not remove global events
267 26 100       129 next if $info->{global};
268              
269 13 100       47 if ($ENV{T2_KEEP_TEMPDIR}) {
270 1         9 my $complete = File::Spec->canonpath("$full.complete");
271 1         4 my ($ok, $err) = do_rename($full, $complete);
272 1 50       7 $self->abort("Could not rename IPC file '$full', '$complete': $err") unless $ok;
273             }
274             else {
275 12         88 my ($ok, $err) = do_unlink("$full");
276 12 50       70 $self->abort("Could not unlink IPC file '$full': $err") unless $ok;
277             }
278             }
279              
280 743         9012 closedir($dh);
281 743         6708 return @out;
282             }
283              
284             sub parse_event_filename {
285 130     130 0 244 my $self = shift;
286 130         254 my ($file) = @_;
287              
288             # The || is to force 0 in false
289 130 100 100     704 my $complete = substr($file, -9, 9) eq '.complete' || 0 and substr($file, -9, 9, "");
290 130 100 100     591 my $ready = substr($file, -6, 6) eq '.ready' || 0 and substr($file, -6, 6, "");
291              
292 130         600 my @parts = split ipc_separator, $file;
293 130 100       531 my ($global, $hid) = $parts[0] eq 'GLOBAL' ? (1, shift @parts) : (0, join ipc_separator, splice(@parts, 0, 4));
294 130         385 my ($pid, $tid, $eid) = splice(@parts, 0, 3);
295 130         348 my $type = join '::' => @parts;
296              
297             return {
298 130         1305 file => $file,
299             ready => $ready,
300             complete => $complete,
301             global => $global,
302             type => $type,
303             hid => $hid,
304             pid => $pid,
305             tid => $tid,
306             eid => $eid,
307             };
308             }
309              
310             sub should_read_event {
311 2661     2661 0 4145 my $self = shift;
312 2661         5130 my ($hid, $file) = @_;
313              
314 2661 100       8161 return if substr($file, 0, 1) eq '.';
315 1175 100       4303 return if substr($file, 0, 3) eq 'HUB';
316 123 50       358 CORE::exit(255) if $file eq 'ABORT';
317              
318 123         386 my $parsed = $self->parse_event_filename($file);
319              
320 123 100       376 return if $parsed->{complete};
321 120 100       290 return unless $parsed->{ready};
322 116 100 100     366 return unless $parsed->{global} || $parsed->{hid} eq $hid;
323              
324 115 100 100     751 return if $parsed->{global} && $self->{+GLOBALS}->{$hid}->{$file}++;
325              
326             # Untaint the path.
327 30         519 my $full = File::Spec->catfile($self->{+TEMPDIR}, $file);
328 30 50       265 ($full) = ($full =~ m/^(.*)$/gs) if ${^TAINT};
329              
330 30         129 $parsed->{full_path} = $full;
331              
332 30         190 return $parsed;
333             }
334              
335             sub cmp_events {
336             # Globals first
337 50 100 100 50 0 189 return -1 if $a->{global} && !$b->{global};
338 42 100 100     132 return 1 if $b->{global} && !$a->{global};
339              
340             return $a->{pid} <=> $b->{pid}
341             || $a->{tid} <=> $b->{tid}
342 38   66     202 || $a->{eid} <=> $b->{eid};
343             }
344              
345             sub read_event_file {
346 31     31 0 157 my $self = shift;
347 31         84 my ($file) = @_;
348              
349 31         171 my $obj = Storable::retrieve($file);
350 31 100       4420 $self->abort("Got an unblessed object: '$obj'")
351             unless blessed($obj);
352              
353 30 100       267 unless ($obj->isa('Test2::Event')) {
354 2         7 my $pkg = blessed($obj);
355 2         8 my $mod_file = pkg_to_file($pkg);
356 2     2   12 my ($ok, $err) = try { require $mod_file };
  2         215  
357              
358 2 100       22 $self->abort("Event has unknown type ($pkg), tried to load '$mod_file' but failed: $err")
359             unless $ok;
360              
361 1 50       15 $self->abort("'$obj' is not a 'Test2::Event' object")
362             unless $obj->isa('Test2::Event');
363             }
364              
365 28         99 return $obj;
366             }
367              
368             sub waiting {
369 18     18 1 78 my $self = shift;
370 18         178 require Test2::Event::Waiting;
371 18         742 $self->send(
372             GLOBAL => Test2::Event::Waiting->new(
373             trace => Test2::EventFacet::Trace->new(frame => [caller()]),
374             ),
375             'GLOBAL'
376             );
377 18         228 return;
378             }
379              
380             sub DESTROY {
381 58     58   232 my $self = shift;
382              
383 58 100       301 return unless defined $self->pid;
384 57 100       240 return unless defined $self->tid;
385              
386 56 100       156 return unless $$ == $self->pid;
387 54 100       143 return unless get_tid() == $self->tid;
388              
389 53         109 my $tempdir = $self->{+TEMPDIR};
390              
391 53         107 my $aborted = 0;
392 53         688 my $abort_file = File::Spec->catfile($self->{+TEMPDIR}, "ABORT");
393 53 50       1493 if (-e $abort_file) {
394 0         0 $aborted = 1;
395 0         0 my ($ok, $err) = do_unlink($abort_file);
396 0 0       0 warn $err unless $ok;
397             }
398              
399 53 50       1592 opendir(my $dh, $tempdir) or $self->abort("Could not open temp dir! ($tempdir)");
400 53         800 while(my $file = readdir($dh)) {
401 114 100       1017 next if $file =~ m/^\.+$/;
402 8 100       30 next if $file =~ m/\.complete$/;
403 6         69 my $full = File::Spec->catfile($tempdir, $file);
404              
405 6         15 my $sep = ipc_separator;
406 6 100 66     102 if ($aborted || $file =~ m/^(GLOBAL|HUB$sep)/) {
407 5         18 $full =~ m/^(.*)$/;
408 5         17 $full = $1; # Untaint it
409 5 100       21 next if $ENV{T2_KEEP_TEMPDIR};
410 4         14 my ($ok, $err) = do_unlink($full);
411 4 50       21 $self->abort("Could not unlink IPC file '$full': $err") unless $ok;
412 4         29 next;
413             }
414              
415 1         6 $self->abort("Leftover files in the directory ($full)!\n");
416             }
417 52         578 closedir($dh);
418              
419 52 100       287 if ($ENV{T2_KEEP_TEMPDIR}) {
420 1         7 print STDERR "# Not removing temp dir: $tempdir\n";
421 1         9 return;
422             }
423              
424 51         645 my $abort = File::Spec->catfile($self->{+TEMPDIR}, "ABORT");
425 51 50       593 unlink($abort) if -e $abort;
426 51 50       3363 rmdir($tempdir) or warn "Could not remove IPC temp dir ($tempdir)";
427             }
428              
429             1;
430              
431             __END__
432              
433             =pod
434              
435             =encoding UTF-8
436              
437             =head1 NAME
438              
439             Test2::IPC::Driver::Files - Temp dir + Files concurrency model.
440              
441             =head1 DESCRIPTION
442              
443             This is the default, and fallback concurrency model for L<Test2>. This
444             sends events between processes and threads using serialized files in a
445             temporary directory. This is not particularly fast, but it works everywhere.
446              
447             =head1 SYNOPSIS
448              
449             use Test2::IPC::Driver::Files;
450              
451             # IPC is now enabled
452              
453             =head1 ENVIRONMENT VARIABLES
454              
455             =over 4
456              
457             =item T2_KEEP_TEMPDIR=0
458              
459             When true, the tempdir used by the IPC driver will not be deleted when the test
460             is done.
461              
462             =item T2_TEMPDIR_TEMPLATE='test2-XXXXXX'
463              
464             This can be used to set the template for the IPC temp dir. The template should
465             follow template specifications from L<File::Temp>.
466              
467             =back
468              
469             =head1 SEE ALSO
470              
471             See L<Test2::IPC::Driver> for methods.
472              
473             =head1 SOURCE
474              
475             The source code repository for Test2 can be found at
476             F<http://github.com/Test-More/test-more/>.
477              
478             =head1 MAINTAINERS
479              
480             =over 4
481              
482             =item Chad Granum E<lt>exodist@cpan.orgE<gt>
483              
484             =back
485              
486             =head1 AUTHORS
487              
488             =over 4
489              
490             =item Chad Granum E<lt>exodist@cpan.orgE<gt>
491              
492             =back
493              
494             =head1 COPYRIGHT
495              
496             Copyright 2020 Chad Granum E<lt>exodist@cpan.orgE<gt>.
497              
498             This program is free software; you can redistribute it and/or
499             modify it under the same terms as Perl itself.
500              
501             See F<http://dev.perl.org/licenses/>
502              
503             =cut