]> Sergey Matveev's repositories - public-inbox.git/blobdiff - lib/PublicInbox/TestCommon.pm
tests: consistently check for xapian-compact
[public-inbox.git] / lib / PublicInbox / TestCommon.pm
index 85cda03190ffba4b3373ac94c731e0d80d48a46b..299b9c6a830833966df288687270d27ae95e1b49 100644 (file)
@@ -1,15 +1,24 @@
-# 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
 package PublicInbox::TestCommon;
 use strict;
 use parent qw(Exporter);
+use v5.10.1;
 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 eml_load tick
+       have_xapian_compact);
+
+sub eml_load ($) {
+       my ($path, $cb) = @_;
+       open(my $fh, '<', $path) or die "open $path: $!";
+       require PublicInbox::Eml;
+       PublicInbox::Eml->new(\(do { local $/; <$fh> }));
+}
 
 sub tmpdir (;$) {
        my ($base) = @_;
@@ -29,59 +38,120 @@ sub tcp_server () {
                Type => Socket::SOCK_STREAM(),
                Listen => 1024,
                Blocking => 0,
-       )
+       ) or Test::More::BAIL_OUT("failed to create TCP server: $!");
 }
 
 sub tcp_connect {
        my ($dest, %opt) = @_;
+       my $addr = $dest->sockhost . ':' . $dest->sockport;
        my $s = IO::Socket::INET->new(
                Proto => 'tcp',
                Type => Socket::SOCK_STREAM(),
-               PeerAddr => $dest->sockhost . ':' . $dest->sockport,
+               PeerAddr => $addr,
                %opt,
-       );
+       ) or Test::More::BAIL_OUT("failed to connect to $addr: $!");
        $s->autoflush(1);
        $s;
 }
 
 sub require_git ($;$) {
        my ($req, $maybe) = @_;
-       my ($req_maj, $req_min) = split(/\./, $req);
-       my ($cur_maj, $cur_min) = (`git --version` =~ /version (\d+)\.(\d+)/);
+       my ($req_maj, $req_min, $req_sub) = split(/\./, $req);
+       my ($cur_maj, $cur_min, $cur_sub) = (xqx([qw(git --version)])
+                       =~ /version (\d+)\.(\d+)(?:\.(\d+))?/);
 
-       my $req_int = ($req_maj << 24) | ($req_min << 16);
-       my $cur_int = ($cur_maj << 24) | ($cur_min << 16);
+       my $req_int = ($req_maj << 24) | ($req_min << 16) | ($req_sub // 0);
+       my $cur_int = ($cur_maj << 24) | ($cur_min << 16) | ($cur_sub // 0);
        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.$cur_sub");
        }
        1;
 }
 
+sub require_mods {
+       my @mods = @_;
+       my $maybe = pop @mods if $mods[-1] =~ /\A[0-9]+\z/;
+       my @need;
+       while (my $mod = shift(@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;
+                       }
+               } elsif (index($mod, '||') >= 0) { # "Foo||Bar"
+                       my $ok;
+                       for my $m (split(/\Q||\E/, $mod)) {
+                               eval "require $m";
+                               next if $@;
+                               $ok = $m;
+                               last;
+                       }
+                       next if $ok;
+               } else {
+                       eval "require $mod";
+               }
+               if ($@) {
+                       push @need, $mod;
+               } elsif ($mod eq 'IO::Socket::SSL' &&
+                       # old versions of IO::Socket::SSL aren't supported
+                       # by libnet, at least:
+                       # https://rt.cpan.org/Ticket/Display.html?id=100529
+                               !eval{ IO::Socket::SSL->VERSION(2.007); 1 }) {
+                       push @need, $@;
+               }
+       }
+       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;
+}
+
+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 chosing between
+# $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
@@ -89,7 +159,7 @@ sub _prepare_redirects ($) {
 # The default is 2.
 our $run_script_exit_code;
 sub RUN_SCRIPT_EXIT () { "RUN_SCRIPT_EXIT\n" };
-sub run_script_exit (;$) {
+sub run_script_exit {
        $run_script_exit_code = $_[0] // 0;
        die RUN_SCRIPT_EXIT;
 }
@@ -111,7 +181,7 @@ package $pkg;
 use strict;
 use subs qw(exit);
 
-*exit = *PublicInbox::TestCommon::run_script_exit;
+*exit = \\&PublicInbox::TestCommon::run_script_exit;
 sub main {
 # the below "line" directive is a magic comment, see perlsyn(1) manpage
 # line 1 "$f"
@@ -153,35 +223,40 @@ sub run_script ($;$$) {
        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
@@ -195,12 +270,66 @@ sub run_script ($;$$) {
        $? == 0;
 }
 
-sub wait_for_tail () { sleep(2) }
+sub tick (;$) {
+       my $tick = shift // 0.1;
+       select undef, undef, undef, $tick;
+       1;
+}
+
+sub wait_for_tail ($;$) {
+       my ($tail_pid, $want) = @_;
+       my $wait = 2;
+       if ($^O eq 'linux') { # GNU tail may use inotify
+               state $tail_has_inotify;
+               return tick if $want < 0 && $tail_has_inotify;
+               my $end = time + $wait;
+               my @ino;
+               do {
+                       @ino = grep {
+                               readlink($_) =~ /\binotify\b/
+                       } glob("/proc/$tail_pid/fd/*");
+               } while (!@ino && time <= $end and tick);
+               return if !@ino;
+               $tail_has_inotify = 1;
+               $ino[0] =~ s!/fd/!/fdinfo/!;
+               my @info;
+               do {
+                       if (open my $fh, '<', $ino[0]) {
+                               local $/ = "\n";
+                               @info = grep(/^inotify wd:/, <$fh>);
+                       }
+               } while (scalar(@info) < $want && time <= $end and tick);
+       } else {
+               sleep($wait);
+       }
+}
+
+# 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}) {
@@ -209,6 +338,18 @@ sub start_script {
                        next unless /\A--std(?:err|out)=(.+)\z/;
                        push @paths, $1;
                }
+               if ($opt) {
+                       for (1, 2) {
+                               my $f = $opt->{$_} or next;
+                               if (!ref($f)) {
+                                       push @paths, $f;
+                               } elsif (ref($f) eq 'GLOB' && $^O eq 'linux') {
+                                       my $fd = fileno($f);
+                                       my $f = readlink "/proc/$$/fd/$fd";
+                                       push @paths, $f if -e $f;
+                               }
+                       }
+               }
                if (@paths) {
                        defined($tail_pid = fork) or die "fork: $!\n";
                        if ($tail_pid == 0) {
@@ -218,11 +359,12 @@ sub start_script {
                                exec(split(' ', $tail_cmd), @paths);
                                die "$tail_cmd failed: $!";
                        }
-                       wait_for_tail();
+                       wait_for_tail($tail_pid, scalar @paths);
                }
        }
        defined(my $pid = fork) or die "fork: $!\n";
        if ($pid == 0) {
+               eval { PublicInbox::DS->Reset };
                # pretend to be systemd (cf. sd_listen_fds(3))
                # 3 == SD_LISTEN_FDS_START
                my $fd;
@@ -245,6 +387,7 @@ sub start_script {
                }
                $0 = join(' ', @$cmd);
                if ($sub) {
+                       eval { PublicInbox::DS->Reset };
                        _run_sub($sub, $key, \@argv);
                        POSIX::_exit($? >> 8);
                } else {
@@ -255,6 +398,12 @@ sub start_script {
        PublicInboxTestProcess->new($pid, $tail_pid);
 }
 
+sub have_xapian_compact () {
+       require PublicInbox::Spawn;
+       # $ENV{XAPIAN_COMPACT} is used by PublicInbox/Xapcmd.pm, too
+       PublicInbox::Spawn::which($ENV{XAPIAN_COMPACT} || 'xapian-compact');
+}
+
 package PublicInboxTestProcess;
 use strict;
 
@@ -272,8 +421,9 @@ sub kill {
 }
 
 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";
@@ -282,12 +432,15 @@ sub join {
 sub DESTROY {
        my ($self) = @_;
        return if $self->{owner} != $$;
-       if (my $tail = delete $self->{tail_pid}) {
-               PublicInbox::TestCommon::wait_for_tail();
-               CORE::kill('TERM', $tail);
+       if (my $tail_pid = delete $self->{tail_pid}) {
+               PublicInbox::TestCommon::wait_for_tail($tail_pid, -1);
+               CORE::kill('TERM', $tail_pid);
        }
-       my $pid = delete $self->{pid} or return;
-       CORE::kill('TERM', $pid);
+       $self->join('TERM');
 }
 
+package PublicInbox::TestCommon::InboxWakeup;
+use strict;
+sub on_inbox_unlock { ${$_[0]}->($_[1]) }
+
 1;