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   2923 use strict;
  30         66  
  30         922  
3 30     30   164 use warnings;
  30         64  
  30         1733  
4              
5             our $VERSION = '1.302182';
6              
7 30     30   13762 BEGIN { require Test2::IPC::Driver; our @ISA = qw(Test2::IPC::Driver) }
  30         1367  
8              
9 30     30   224 use Test2::Util::HashBase qw{tempdir event_ids read_ids timeouts tid pid globals};
  30         64  
  30         136  
10              
11 30     30   207 use Scalar::Util qw/blessed/;
  30         123  
  30         1569  
12 30     30   21142 use File::Temp();
  30         514926  
  30         851  
13 30     30   19280 use Storable();
  30         97702  
  30         761  
14 30     30   222 use File::Spec();
  30         68  
  30         478  
15 30     30   145 use POSIX();
  30         72  
  30         708  
16              
17 30     30   158 use Test2::Util qw/try get_tid pkg_to_file IS_WIN32 ipc_separator do_rename do_unlink try_sig_mask/;
  30         67  
  30         2684  
18 30     30   198 use Test2::API qw/test2_ipc_set_pending/;
  30         66  
  30         98621  
19              
20 57     57 1 432 sub is_viable { 1 }
21              
22             sub init {
23 83     83 0 190 my $self = shift;
24              
25             my $tmpdir = File::Temp::tempdir(
26 83   33     1045 $ENV{T2_TEMPDIR_TEMPLATE} || "test2" . ipc_separator . $$ . ipc_separator . "XXXXXX",
27             CLEANUP => 0,
28             TMPDIR => 1,
29             );
30              
31 83 50       39193 $self->abort_trace("Could not get a temp dir") unless $tmpdir;
32              
33 83         657 $self->{+TEMPDIR} = File::Spec->canonpath($tmpdir);
34              
35             print STDERR "\nIPC Temp Dir: $tmpdir\n\n"
36 83 100       321 if $ENV{T2_KEEP_TEMPDIR};
37              
38 83         250 $self->{+EVENT_IDS} = {};
39 83         199 $self->{+READ_IDS} = {};
40 83         527 $self->{+TIMEOUTS} = {};
41              
42 83         240 $self->{+TID} = get_tid();
43 83         296 $self->{+PID} = $$;
44              
45 83         199 $self->{+GLOBALS} = {};
46              
47 83         242 return $self;
48             }
49              
50             sub hub_file {
51 187     187 0 335 my $self = shift;
52 187         354 my ($hid) = @_;
53 187         368 my $tdir = $self->{+TEMPDIR};
54 187         3244 return File::Spec->catfile($tdir, "HUB" . ipc_separator . $hid);
55             }
56              
57             sub event_file {
58 38     38 0 132 my $self = shift;
59 38         186 my ($hid, $e) = @_;
60              
61 38         135 my $tempdir = $self->{+TEMPDIR};
62 38 100       441 my $type = blessed($e) or $self->abort("'$e' is not a blessed object!");
63              
64 37 100       640 $self->abort("'$e' is not an event object!")
65             unless $type->isa('Test2::Event');
66              
67 36         126 my $tid = get_tid();
68 36         356 my $eid = $self->{+EVENT_IDS}->{$hid}->{$$}->{$tid} += 1;
69              
70 36         276 my @type = split '::', $type;
71 36         207 my $name = join(ipc_separator, $hid, $$, $tid, $eid, @type);
72              
73 36         505 return File::Spec->catfile($tempdir, $name);
74             }
75              
76             sub add_hub {
77 92     92 1 219 my $self = shift;
78 92         200 my ($hid) = @_;
79              
80 92         292 my $hfile = $self->hub_file($hid);
81              
82 92 100       2184 $self->abort_trace("File for hub '$hid' already exists")
83             if -e $hfile;
84              
85 91 50       11122 open(my $fh, '>', $hfile) or $self->abort_trace("Could not create hub file '$hid': $!");
86 91         1637 print $fh "$$\n" . get_tid() . "\n";
87 91         3919 close($fh);
88             }
89              
90             sub drop_hub {
91 56     56 1 179 my $self = shift;
92 56         140 my ($hid) = @_;
93              
94 56         122 my $tdir = $self->{+TEMPDIR};
95 56         196 my $hfile = $self->hub_file($hid);
96              
97 56 100       1059 $self->abort_trace("File for hub '$hid' does not exist")
98             unless -e $hfile;
99              
100 55 50       2072 open(my $fh, '<', $hfile) or $self->abort_trace("Could not open hub file '$hid': $!");
101 55         1528 my ($pid, $tid) = <$fh>;
102 55         589 close($fh);
103              
104 55 50       354 $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       191 $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       230 if ($ENV{T2_KEEP_TEMPDIR}) {
111 1         10 my ($ok, $err) = do_rename($hfile, File::Spec->canonpath("$hfile.complete"));
112 1 50       6 $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       284 $self->abort_trace("Could not remove file for hub '$hid': $err") unless $ok
117             }
118              
119 55 50       1602 opendir(my $dh, $tdir) or $self->abort_trace("Could not open temp dir!");
120              
121 55         214 my %bad;
122 55         999 for my $file (readdir($dh)) {
123 169 100       586 next if $file =~ m{\.complete$};
124 167 100       1609 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         17  
  1         7  
127             }
128 55         623 closedir($dh);
129              
130 55 100       1987 return unless keys %bad;
131              
132 1         3 my $data;
133 1         2 my $ok = eval {
134 1         835 require JSON::PP;
135 1     1   14833 local *UNIVERSAL::TO_JSON = sub { +{ %{$_[0]} } };
  1         164  
  1         5  
136 1         6 my $json = JSON::PP->new->ascii->pretty->canonical->allow_unknown->allow_blessed->convert_blessed;
137 1         193 $data = $json->encode(\%bad);
138 1         160 1;
139             };
140 1   33     7 $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         56 $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 306 my $self = shift;
154 39         271 my ($hid, $e, $global) = @_;
155              
156 39         219 my $tempdir = $self->{+TEMPDIR};
157 39         375 my $hfile = $self->hub_file($hid);
158 39 100       338 my $dest = $global ? 'GLOBAL' : $hid;
159              
160 39 100 100     852 $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         290 my $file = $self->event_file($dest, $e);
174 36         228 my $ready = File::Spec->canonpath("$file.ready");
175              
176 36 100       168 if ($global) {
177 21         84 my $name = $ready;
178 21         281 $name =~ s{^.*(GLOBAL)}{GLOBAL};
179 21         120 $self->{+GLOBALS}->{$hid}->{$name}++;
180             }
181              
182             # Write and rename the file.
183 36         137 my ($ren_ok, $ren_err);
184             my ($ok, $err) = try_sig_mask(sub {
185 36     36   508 Storable::store($e, $file);
186 35         11734 ($ren_ok, $ren_err) = do_rename("$file", $ready);
187 36         693 });
188              
189 36 100       357 if ($ok) {
190 35 50       138 $self->abort("Could not rename file '$file' -> '$ready': $ren_err") unless $ren_ok;
191 35         374 test2_ipc_set_pending($file);
192             }
193             else {
194 1         3 my $src_file = __FILE__;
195 1         24 $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         5 my $type = blessed($e);
200              
201 1         8 $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         131 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 1289 my $self = shift;
237 743         1586 my ($hid) = @_;
238              
239 743         1474 my $tempdir = $self->{+TEMPDIR};
240              
241 743 50       24768 opendir(my $dh, $tempdir) or $self->abort("could not open IPC temp dir ($tempdir)!");
242              
243 743         2842 my $read = $self->{+READ_IDS};
244 743         1362 my $timeouts = $self->{+TIMEOUTS};
245              
246 743         1419 my @out;
247 743         13846 for my $info (sort cmp_events map { $self->should_read_event($hid, $_) } readdir($dh)) {
  2648         7169  
248 26 100       117 unless ($info->{global}) {
249 13   100     127 my $next = $self->{+READ_IDS}->{$info->{hid}}->{$info->{pid}}->{$info->{tid}} ||= 1;
250              
251 13   33     116 $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         52 $self->{+READ_IDS}->{$info->{hid}}->{$info->{pid}}->{$info->{tid}} = $info->{eid} + 1;
260             }
261              
262 26         68 my $full = $info->{full_path};
263 26         113 my $obj = $self->read_event_file($full);
264 26         66 push @out => $obj;
265              
266             # Do not remove global events
267 26 100       112 next if $info->{global};
268              
269 13 100       46 if ($ENV{T2_KEEP_TEMPDIR}) {
270 1         15 my $complete = File::Spec->canonpath("$full.complete");
271 1         4 my ($ok, $err) = do_rename($full, $complete);
272 1 50       6 $self->abort("Could not rename IPC file '$full', '$complete': $err") unless $ok;
273             }
274             else {
275 12         91 my ($ok, $err) = do_unlink("$full");
276 12 50       72 $self->abort("Could not unlink IPC file '$full': $err") unless $ok;
277             }
278             }
279              
280 743         9171 closedir($dh);
281 743         6857 return @out;
282             }
283              
284             sub parse_event_filename {
285 130     130 0 246 my $self = shift;
286 130         240 my ($file) = @_;
287              
288             # The || is to force 0 in false
289 130 100 100     836 my $complete = substr($file, -9, 9) eq '.complete' || 0 and substr($file, -9, 9, "");
290 130 100 100     571 my $ready = substr($file, -6, 6) eq '.ready' || 0 and substr($file, -6, 6, "");
291              
292 130         582 my @parts = split ipc_separator, $file;
293 130 100       477 my ($global, $hid) = $parts[0] eq 'GLOBAL' ? (1, shift @parts) : (0, join ipc_separator, splice(@parts, 0, 4));
294 130         440 my ($pid, $tid, $eid) = splice(@parts, 0, 3);
295 130         382 my $type = join '::' => @parts;
296              
297             return {
298 130         1159 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 3928 my $self = shift;
312 2661         5321 my ($hid, $file) = @_;
313              
314 2661 100       7934 return if substr($file, 0, 1) eq '.';
315 1175 100       4361 return if substr($file, 0, 3) eq 'HUB';
316 123 50       324 CORE::exit(255) if $file eq 'ABORT';
317              
318 123         409 my $parsed = $self->parse_event_filename($file);
319              
320 123 100       420 return if $parsed->{complete};
321 120 100       306 return unless $parsed->{ready};
322 116 100 100     347 return unless $parsed->{global} || $parsed->{hid} eq $hid;
323              
324 115 100 100     875 return if $parsed->{global} && $self->{+GLOBALS}->{$hid}->{$file}++;
325              
326             # Untaint the path.
327 30         489 my $full = File::Spec->catfile($self->{+TEMPDIR}, $file);
328 30 50       227 ($full) = ($full =~ m/^(.*)$/gs) if ${^TAINT};
329              
330 30         95 $parsed->{full_path} = $full;
331              
332 30         171 return $parsed;
333             }
334              
335             sub cmp_events {
336             # Globals first
337 49 100 100 49 0 198 return -1 if $a->{global} && !$b->{global};
338 45 100 100     138 return 1 if $b->{global} && !$a->{global};
339              
340             return $a->{pid} <=> $b->{pid}
341             || $a->{tid} <=> $b->{tid}
342 39   66     239 || $a->{eid} <=> $b->{eid};
343             }
344              
345             sub read_event_file {
346 31     31 0 158 my $self = shift;
347 31         162 my ($file) = @_;
348              
349 31         188 my $obj = Storable::retrieve($file);
350 31 100       3907 $self->abort("Got an unblessed object: '$obj'")
351             unless blessed($obj);
352              
353 30 100       238 unless ($obj->isa('Test2::Event')) {
354 2         13 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         278  
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       10 $self->abort("'$obj' is not a 'Test2::Event' object")
362             unless $obj->isa('Test2::Event');
363             }
364              
365 28         81 return $obj;
366             }
367              
368             sub waiting {
369 18     18 1 52 my $self = shift;
370 18         204 require Test2::Event::Waiting;
371 18         782 $self->send(
372             GLOBAL => Test2::Event::Waiting->new(
373             trace => Test2::EventFacet::Trace->new(frame => [caller()]),
374             ),
375             'GLOBAL'
376             );
377 18         239 return;
378             }
379              
380             sub DESTROY {
381 58     58   245 my $self = shift;
382              
383 58 100       282 return unless defined $self->pid;
384 57 100       222 return unless defined $self->tid;
385              
386 56 100       170 return unless $$ == $self->pid;
387 54 100       206 return unless get_tid() == $self->tid;
388              
389 53         133 my $tempdir = $self->{+TEMPDIR};
390              
391 53         98 my $aborted = 0;
392 53         691 my $abort_file = File::Spec->catfile($self->{+TEMPDIR}, "ABORT");
393 53 50       1352 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       1647 opendir(my $dh, $tempdir) or $self->abort("Could not open temp dir! ($tempdir)");
400 53         774 while(my $file = readdir($dh)) {
401 114 100       1025 next if $file =~ m/^\.+$/;
402 8 100       35 next if $file =~ m/\.complete$/;
403 6         67 my $full = File::Spec->catfile($tempdir, $file);
404              
405 6         19 my $sep = ipc_separator;
406 6 100 66     105 if ($aborted || $file =~ m/^(GLOBAL|HUB$sep)/) {
407 5         23 $full =~ m/^(.*)$/;
408 5         19 $full = $1; # Untaint it
409 5 100       20 next if $ENV{T2_KEEP_TEMPDIR};
410 4         16 my ($ok, $err) = do_unlink($full);
411 4 50       20 $self->abort("Could not unlink IPC file '$full': $err") unless $ok;
412 4         26 next;
413             }
414              
415 1         7 $self->abort("Leftover files in the directory ($full)!\n");
416             }
417 52         642 closedir($dh);
418              
419 52 100       253 if ($ENV{T2_KEEP_TEMPDIR}) {
420 1         6 print STDERR "# Not removing temp dir: $tempdir\n";
421 1         9 return;
422             }
423              
424 51         655 my $abort = File::Spec->catfile($self->{+TEMPDIR}, "ABORT");
425 51 50       593 unlink($abort) if -e $abort;
426 51 50       3660 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