about summary refs log tree commit homepage
path: root/xt
diff options
context:
space:
mode:
Diffstat (limited to 'xt')
-rw-r--r--xt/git-http-backend.t116
-rw-r--r--xt/nntpd-validate.t217
-rw-r--r--xt/perf-msgview.t52
-rw-r--r--xt/perf-nntpd.t111
-rw-r--r--xt/perf-threading.t32
5 files changed, 528 insertions, 0 deletions
diff --git a/xt/git-http-backend.t b/xt/git-http-backend.t
new file mode 100644
index 00000000..a927d89e
--- /dev/null
+++ b/xt/git-http-backend.t
@@ -0,0 +1,116 @@
+# Copyright (C) 2016-2019 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+#
+# Ensure buffering behavior in -httpd doesn't cause runaway memory use
+# or data corruption
+use strict;
+use warnings;
+use Test::More;
+use POSIX qw(setsid);
+require './t/common.perl';
+
+my $git_dir = $ENV{GIANT_GIT_DIR};
+plan 'skip_all' => 'GIANT_GIT_DIR not defined' unless $git_dir;
+foreach my $mod (qw(BSD::Resource
+                        Plack::Util Plack::Builder
+                        HTTP::Date HTTP::Status Net::HTTP)) {
+        eval "require $mod";
+        plan skip_all => "$mod missing for git-http-backend.t" if $@;
+}
+my $psgi = "./t/git-http-backend.psgi";
+my ($tmpdir, $for_destroy) = tmpdir();
+my $err = "$tmpdir/stderr.log";
+my $out = "$tmpdir/stdout.log";
+my $sock = tcp_server();
+my $host = $sock->sockhost;
+my $port = $sock->sockport;
+my $td;
+
+my $get_maxrss = sub {
+        my $http = Net::HTTP->new(Host => "$host:$port");
+        ok($http, 'Net::HTTP object created for maxrss');
+        $http->write_request(GET => '/');
+        my ($code, $mess, %h) = $http->read_response_headers;
+        is($code, 200, 'success reading maxrss');
+        my $n = $http->read_entity_body(my $buf, 256);
+        ok(defined $n, 'read response body');
+        like($buf, qr/\A\d+\n\z/, 'got memory response');
+        ok(int($buf) > 0, 'got non-zero memory response');
+        int($buf);
+};
+
+{
+        ok($sock, 'sock created');
+        my $cmd = [ '-httpd', '-W0', "--stdout=$out", "--stderr=$err", $psgi ];
+        $td = start_script($cmd, undef, { 3 => $sock });
+}
+my $mem_a = $get_maxrss->();
+
+SKIP: {
+        my $max = 0;
+        my $pack;
+        my $glob = "$git_dir/objects/pack/pack-*.pack";
+        foreach my $f (glob($glob)) {
+                my $n = -s $f;
+                if ($n > $max) {
+                        $max = $n;
+                        $pack = $f;
+                }
+        }
+        skip "no packs found in $git_dir" unless defined $pack;
+        if ($pack !~ m!(/objects/pack/pack-[a-f0-9]{40}.pack)\z!) {
+                skip "bad pack name: $pack";
+        }
+        my $url = $1;
+        my $http = Net::HTTP->new(Host => "$host:$port");
+        ok($http, 'Net::HTTP object created');
+        $http->write_request(GET => $url);
+        my ($code, $mess, %h) = $http->read_response_headers;
+        is(200, $code, 'got 200 success for pack');
+        is($max, $h{'Content-Length'}, 'got expected Content-Length for pack');
+
+        # no $http->read_entity_body, here, since we want to force buffering
+        foreach my $i (1..3) {
+                sleep 1;
+                my $diff = $get_maxrss->() - $mem_a;
+                note "${diff}K memory increase after $i seconds";
+                ok($diff < 1024, 'no bloating caused by slow dumb client');
+        }
+}
+
+{
+        my $c = fork;
+        if ($c == 0) {
+                setsid();
+                exec qw(git clone -q --mirror), "http://$host:$port/",
+                        "$tmpdir/mirror.git";
+                die "Failed start git clone: $!\n";
+        }
+        select(undef, undef, undef, 0.1);
+        foreach my $i (1..10) {
+                is(1, kill('STOP', -$c), 'signaled clone STOP');
+                sleep 1;
+                ok(kill('CONT', -$c), 'continued clone');
+                my $diff = $get_maxrss->() - $mem_a;
+                note "${diff}K memory increase after $i seconds";
+                ok($diff < 2048, 'no bloating caused by slow smart client');
+        }
+        ok(kill('CONT', -$c), 'continued clone');
+        is($c, waitpid($c, 0), 'reaped wayward slow clone');
+        is($?, 0, 'clone did not error out');
+        note 'clone done, fsck-ing clone result...';
+        is(0, system("git", "--git-dir=$tmpdir/mirror.git",
+                        qw(fsck --no-progress)),
+                'fsck did not report corruption');
+
+        my $diff = $get_maxrss->() - $mem_a;
+        note "${diff}K memory increase after smart clone";
+        ok($diff < 2048, 'no bloating caused by slow smart client');
+}
+
+{
+        ok($td->kill, 'killed httpd');
+        $td->join;
+}
+
+done_testing();
diff --git a/xt/nntpd-validate.t b/xt/nntpd-validate.t
new file mode 100644
index 00000000..39108639
--- /dev/null
+++ b/xt/nntpd-validate.t
@@ -0,0 +1,217 @@
+# Copyright (C) 2019 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+
+# Integration test to validate compression.
+use strict;
+use warnings;
+use Test::More;
+use Symbol qw(gensym);
+use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC);
+use POSIX qw(_exit);
+my $inbox_dir = $ENV{GIANT_INBOX_DIR};
+plan skip_all => "GIANT_INBOX_DIR not defined for $0" unless $inbox_dir;
+my $mid = $ENV{TEST_MID};
+
+# Net::NNTP is part of the standard library, but distros may split it off...
+foreach my $mod (qw(DBD::SQLite Net::NNTP Compress::Raw::Zlib)) {
+        eval "require $mod";
+        plan skip_all => "$mod missing for $0" if $@;
+}
+
+my $test_compress = Net::NNTP->can('compress');
+if (!$test_compress) {
+        diag 'Your Net::NNTP does not yet support compression';
+        diag 'See: https://rt.cpan.org/Ticket/Display.html?id=129967';
+}
+my $test_tls = $ENV{TEST_SKIP_TLS} ? 0 : eval { require IO::Socket::SSL };
+my $cert = 'certs/server-cert.pem';
+my $key = 'certs/server-key.pem';
+if ($test_tls && !-r $key || !-r $cert) {
+        plan skip_all => "certs/ missing for $0, run $^X ./certs/create-certs.perl";
+}
+require './t/common.perl';
+my ($tmpdir, $ftd) = tmpdir();
+$File::Temp::KEEP_ALL = !!$ENV{TEST_KEEP_TMP};
+my (%OPT, $td, $host_port, $group);
+my $batch = 1000;
+if (($ENV{NNTP_TEST_URL} // '') =~ m!\Anntp://([^/]+)/([^/]+)\z!) {
+        ($host_port, $group) = ($1, $2);
+        $host_port .= ":119" unless index($host_port, ':') > 0;
+} else {
+        make_local_server();
+}
+my $test_article = $ENV{TEST_ARTICLE} // 0;
+my $test_xover = $ENV{TEST_XOVER} // 1;
+
+if ($test_tls) {
+        my $nntp = Net::NNTP->new($host_port, %OPT);
+        ok($nntp->starttls, 'STARTTLS works');
+        ok($nntp->compress, 'COMPRESS works') if $test_compress;
+        ok($nntp->quit, 'QUIT after starttls OK');
+}
+if ($test_compress) {
+        my $nntp = Net::NNTP->new($host_port, %OPT);
+        ok($nntp->compress, 'COMPRESS works');
+        ok($nntp->quit, 'QUIT after compress OK');
+}
+
+sub do_get_all {
+        my ($methods) = @_;
+        my $desc = join(',', @$methods);
+        my $t0 = clock_gettime(CLOCK_MONOTONIC);
+        my $dig = Digest::SHA->new(1);
+        my $digfh = gensym;
+        my $tmpfh;
+        if ($File::Temp::KEEP_ALL) {
+                open $tmpfh, '>', "$tmpdir/$desc.raw" or die $!;
+        }
+        my $tmp = { dig => $dig, tmpfh => $tmpfh };
+        tie *$digfh, 'DigestPipe', $tmp;
+        my $nntp = Net::NNTP->new($host_port, %OPT);
+        $nntp->article("<$mid>", $digfh) if $mid;
+        foreach my $m (@$methods) {
+                my $res = $nntp->$m;
+                print STDERR "# $m got $res ($desc)\n" if !$res;
+        }
+        $nntp->article("<$mid>", $digfh) if $mid;
+        my ($num, $first, $last) = $nntp->group($group);
+        unless (defined $num && defined $first && defined $last) {
+                warn "Invalid group\n";
+                return undef;
+        }
+        my $i;
+        for ($i = $first; $i < $last; $i += $batch) {
+                my $j = $i + $batch - 1;
+                $j = $last if $j > $last;
+                if ($test_xover) {
+                        my $xover = $nntp->xover("$i-$j");
+                        for my $n (sort { $a <=> $b } keys %$xover) {
+                                my $line = join("\t", @{$xover->{$n}});
+                                $line =~ tr/\r//d;
+                                $dig->add("$n\t".$line);
+                        }
+                }
+                if ($test_article) {
+                        for my $n ($i..$j) {
+                                $nntp->article($n, $digfh) and next;
+                                next if $nntp->code == 423;
+                                my $res = $nntp->code.' '.  $nntp->message;
+
+                                $res =~ tr/\r\n//d;
+                                print STDERR "# Article $n ($desc): $res\n";
+                        }
+                }
+        }
+
+        # hacky bytes_read thing added to Net::NNTP for testing:
+        my $bytes_read = '';
+        if ($nntp->can('bytes_read')) {
+                $bytes_read .= ' '.$nntp->bytes_read.'b';
+        }
+        my $q = $nntp->quit;
+        print STDERR "# quit failed: ".$nntp->code."\n" if !$q;
+        my $elapsed = sprintf('%0.3f', clock_gettime(CLOCK_MONOTONIC) - $t0);
+        my $res = $dig->hexdigest;
+        print STDERR "# $desc - $res (${elapsed}s)$bytes_read\n";
+        $res;
+}
+my @tests = ([]);
+push @tests, [ 'compress' ] if $test_compress;
+push @tests, [ 'starttls' ] if $test_tls;
+push @tests, [ 'starttls', 'compress' ] if $test_tls && $test_compress;
+my (@keys, %thr, %res);
+for my $m (@tests) {
+        my $key = join(',', @$m);
+        push @keys, $key;
+        pipe(my ($r, $w)) or die;
+        my $pid = fork;
+        if ($pid == 0) {
+                close $r or die;
+                my $res = do_get_all($m);
+                print $w $res or die;
+                $w->flush;
+                _exit(0);
+        }
+        close $w or die;
+        $thr{$key} = [ $pid, $r ];
+}
+for my $key (@keys) {
+        my ($pid, $r) = @{delete $thr{$key}};
+        local $/;
+        $res{$key} = <$r>;
+        defined $res{$key} or die "nothing for $key";
+        my $w = waitpid($pid, 0);
+        defined($w) or die;
+        $w == $pid or die "waitpid($pid) != $w)";
+        is($?, 0, "`$key' exited successfully")
+}
+
+my $plain = $res{''};
+ok($plain, "plain got $plain");
+is($res{$_}, $plain, "$_ matches '' result") for @keys;
+
+done_testing();
+
+sub make_local_server {
+        require PublicInbox::Inbox;
+        $group = 'inbox.test.perf.nntpd';
+        my $ibx = { inboxdir => $inbox_dir, newsgroup => $group };
+        $ibx = PublicInbox::Inbox->new($ibx);
+        my $pi_config = "$tmpdir/config";
+        {
+                open my $fh, '>', $pi_config or die "open($pi_config): $!";
+                print $fh <<"" or die "print $pi_config: $!";
+[publicinbox "test"]
+        newsgroup = $group
+        inboxdir = $inbox_dir
+        address = test\@example.com
+
+                close $fh or die "close($pi_config): $!";
+        }
+        my ($out, $err) = ("$tmpdir/out", "$tmpdir/err");
+        for ($out, $err) {
+                open my $fh, '>', $_ or die "truncate: $!";
+        }
+        my $sock = tcp_server();
+        ok($sock, 'sock created');
+        $host_port = $sock->sockhost . ':' . $sock->sockport;
+
+        # not using multiple workers, here, since we want to increase
+        # the chance of tripping concurrency bugs within PublicInbox/NNTP*.pm
+        my $cmd = [ '-nntpd', "--stdout=$out", "--stderr=$err", '-W0' ];
+        push @$cmd, "-lnntp://$host_port";
+        if ($test_tls) {
+                push @$cmd, "--cert=$cert", "--key=$key";
+                %OPT = (
+                        SSL_hostname => 'server.local',
+                        SSL_verifycn_name => 'server.local',
+                        SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_PEER(),
+                        SSL_ca_file => 'certs/test-ca.pem',
+                );
+        }
+        print STDERR "# CMD ". join(' ', @$cmd). "\n";
+        my $env = { PI_CONFIG => $pi_config };
+        $td = start_script($cmd, $env, { 3 => $sock });
+}
+
+package DigestPipe;
+use strict;
+use warnings;
+
+sub TIEHANDLE {
+        my ($class, $self) = @_;
+        bless $self, $class;
+}
+
+sub PRINT {
+        my $self = shift;
+        my $data = join('', @_);
+        # Net::NNTP emit different line-endings depending on TLS or not...:
+        $data =~ tr/\r//d;
+        $self->{dig}->add($data);
+        if (my $tmpfh = $self->{tmpfh}) {
+                print $tmpfh $data;
+        }
+        1;
+}
+1;
diff --git a/xt/perf-msgview.t b/xt/perf-msgview.t
new file mode 100644
index 00000000..22d8ce20
--- /dev/null
+++ b/xt/perf-msgview.t
@@ -0,0 +1,52 @@
+# Copyright (C) 2019 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+use strict;
+use warnings;
+use Test::More;
+use Benchmark qw(:all);
+use PublicInbox::Inbox;
+use PublicInbox::View;
+require './t/common.perl';
+
+my $inboxdir = $ENV{GIANT_INBOX_DIR} // $ENV{GIANT_PI_DIR};
+plan skip_all => "GIANT_INBOX_DIR not defined for $0" unless $inboxdir;
+
+my @cat = qw(cat-file --buffer --batch-check --batch-all-objects);
+if (require_git(2.19, 1)) {
+        push @cat, '--unordered';
+} else {
+        warn
+"git <2.19, cat-file lacks --unordered, locality suffers\n";
+}
+
+use_ok 'Plack::Util';
+my $ibx = PublicInbox::Inbox->new({ inboxdir => $inboxdir, name => 'name' });
+my $git = $ibx->git;
+my $fh = $git->popen(@cat);
+my $vec = '';
+vec($vec, fileno($fh), 1) = 1;
+select($vec, undef, undef, 60) or die "timed out waiting for --batch-check";
+
+my $ctx = {
+        env => { HTTP_HOST => 'example.com', 'psgi.url_scheme' => 'https' },
+        -inbox => $ibx,
+        www => Plack::Util::inline_object(style => sub {''}),
+};
+my ($str, $mime, $res, $cmt, $type);
+my $n = 0;
+my $t = timeit(1, sub {
+        while (<$fh>) {
+                ($cmt, $type) = split / /;
+                next if $type ne 'blob';
+                ++$n;
+                $str = $git->cat_file($cmt);
+                $mime = PublicInbox::MIME->new($str);
+                $res = PublicInbox::View::msg_html($ctx, $mime);
+                $res = $res->[2];
+                while (defined($res->getline)) {}
+                $res->close;
+        }
+});
+diag 'msg_html took '.timestr($t)." for $n messages";
+ok 1;
+done_testing();
diff --git a/xt/perf-nntpd.t b/xt/perf-nntpd.t
new file mode 100644
index 00000000..5a176e08
--- /dev/null
+++ b/xt/perf-nntpd.t
@@ -0,0 +1,111 @@
+# Copyright (C) 2018-2019 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+use strict;
+use warnings;
+use Test::More;
+use Benchmark qw(:all :hireswallclock);
+use PublicInbox::Inbox;
+use Net::NNTP;
+my $inboxdir = $ENV{GIANT_INBOX_DIR} // $ENV{GIANT_PI_DIR};
+plan skip_all => "GIANT_INBOX_DIR not defined for $0" unless defined($inboxdir);
+eval { require PublicInbox::Search };
+my ($host_port, $group, %opts, $s, $td, $tmp_obj);
+require './t/common.perl';
+
+if (($ENV{NNTP_TEST_URL} || '') =~ m!\Anntp://([^/]+)/([^/]+)\z!) {
+        ($host_port, $group) = ($1, $2);
+        $host_port .= ":119" unless index($host_port, ':') > 0;
+} else {
+        $group = 'inbox.test.perf.nntpd';
+        my $ibx = { inboxdir => $inboxdir, newsgroup => $group };
+        $ibx = PublicInbox::Inbox->new($ibx);
+        my $tmpdir;
+        ($tmpdir, $tmp_obj) = tmpdir();
+
+        my $pi_config = "$tmpdir/config";
+        {
+                open my $fh, '>', $pi_config or die "open($pi_config): $!";
+                print $fh <<"" or die "print $pi_config: $!";
+[publicinbox "test"]
+        newsgroup = $group
+        inboxdir = $inboxdir
+        address = test\@example.com
+
+                close $fh or die "close($pi_config): $!";
+        }
+
+        my $sock = tcp_server();
+        ok($sock, 'sock created');
+        my $cmd = [ '-nntpd', '-W0' ];
+        $td = start_script($cmd, { PI_CONFIG => $pi_config }, { 3 => $sock });
+        $host_port = $sock->sockhost . ':' . $sock->sockport;
+}
+%opts = (
+        PeerAddr => $host_port,
+        Proto => 'tcp',
+        Timeout => 1,
+);
+$s = IO::Socket::INET->new(%opts);
+$s->autoflush(1);
+my $buf = $s->getline;
+like($buf, qr/\A201 .* ready - post via email\r\n/s, 'got greeting');
+
+my $t = timeit(10, sub {
+        ok($s->print("GROUP $group\r\n"), 'changed group');
+        $buf = $s->getline;
+});
+diag 'GROUP took: ' . timestr($t);
+
+my ($tot, $min, $max) = ($buf =~ /\A211 (\d+) (\d+) (\d+) /);
+ok($tot && $min && $max, 'got GROUP response');
+my $nr = $max - $min;
+my $nmax = 50000;
+my $nmin = $max - $nmax;
+$nmin = $min if $nmin < $min;
+my $res;
+my $spec = "$nmin-$max";
+my $n;
+
+sub read_until_dot ($) {
+        my $n = 0;
+        do {
+                $buf = $s->getline;
+                ++$n
+        } until $buf eq ".\r\n";
+        $n;
+}
+
+$t = timeit(1, sub {
+        $s->print("XOVER $spec\r\n");
+        $n = read_until_dot($s);
+});
+diag 'xover took: ' . timestr($t) . " for $n";
+
+$t = timeit(1, sub {
+        $s->print("HDR From $spec\r\n");
+        $n = read_until_dot($s);
+
+});
+diag "XHDR From ". timestr($t) . " for $n";
+
+my $date = $ENV{NEWNEWS_DATE};
+unless ($date) {
+        my (undef, undef, undef, $d, $m, $y) = gmtime(time - 30 * 86400);
+        $date = sprintf('%04u%02u%02u', $y + 1900, $m + 1, $d);
+        diag "NEWNEWS_DATE undefined, using $date";
+}
+$t = timeit(1, sub {
+        $s->print("NEWNEWS * $date 000000 GMT\r\n");
+        $n = read_until_dot($s);
+});
+diag 'newnews took: ' . timestr($t) . " for $n";
+
+if ($s) {
+        $s->print("QUIT\r\n");
+        $s->getline;
+}
+
+
+done_testing();
+
+1;
diff --git a/xt/perf-threading.t b/xt/perf-threading.t
new file mode 100644
index 00000000..1038bda5
--- /dev/null
+++ b/xt/perf-threading.t
@@ -0,0 +1,32 @@
+# Copyright (C) 2016-2019 all contributors <meta@public-inbox.org>
+# License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
+#
+# real-world testing of search threading
+use strict;
+use warnings;
+use Test::More;
+use Benchmark qw(:all);
+use PublicInbox::Inbox;
+my $inboxdir = $ENV{GIANT_INBOX_DIR} // $ENV{GIANT_PI_DIR};
+plan skip_all => "GIANT_INBOX_DIR not defined for $0" unless $inboxdir;
+my $ibx = PublicInbox::Inbox->new({ inboxdir => $inboxdir });
+eval { require PublicInbox::Search };
+my $srch = $ibx->search;
+plan skip_all => "$inboxdir not configured for search $0 $@" unless $srch;
+
+require PublicInbox::View;
+
+my $msgs;
+my $elapsed = timeit(1, sub {
+        $msgs = $srch->{over_ro}->recent({limit => 200000});
+});
+my $n = scalar(@$msgs);
+ok($n, 'got some messages');
+diag "enquire: ".timestr($elapsed)." for $n";
+
+$elapsed = timeit(1, sub {
+        PublicInbox::View::thread_results({-inbox => $ibx}, $msgs);
+});
+diag "thread_results ".timestr($elapsed);
+
+done_testing();