lei/store: stop shard workers + cat-file on idle
[public-inbox.git] / t / cindex.t
blobe5f26ec33fab9e2642ca7e76a05b399b87b5c16e
1 #!perl -w
2 # Copyright (C) all contributors <meta@public-inbox.org>
3 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4 use v5.12;
5 use PublicInbox::TestCommon;
6 use Cwd qw(getcwd);
7 use List::Util qw(sum);
8 use autodie qw(close mkdir open rename);
9 require_mods(qw(json Xapian +SCM_RIGHTS));
10 use_ok 'PublicInbox::CodeSearchIdx';
11 use PublicInbox::Import;
12 my ($tmp, $for_destroy) = tmpdir();
13 my $pwd = getcwd();
14 my @unused_keys = qw(last_commit has_threadid skip_docdata);
15 local $ENV{PI_CONFIG} = '/dev/null';
16 # local $ENV{TAIL_ALL} = $ENV{TAIL_ALL} // 1; # while features are unstable
17 my $opt = { 1 => \(my $cidx_out), 2 => \(my $cidx_err) };
19 # I reworked CodeSearchIdx->shard_worker to handle empty trees
20 # in the initial commit generated by cvs2svn for xapian.git
21 create_coderepo 'empty-tree-root-0600', tmpdir => "$tmp/wt0", sub {
22         xsys_e([qw(/bin/sh -c), <<'EOM']);
23 git init -q &&
24 git config core.sharedRepository 0600
25 tree=$(git mktree </dev/null) &&
26 head=$(git symbolic-ref HEAD) &&
27 cmt=$(echo 'empty root' | git commit-tree $tree) &&
28 git update-ref $head $cmt &&
29 echo hi >f &&
30 git add f &&
31 git commit -q -m hi &&
32 git gc -q
33 EOM
34 }; # /create_coderepo
36 ok(run_script([qw(-cindex --dangerous -q -g), "$tmp/wt0"]), 'cindex internal');
38         my $exists = -e "$tmp/wt0/.git/public-inbox-cindex/cidx.lock";
39         my @st = stat(_);
40         ok($exists, 'internal dir created');
41         is($st[2] & 0600, 0600, 'mode respects core.sharedRepository');
42         @st = stat("$tmp/wt0/.git/public-inbox-cindex");
43         is($st[2] & 0700, 0700, 'dir mode respects core.sharedRepository');
46 # it's possible for git to emit NUL characters in diffs
47 # (see c4201214cbf10636e2c1ab9131573f735b42c8d4 in linux.git)
48 my $zp = create_coderepo 'NUL in patch', sub {
49         my $src = PublicInbox::IO::try_cat("$pwd/COPYING");
50         xsys_e([qw(git init -q)]);
52         # needs to be further than FIRST_FEW_BYTES (8000) in git.git
53         $src =~ s/\b(Limitation of Liability\.)\n\n/$1\n\0\n/s or
54                 xbail "BUG: no `\\n\\n' in $pwd/COPYING";
56         PublicInbox::IO::write_file '>', 'f', $src;
57         xsys_e([qw(/bin/sh -c), <<'EOM']);
58 git add f &&
59 git commit -q -m 'initial with NUL character'
60 EOM
61         $src =~ s/\n\0\n/\n\n/ or xbail "BUG: no `\\n\\0\\n'";
62         PublicInbox::IO::write_file '>', 'f', $src;
63         xsys_e([qw(/bin/sh -c), <<'EOM']);
64 git add f &&
65 git commit -q -m 'remove NUL character' &&
66 git gc -q
67 EOM
68 }; # /create_coderepo
70 $zp = File::Spec->rel2abs($zp);
71 ok(run_script([qw(-cindex --dangerous -q -d), "$tmp/ext",
72                 '-g', $zp, '-g', "$tmp/wt0" ]),
73         'cindex external');
74 ok(-e "$tmp/ext/cidx.lock", 'external dir created');
75 ok(!-d "$zp/.git/public-inbox-cindex", 'no cindex in original coderepo');
77 ok(run_script([qw(-cindex -L medium --dangerous -q -d),
78         "$tmp/med", '-g', $zp, '-g', "$tmp/wt0"]), 'cindex external medium');
81 SKIP: {
82         have_xapian_compact 2;
83         ok(run_script([qw(-compact -q), "$tmp/ext"]), 'compact on full');
84         ok(run_script([qw(-compact -q), "$tmp/med"]), 'compact on medium');
87 my $no_metadata_set = sub {
88         my ($i, $extra, $xdb) = @_;
89         for my $xdb (@$xdb) {
90                 for my $k (@unused_keys, @$extra) {
91                         is($xdb->get_metadata($k) // '', '',
92                                 "metadata $k unset in shard #$i");
93                 }
94                 ++$i;
95         }
99         my $mid_size = sum(map { -s $_ } glob("$tmp/med/cidx*/*/*"));
100         my $full_size = sum(map { -s $_ } glob("$tmp/ext/cidx*/*/*"));
101         ok($full_size > $mid_size, 'full size > mid size') or
102                 diag "full=$full_size mid=$mid_size";
103         for my $l (qw(med ext)) {
104                 ok(run_script([qw(-cindex -q --reindex -u -d), "$tmp/$l"]),
105                         "reindex $l");
106         }
107         $mid_size = sum(map { -s $_ } glob("$tmp/med/cidx*/*/*"));
108         $full_size = sum(map { -s $_ } glob("$tmp/ext/cidx*/*/*"));
109         ok($full_size > $mid_size, 'full size > mid size after reindex') or
110                 diag "full=$full_size mid=$mid_size";
111         my $csrch = PublicInbox::CodeSearch->new("$tmp/med");
112         my ($xdb0, @xdb) = $csrch->xdb_shards_flat;
113         $no_metadata_set->(0, [], [ $xdb0 ]);
114         is($xdb0->get_metadata('indexlevel'), 'medium',
115                 'indexlevel set in shard #0');
116         $no_metadata_set->(1, ['indexlevel'], \@xdb);
118         ok(run_script([qw(-cindex -q -L full --reindex -u -d), "$tmp/med"]),
119                 'reindex medium as full');
120         @xdb = $csrch->xdb_shards_flat;
121         $no_metadata_set->(0, ['indexlevel'], \@xdb);
124 use_ok 'PublicInbox::CodeSearch';
127 my @xh_args;
128 my $exp = [ 'initial with NUL character', 'remove NUL character' ];
129 my $zp_git = "$zp/.git";
130 if ('multi-repo search') {
131         my $csrch = PublicInbox::CodeSearch->new("$tmp/ext");
132         my $mset = $csrch->mset('NUL');
133         is(scalar($mset->items), 2, 'got results');
134         my @have = sort(map { $_->get_document->get_data } $mset->items);
135         is_xdeeply(\@have, $exp, 'got expected subjects');
137         $mset = $csrch->mset('NUL', { git_dir => "$tmp/wt0/.git" });
138         is(scalar($mset->items), 0, 'no results with other GIT_DIR');
140         $mset = $csrch->mset('NUL', { git_dir => $zp_git });
141         @have = sort(map { $_->get_document->get_data } $mset->items);
142         is_xdeeply(\@have, $exp, 'got expected subjects w/ GIT_DIR filter');
143         my @xdb = $csrch->xdb_shards_flat;
144         $no_metadata_set->(0, ['indexlevel'], \@xdb);
145         @xh_args = $csrch->xh_args;
148 my $test_xhc = sub {
149         my ($xhc) = @_;
150         my $impl = $xhc->{impl};
151         my ($r, @l);
152         $r = $xhc->mkreq([], qw(mset -D -c -g), $zp_git, @xh_args, 'NUL');
153         chomp(@l = <$r>);
154         is(shift(@l), 'mset.size=2', "got expected header $impl");
155         my %docid2data;
156         my @got = sort map {
157                 my @f = split /\0/;
158                 is scalar(@f), 2, 'got 2 entries';
159                 $docid2data{$f[0]} = $f[1];
160                 $f[1];
161         } @l;
162         is_deeply(\@got, $exp, "expected doc_data $impl");
164         $r = $xhc->mkreq([], qw(mset -c -g), "$tmp/wt0/.git", @xh_args, 'NUL');
165         chomp(@l = <$r>);
166         is(shift(@l), 'mset.size=0', "got miss in wrong dir $impl");
167         is_deeply(\@l, [], "no extra lines $impl");
169         my $csrch = PublicInbox::CodeSearch->new("$tmp/ext");
170         while (my ($did, $expect) = each %docid2data) {
171                 is_deeply($csrch->xdb->get_document($did)->get_data,
172                         $expect, "docid=$did data matches");
173         }
174         ok(!$xhc->{io}->close, "$impl close");
175         is($?, 66 << 8, "got EX_NOINPUT from $impl exit");
178 SKIP: {
179         require_mods('+SCM_RIGHTS', 1);
180         require PublicInbox::XapClient;
181         my $xhc = PublicInbox::XapClient::start_helper('-j0');
182         $test_xhc->($xhc);
183         skip 'PI_NO_CXX set', 1 if $ENV{PI_NO_CXX};
184         $xhc->{impl} =~ /Cxx/ or
185                 skip 'C++ compiler or xapian development libs missing', 1;
186         skip 'TEST_XH_CXX_ONLY set', 1 if $ENV{TEST_XH_CXX_ONLY};
187         local $ENV{PI_NO_CXX} = 1; # force XS or SWIG binding test
188         $xhc = PublicInbox::XapClient::start_helper('-j0');
189         $test_xhc->($xhc);
192 if ('--update') {
193         my $csrch = PublicInbox::CodeSearch->new("$tmp/ext");
194         my $mset = $csrch->mset('dfn:for-update');
195         is(scalar($mset->items), 0, 'no result before update');
197         my $e = \%PublicInbox::TestCommon::COMMIT_ENV;
198         xsys_e([qw(/bin/sh -c), <<'EOM'], $e, { -C => "$tmp/wt0" });
199 >for-update && git add for-update && git commit -q -m updated
201         ok(run_script([qw(-cindex -qu -d), "$tmp/ext"]), '-cindex -u');
202         $mset = $csrch->reopen->mset('dfn:for-update');
203         is(scalar($mset->items), 1, 'got updated result');
205         ok(run_script([qw(-cindex -qu --reindex -d), "$tmp/ext"]), 'reindex');
206         $mset = $csrch->reopen->mset('dfn:for-update');
207         is(scalar($mset->items), 1, 'same result after reindex');
210 SKIP: { # --prune
211         require_cmd($ENV{XAPIAN_DELVE} || 'xapian-delve', 1);
212         require_git v2.6, 1;
213         my $csrch = PublicInbox::CodeSearch->new("$tmp/ext");
214         is(scalar($csrch->mset('s:hi')->items), 1, 'got hit');
216         rename("$tmp/wt0/.git", "$tmp/wt0/.giit");
217         ok(run_script([qw(-cindex -q --prune -d), "$tmp/ext"], undef, $opt),
218                 'prune');
219         is(${$opt->{2}}, '', 'nothing in stderr') or diag explain($opt);
220         $csrch->reopen;
221         is(scalar($csrch->mset('s:hi')->items), 0, 'hit pruned');
223         rename("$tmp/wt0/.giit", "$tmp/wt0/.git");
224         ok(run_script([qw(-cindex -qu -d), "$tmp/ext"]), 'update');
225         $csrch->reopen;
226         is(scalar($csrch->mset('s:hi')->items), 0,
227                 'hit stays pruned since GIT_DIR was previously pruned');
228         isnt(scalar($csrch->mset('s:NUL')->items), 0,
229                 'prune did not clobber entire index');
232 File::Path::remove_tree("$tmp/ext");
233 mkdir("$tmp/ext", 0707);
234 ok(run_script([qw(-cindex --dangerous -q -d), "$tmp/ext", '-g', $zp]),
235         'external on existing dir');
237         my @st = stat("$tmp/ext/cidx.lock");
238         is($st[2] & 0777, 0604, 'created lock respects odd permissions');
241 ok(run_script([qw(-xcpdb), "$tmp/ext"]), 'xcpdb upgrade');
242 ok(run_script([qw(-xcpdb -R4), "$tmp/ext"]), 'xcpdb reshard');
244 SKIP: {
245         have_xapian_compact 2;
246         ok(run_script([qw(-xcpdb -R2 --compact), "$tmp/ext"]),
247                 'xcpdb reshard+compact');
248         ok(run_script([qw(-xcpdb --compact), "$tmp/ext"]), 'xcpdb compact');
251 SKIP: {
252         require_cmd('join', 1);
253         my $basic = create_inbox 'basic', indexlevel => 'basic', sub {
254                 my ($im, $ibx) = @_;
255                 $im->add(eml_load('t/plack-qp.eml'));
256         };
257         my $env = { PI_CONFIG => "$tmp/pi_config" };
258         PublicInbox::IO::write_file '>', $env->{PI_CONFIG}, <<EOM;
259 [publicinbox "basictest"]
260         inboxdir = $basic->{inboxdir}
261         address = basic\@example.com
263         my $cmd = [ qw(-cindex -u --all -d), "$tmp/ext",
264                 '--join=aggressive,dt:19700101000000..now',
265                 '-I', $basic->{inboxdir} ];
266         $cidx_out = $cidx_err = '';
267         ok(run_script($cmd, $env, $opt), 'join w/o search');
268         like($cidx_err, qr/W: \Q$basic->{inboxdir}\E not indexed for search/s,
269                 'non-Xapian-enabled inbox noted');
272 # we need to support blank sections for a top-level repos
273 # (e.g. <https://example.com/my-project>
274 # git.kernel.org could use "pub" as section name, though, since all git repos
275 # are currently under //git.kernel.org/pub/**/*
277         mkdir(my $d = "$tmp/blanksection");
278         my $cfg = cfg_new($d, <<EOM);
279 [cindex ""]
280         topdir = $tmp/ext
281         localprefix = $tmp
283         my $csrch = $cfg->lookup_cindex('');
284         is ref($csrch), 'PublicInbox::CodeSearch', 'codesearch w/ blank name';
285         is_deeply $csrch->{localprefix}, [ "$tmp" ], 'localprefix respected';
286         my $nr = 0;
287         $cfg->each_cindex(sub {
288                 my ($cs, @rest) = @_;
289                 is $cs->{topdir}, $csrch->{topdir}, 'each_cindex works';
290                 is_deeply \@rest, [ '.' ], 'got expected arg';
291                 ++$nr;
292         }, '.');
293         is $nr, 1, 'iterated through cindices';
294         my $oid = 'dba13ed2ddf783ee8118c6a581dbf75305f816a3';
295         my $mset = $csrch->mset("dfpost:$oid");
296         is $mset->size, 1, 'got result from full OID search';
299 done_testing;