-# Copyright (C) 2015-2019 all contributors <meta@public-inbox.org>
+# Copyright (C) 2015-2020 all contributors <meta@public-inbox.org>
# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
# internal APIs used only for tests
use Fcntl qw(FD_CLOEXEC F_SETFD F_GETFD :seek);
use POSIX qw(dup2);
use IO::Socket::INET;
-our @EXPORT = qw(tmpdir tcp_server tcp_connect require_git
- run_script start_script key2sub);
+our @EXPORT = qw(tmpdir tcp_server tcp_connect require_git require_mods
+ run_script start_script key2sub xsys xqx mime_load);
+
+sub mime_load ($;&) {
+ my ($path, $cb) = @_;
+ if (open(my $fh, '<', $path)) {
+ PublicInbox::MIME->new(\(do { local $/; <$fh> }));
+ } elsif ($cb) {
+ require File::Temp;
+
+ my $mime = $cb->();
+ my ($dir) = ($path =~ m!(.+)/(?:[^/]+)\z!);
+ -d $dir or die "BUG: dir=$dir is not the dir of $path";
+ my $fh = File::Temp->new(DIR => $dir);
+ $fh->autoflush(1);
+ print $fh $mime->as_string or die "print: $!";
+ my $fn = $fh->filename;
+ rename($fn, $path) or die "link $fn => $path: $!";
+ $fh->unlink_on_destroy(0);
+ pop @_; # retry via tail recursion
+ goto &mime_load;
+ } else {
+ die "open $path: $!";
+ }
+}
sub tmpdir (;$) {
my ($base) = @_;
my $cur_int = ($cur_maj << 24) | ($cur_min << 16);
if ($cur_int < $req_int) {
return 0 if $maybe;
- plan(skip_all => "git $req+ required, have $cur_maj.$cur_min");
+ Test::More::plan(skip_all =>
+ "git $req+ required, have $cur_maj.$cur_min");
}
1;
}
+sub require_mods {
+ my @mods = @_;
+ my $maybe = pop @mods if $mods[-1] =~ /\A[0-9]+\z/;
+ my @need;
+ for my $mod (@mods) {
+ if ($mod eq 'Search::Xapian') {
+ if (eval { require PublicInbox::Search } &&
+ PublicInbox::Search::load_xapian()) {
+ next;
+ }
+ } elsif ($mod eq 'Search::Xapian::WritableDatabase') {
+ if (eval { require PublicInbox::SearchIdx } &&
+ PublicInbox::SearchIdx::load_xapian_writable()){
+ next;
+ }
+ } else {
+ eval "require $mod";
+ }
+ push @need, $mod if $@;
+ }
+ return unless @need;
+ my $m = join(', ', @need)." missing for $0";
+ Test::More::skip($m, $maybe) if $maybe;
+ Test::More::plan(skip_all => $m)
+}
+
sub key2script ($) {
my ($key) = @_;
- return $key if (index($key, '/') >= 0);
+ return $key if ($key eq 'git' || index($key, '/') >= 0);
# n.b. we may have scripts which don't start with "public-inbox" in
# the future:
$key =~ s/\A([-\.])/public-inbox$1/;
'blib/script/'.$key;
}
+my @io_mode = ([ *STDIN{IO}, '<&' ], [ *STDOUT{IO}, '>&' ],
+ [ *STDERR{IO}, '>&' ]);
+
sub _prepare_redirects ($) {
my ($fhref) = @_;
- my @x = ([ \*STDIN, '<&' ], [ \*STDOUT, '>&' ], [ \*STDERR, '>&' ]);
- for (my $fd = 0; $fd <= $#x; $fd++) {
+ my $orig_io = [];
+ for (my $fd = 0; $fd <= $#io_mode; $fd++) {
my $fh = $fhref->[$fd] or next;
- my ($oldfh, $mode) = @{$x[$fd]};
+ my ($oldfh, $mode) = @{$io_mode[$fd]};
+ open my $orig, $mode, $oldfh or die "$$oldfh $mode stash: $!";
+ $orig_io->[$fd] = $orig;
open $oldfh, $mode, $fh or die "$$oldfh $mode redirect: $!";
}
+ $orig_io;
}
-# $opt->{run_mode} (or $ENV{TEST_RUN_MODE}) allows chosing between
+sub _undo_redirects ($) {
+ my ($orig_io) = @_;
+ for (my $fd = 0; $fd <= $#io_mode; $fd++) {
+ my $fh = $orig_io->[$fd] or next;
+ my ($oldfh, $mode) = @{$io_mode[$fd]};
+ open $oldfh, $mode, $fh or die "$$oldfh $mode redirect: $!";
+ }
+}
+
+# $opt->{run_mode} (or $ENV{TEST_RUN_MODE}) allows choosing between
# three ways to spawn our own short-lived Perl scripts for testing:
#
# 0 - (fork|vfork) + execve, the most realistic but slowest
-# 1 - preloading and running in a forked subprocess (fast)
+# 1 - (not currently implemented)
# 2 - preloading and running in current process (slightly faster than 1)
#
# 2 is not compatible with scripts which use "exit" (which we'll try to
my $spawn_opt = {};
for my $fd (0..2) {
my $redir = $opt->{$fd};
- next unless ref($redir);
- open my $fh, '+>', undef or die "open: $!";
- $fhref->[$fd] = $fh;
- $spawn_opt->{$fd} = fileno($fh);
- next if $fd > 0;
- $fh->autoflush(1);
- print $fh $$redir or die "print: $!";
- seek($fh, 0, SEEK_SET) or die "seek: $!";
+ my $ref = ref($redir);
+ if ($ref eq 'SCALAR') {
+ open my $fh, '+>', undef or die "open: $!";
+ $fhref->[$fd] = $fh;
+ $spawn_opt->{$fd} = $fh;
+ next if $fd > 0;
+ $fh->autoflush(1);
+ print $fh $$redir or die "print: $!";
+ seek($fh, 0, SEEK_SET) or die "seek: $!";
+ } elsif ($ref eq 'GLOB') {
+ $spawn_opt->{$fd} = $fhref->[$fd] = $redir;
+ } elsif ($ref) {
+ die "unable to deal with $ref $redir";
+ }
}
if ($run_mode == 0) {
# spawn an independent new process, like real-world use cases:
require PublicInbox::Spawn;
my $cmd = [ key2script($key), @argv ];
my $pid = PublicInbox::Spawn::spawn($cmd, $env, $spawn_opt);
- defined($pid) or die "spawn: $!";
if (defined $pid) {
my $r = waitpid($pid, 0);
defined($r) or die "waitpid: $!";
$r == $pid or die "waitpid: expected $pid, got $r";
}
} else { # localize and run everything in the same process:
- local *STDIN = *STDIN;
- local *STDOUT = *STDOUT;
- local *STDERR = *STDERR;
+ # note: "local *STDIN = *STDIN;" and so forth did not work in
+ # old versions of perl
local %ENV = $env ? (%ENV, %$env) : %ENV;
local %SIG = %SIG;
local $0 = join(' ', @$cmd);
- _prepare_redirects($fhref);
+ my $orig_io = _prepare_redirects($fhref);
_run_sub($sub, $key, \@argv);
+ _undo_redirects($orig_io);
}
# slurp the redirects back into user-supplied strings
sub wait_for_tail () { sleep(2) }
+# like system() built-in, but uses spawn() for env/rdr + vfork
+sub xsys {
+ my ($cmd, $env, $rdr) = @_;
+ if (ref($cmd)) {
+ $rdr ||= {};
+ } else {
+ $cmd = [ @_ ];
+ $env = undef;
+ $rdr = {};
+ }
+ run_script($cmd, $env, { %$rdr, run_mode => 0 });
+ $? >> 8
+}
+
+# like `backtick` or qx{} op, but uses spawn() for env/rdr + vfork
+sub xqx {
+ my ($cmd, $env, $rdr) = @_;
+ $rdr //= {};
+ run_script($cmd, $env, { %$rdr, run_mode => 0, 1 => \(my $out) });
+ wantarray ? split(/^/m, $out) : $out;
+}
+
sub start_script {
my ($cmd, $env, $opt) = @_;
my ($key, @argv) = @$cmd;
- my $run_mode = $ENV{TEST_RUN_MODE} // $opt->{run_mode} // 1;
+ my $run_mode = $ENV{TEST_RUN_MODE} // $opt->{run_mode} // 2;
my $sub = $run_mode == 0 ? undef : key2sub($key);
my $tail_pid;
if (my $tail_cmd = $ENV{TAIL}) {
}
sub join {
- my ($self) = @_;
+ my ($self, $sig) = @_;
my $pid = delete $self->{pid} or return;
+ CORE::kill($sig, $pid) if defined $sig;
my $ret = waitpid($pid, 0);
defined($ret) or die "waitpid($pid): $!";
$ret == $pid or die "waitpid($pid) != $ret";
PublicInbox::TestCommon::wait_for_tail();
CORE::kill('TERM', $tail);
}
- my $pid = delete $self->{pid} or return;
- CORE::kill('TERM', $pid);
+ $self->join('TERM');
}
1;