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