]> Sergey Matveev's repositories - public-inbox.git/blob - t/v2reindex.t
public-inbox 1.7.0
[public-inbox.git] / t / v2reindex.t
1 # Copyright (C) 2018-2021 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3 use strict; use v5.10.1; use PublicInbox::TestCommon;
4 use PublicInbox::Eml;
5 use PublicInbox::ContentHash qw(content_digest);
6 use File::Path qw(remove_tree);
7 require_git(2.6);
8 require_mods(qw(DBD::SQLite Search::Xapian));
9 use_ok 'PublicInbox::V2Writable';
10 use_ok 'PublicInbox::OverIdx';
11 my ($inboxdir, $for_destroy) = tmpdir();
12 my $ibx_config = {
13         inboxdir => $inboxdir,
14         name => 'test-v2writable',
15         version => 2,
16         -primary_address => 'test@example.com',
17         indexlevel => 'full',
18         -no_fsync => 1,
19 };
20 my $agpl = do {
21         open my $fh, '<', 'COPYING' or die "can't open COPYING: $!";
22         local $/;
23         <$fh>;
24 };
25 my $phrase = q("defending all users' freedom");
26 my $mime = PublicInbox::Eml->new(<<'EOF'.$agpl);
27 From: a@example.com
28 To: test@example.com
29 Subject: this is a subject
30 Date: Fri, 02 Oct 1993 00:00:00 +0000
31
32 EOF
33 my $minmax;
34 my $msgmap;
35 my ($mark1, $mark2, $mark3, $mark4);
36 {
37         my %config = %$ibx_config;
38         my $ibx = PublicInbox::Inbox->new(\%config);
39         my $im = PublicInbox::V2Writable->new($ibx, {nproc => 1});
40         my $im0 = $im->importer(0);
41         foreach my $i (1..10) {
42                 $mime->header_set('Message-Id', "<$i\@example.com>");
43                 ok($im->add($mime), "message $i added");
44                 if ($i == 4) {
45                         $mark1 = $im0->get_mark($im0->{tip});
46                         $im->remove($mime);
47                         $mark2 = $im0->get_mark($im0->{tip});
48                 }
49         }
50
51         if ('test remove later') {
52                 $mark3 = $im0->get_mark($im0->{tip});
53                 $mime->header_set('Message-Id', "<5\@example.com>");
54                 $im->remove($mime);
55                 $mark4 = $im0->get_mark($im0->{tip});
56         }
57
58         $im->done;
59         $minmax = [ $ibx->mm->minmax ];
60         ok(defined $minmax->[0] && defined $minmax->[1], 'minmax defined');
61         is_deeply($minmax, [ 1, 10 ], 'minmax as expected');
62         is($ibx->mm->num_highwater, 10, 'num_highwater as expected');
63
64         my ($min, $max) = @$minmax;
65         $msgmap = $ibx->mm->msg_range(\$min, $max);
66         is_deeply($msgmap, [
67                           [1, '1@example.com' ],
68                           [2, '2@example.com' ],
69                           [3, '3@example.com' ],
70                           [6, '6@example.com' ],
71                           [7, '7@example.com' ],
72                           [8, '8@example.com' ],
73                           [9, '9@example.com' ],
74                           [10, '10@example.com' ],
75                   ], 'msgmap as expected');
76 }
77
78 {
79         my %config = %$ibx_config;
80         my $ibx = PublicInbox::Inbox->new(\%config);
81         my $im = PublicInbox::V2Writable->new($ibx, 1);
82         eval { $im->index_sync({reindex => 1}) };
83         is($@, '', 'no error from reindexing');
84         $im->done;
85
86         delete $ibx->{mm};
87         is_deeply([ $ibx->mm->minmax ], $minmax, 'minmax unchanged');
88         is($ibx->mm->num_highwater, 10, 'num_highwater as expected');
89
90         my ($min, $max) = $ibx->mm->minmax;
91         is_deeply($ibx->mm->msg_range(\$min, $max), $msgmap, 'msgmap unchanged');
92 }
93
94 my $xap = "$inboxdir/xap".PublicInbox::Search::SCHEMA_VERSION();
95 remove_tree($xap);
96 ok(!-d $xap, 'Xapian directories removed');
97 {
98         my %config = %$ibx_config;
99         my $ibx = PublicInbox::Inbox->new(\%config);
100         my $im = PublicInbox::V2Writable->new($ibx, 1);
101         eval { $im->index_sync({reindex => 1}) };
102         is($@, '', 'no error from reindexing');
103         $im->done;
104         ok(-d $xap, 'Xapian directories recreated');
105
106         delete $ibx->{mm};
107         is_deeply([ $ibx->mm->minmax ], $minmax, 'minmax unchanged');
108         is($ibx->mm->num_highwater, 10, 'num_highwater as expected');
109
110         my ($min, $max) = $ibx->mm->minmax;
111         is_deeply($ibx->mm->msg_range(\$min, $max), $msgmap, 'msgmap unchanged');
112 }
113
114 ok(unlink "$inboxdir/msgmap.sqlite3", 'remove msgmap');
115 remove_tree($xap);
116 ok(!-d $xap, 'Xapian directories removed again');
117 {
118         my @warn;
119         local $SIG{__WARN__} = sub { push @warn, @_ };
120         my %config = %$ibx_config;
121         my $ibx = PublicInbox::Inbox->new(\%config);
122         my $im = PublicInbox::V2Writable->new($ibx, 1);
123         eval { $im->index_sync({reindex => 1}) };
124         is($@, '', 'no error from reindexing without msgmap');
125         is(scalar(@warn), 0, 'no warnings from reindexing');
126         $im->done;
127         ok(-d $xap, 'Xapian directories recreated');
128         delete $ibx->{mm};
129         is_deeply([ $ibx->mm->minmax ], $minmax, 'minmax unchanged');
130         is($ibx->mm->num_highwater, 10, 'num_highwater as expected');
131
132         my ($min, $max) = $ibx->mm->minmax;
133         is_deeply($ibx->mm->msg_range(\$min, $max), $msgmap, 'msgmap unchanged');
134 }
135
136 my %sizes;
137 ok(unlink "$inboxdir/msgmap.sqlite3", 'remove msgmap');
138 remove_tree($xap);
139 ok(!-d $xap, 'Xapian directories removed again');
140 {
141         my @warn;
142         local $SIG{__WARN__} = sub { push @warn, @_ };
143         my %config = %$ibx_config;
144         my $ibx = PublicInbox::Inbox->new(\%config);
145         my $im = PublicInbox::V2Writable->new($ibx, 1);
146         eval { $im->index_sync({reindex => 1}) };
147         is($@, '', 'no error from reindexing without msgmap');
148         is_deeply(\@warn, [], 'no warnings');
149         $im->done;
150         ok(-d $xap, 'Xapian directories recreated');
151         delete $ibx->{mm};
152         is_deeply([ $ibx->mm->minmax ], $minmax, 'minmax unchanged');
153         is($ibx->mm->num_highwater, 10, 'num_highwater as expected');
154         my $mset = $ibx->search->mset($phrase);
155         isnt($mset->size, 0, "phrase search succeeds on indexlevel=full");
156         for (glob("$xap/*/*")) { $sizes{$ibx->{indexlevel}} += -s _ if -f $_ }
157
158         my ($min, $max) = $ibx->mm->minmax;
159         is_deeply($ibx->mm->msg_range(\$min, $max), $msgmap, 'msgmap unchanged');
160 }
161
162 ok(unlink "$inboxdir/msgmap.sqlite3", 'remove msgmap');
163 remove_tree($xap);
164 ok(!-d $xap, 'Xapian directories removed again');
165 {
166         my @warn;
167         local $SIG{__WARN__} = sub { push @warn, @_ };
168         my %config = %$ibx_config;
169         $config{indexlevel} = 'medium';
170         my $ibx = PublicInbox::Inbox->new(\%config);
171         my $im = PublicInbox::V2Writable->new($ibx);
172         eval { $im->index_sync({reindex => 1}) };
173         is($@, '', 'no error from reindexing without msgmap');
174         is_deeply(\@warn, [], 'no warnings');
175         $im->done;
176         ok(-d $xap, 'Xapian directories recreated');
177         delete $ibx->{mm};
178         is_deeply([ $ibx->mm->minmax ], $minmax, 'minmax unchanged');
179         is($ibx->mm->num_highwater, 10, 'num_highwater as expected');
180
181         if (0) {
182                 # not sure why, but Xapian seems to fallback to terms and
183                 # phrase searches still work
184                 delete $ibx->{search};
185                 my $mset = $ibx->search->mset($phrase);
186                 is($mset->size, 0, 'phrase search does not work on medium');
187         }
188         my $words = $phrase;
189         $words =~ tr/"'//d;
190         my $mset = $ibx->search->mset($words);
191         isnt($mset->size, 0, "normal search works on indexlevel=medium");
192         for (glob("$xap/*/*")) { $sizes{$ibx->{indexlevel}} += -s _ if -f $_ }
193
194         ok($sizes{full} > $sizes{medium}, 'medium is smaller than full');
195
196
197         my ($min, $max) = $ibx->mm->minmax;
198         is_deeply($ibx->mm->msg_range(\$min, $max), $msgmap, 'msgmap unchanged');
199 }
200
201 ok(unlink "$inboxdir/msgmap.sqlite3", 'remove msgmap');
202 remove_tree($xap);
203 ok(!-d $xap, 'Xapian directories removed again');
204 {
205         my @warn;
206         local $SIG{__WARN__} = sub { push @warn, @_ };
207         my %config = %$ibx_config;
208         $config{indexlevel} = 'basic';
209         my $ibx = PublicInbox::Inbox->new(\%config);
210         my $im = PublicInbox::V2Writable->new($ibx);
211         eval { $im->index_sync({reindex => 1}) };
212         is($@, '', 'no error from reindexing without msgmap');
213         is_deeply(\@warn, [], 'no warnings');
214         $im->done;
215         ok(-d $xap, 'Xapian directories recreated');
216         delete $ibx->{mm};
217         is_deeply([ $ibx->mm->minmax ], $minmax, 'minmax unchanged');
218         is($ibx->mm->num_highwater, 10, 'num_highwater as expected');
219
220         isnt($ibx->search, 'no search for basic');
221
222         for (glob("$xap/*/*")) { $sizes{$ibx->{indexlevel}} += -s _ if -f $_ }
223         ok($sizes{medium} > $sizes{basic}, 'basic is smaller than medium');
224
225         my ($min, $max) = $ibx->mm->minmax;
226         is_deeply($ibx->mm->msg_range(\$min, $max), $msgmap, 'msgmap unchanged');
227 }
228
229
230 # An incremental indexing test
231 ok(unlink "$inboxdir/msgmap.sqlite3", 'remove msgmap');
232 remove_tree($xap);
233 ok(!-d $xap, 'Xapian directories removed again');
234 {
235         my @warn;
236         local $SIG{__WARN__} = sub { push @warn, @_ };
237         my %config = %$ibx_config;
238         my $ibx = PublicInbox::Inbox->new(\%config);
239         # mark1 4 simple additions in the same index_sync
240         $ibx->{ref_head} = $mark1;
241         my $im = PublicInbox::V2Writable->new($ibx);
242         eval { $im->index_sync() };
243         is($@, '', 'no error from reindexing without msgmap');
244         is_deeply(\@warn, [], 'no warnings');
245         $im->done;
246         my ($min, $max) = $ibx->mm->minmax;
247         is($min, 1, 'min as expected');
248         is($max, 4, 'max as expected');
249         is($ibx->mm->num_highwater, 4, 'num_highwater as expected');
250         is_deeply($ibx->mm->msg_range(\$min, $max),
251                   [
252                    [1, '1@example.com' ],
253                    [2, '2@example.com' ],
254                    [3, '3@example.com' ],
255                    [4, '4@example.com' ],
256                   ], 'msgmap as expected' );
257 }
258 {
259         my @warn;
260         local $SIG{__WARN__} = sub { push @warn, @_ };
261         my %config = %$ibx_config;
262         my $ibx = PublicInbox::Inbox->new(\%config);
263         # mark2 A delete separated from an add in the same index_sync
264         $ibx->{ref_head} = $mark2;
265         my $im = PublicInbox::V2Writable->new($ibx);
266         eval { $im->index_sync() };
267         is($@, '', 'no error from reindexing without msgmap');
268         is_deeply(\@warn, [], 'no warnings');
269         $im->done;
270         my ($min, $max) = $ibx->mm->minmax;
271         is($min, 1, 'min as expected');
272         is($max, 3, 'max as expected');
273         is($ibx->mm->num_highwater, 4, 'num_highwater as expected');
274         is_deeply($ibx->mm->msg_range(\$min, $max),
275                   [
276                    [1, '1@example.com' ],
277                    [2, '2@example.com' ],
278                    [3, '3@example.com' ],
279                   ], 'msgmap as expected' );
280 }
281 {
282         my @warn;
283         local $SIG{__WARN__} = sub { push @warn, @_ };
284         my %config = %$ibx_config;
285         my $ibx = PublicInbox::Inbox->new(\%config);
286         # mark3 adds following the delete at mark2
287         $ibx->{ref_head} = $mark3;
288         my $im = PublicInbox::V2Writable->new($ibx);
289         eval { $im->index_sync() };
290         is($@, '', 'no error from reindexing without msgmap');
291         is_deeply(\@warn, [], 'no warnings');
292         $im->done;
293         my ($min, $max) = $ibx->mm->minmax;
294         is($min, 1, 'min as expected');
295         is($max, 10, 'max as expected');
296         is($ibx->mm->num_highwater, 10, 'num_highwater as expected');
297         is_deeply($ibx->mm->msg_range(\$min, $max),
298                   [
299                    [1, '1@example.com' ],
300                    [2, '2@example.com' ],
301                    [3, '3@example.com' ],
302                    [5, '5@example.com' ],
303                    [6, '6@example.com' ],
304                    [7, '7@example.com' ],
305                    [8, '8@example.com' ],
306                    [9, '9@example.com' ],
307                    [10, '10@example.com' ],
308                   ], 'msgmap as expected' );
309 }
310 {
311         my @warn;
312         local $SIG{__WARN__} = sub { push @warn, @_ };
313         my %config = %$ibx_config;
314         my $ibx = PublicInbox::Inbox->new(\%config);
315         # mark4 A delete of an older message
316         $ibx->{ref_head} = $mark4;
317         my $im = PublicInbox::V2Writable->new($ibx);
318         eval { $im->index_sync() };
319         is($@, '', 'no error from reindexing without msgmap');
320         is_deeply(\@warn, [], 'no warnings');
321         $im->done;
322         my ($min, $max) = $ibx->mm->minmax;
323         is($min, 1, 'min as expected');
324         is($max, 10, 'max as expected');
325         is($ibx->mm->num_highwater, 10, 'num_highwater as expected');
326         is_deeply($ibx->mm->msg_range(\$min, $max),
327                   [
328                    [1, '1@example.com' ],
329                    [2, '2@example.com' ],
330                    [3, '3@example.com' ],
331                    [6, '6@example.com' ],
332                    [7, '7@example.com' ],
333                    [8, '8@example.com' ],
334                    [9, '9@example.com' ],
335                    [10, '10@example.com' ],
336                   ], 'msgmap as expected' );
337 }
338
339
340 # Another incremental indexing test
341 ok(unlink "$inboxdir/msgmap.sqlite3", 'remove msgmap');
342 remove_tree($xap);
343 ok(!-d $xap, 'Xapian directories removed again');
344 {
345         my @warn;
346         local $SIG{__WARN__} = sub { push @warn, @_ };
347         my %config = %$ibx_config;
348         my $ibx = PublicInbox::Inbox->new(\%config);
349         # mark2 an add and it's delete in the same index_sync
350         $ibx->{ref_head} = $mark2;
351         my $im = PublicInbox::V2Writable->new($ibx);
352         eval { $im->index_sync() };
353         is($@, '', 'no error from reindexing without msgmap');
354         is_deeply(\@warn, [], 'no warnings');
355         $im->done;
356         my ($min, $max) = $ibx->mm->minmax;
357         is($min, 1, 'min as expected');
358         is($max, 3, 'max as expected');
359         is($ibx->mm->num_highwater, 4, 'num_highwater as expected');
360         is_deeply($ibx->mm->msg_range(\$min, $max),
361                   [
362                    [1, '1@example.com' ],
363                    [2, '2@example.com' ],
364                    [3, '3@example.com' ],
365                   ], 'msgmap as expected' );
366 }
367 {
368         my @warn;
369         local $SIG{__WARN__} = sub { push @warn, @_ };
370         my %config = %$ibx_config;
371         my $ibx = PublicInbox::Inbox->new(\%config);
372         # mark3 adds following the delete at mark2
373         $ibx->{ref_head} = $mark3;
374         my $im = PublicInbox::V2Writable->new($ibx);
375         eval { $im->index_sync() };
376         is($@, '', 'no error from reindexing without msgmap');
377         is_deeply(\@warn, [], 'no warnings');
378         $im->done;
379         my ($min, $max) = $ibx->mm->minmax;
380         is($min, 1, 'min as expected');
381         is($max, 10, 'max as expected');
382         is($ibx->mm->num_highwater, 10, 'num_highwater as expected');
383         is_deeply($ibx->mm->msg_range(\$min, $max),
384                   [
385                    [1, '1@example.com' ],
386                    [2, '2@example.com' ],
387                    [3, '3@example.com' ],
388                    [5, '5@example.com' ],
389                    [6, '6@example.com' ],
390                    [7, '7@example.com' ],
391                    [8, '8@example.com' ],
392                    [9, '9@example.com' ],
393                    [10, '10@example.com' ],
394                   ], 'msgmap as expected' );
395 }
396 {
397         my @warn;
398         local $SIG{__WARN__} = sub { push @warn, @_ };
399         my %config = %$ibx_config;
400         my $ibx = PublicInbox::Inbox->new(\%config);
401         # mark4 A delete of an older message
402         $ibx->{ref_head} = $mark4;
403         my $im = PublicInbox::V2Writable->new($ibx);
404         eval { $im->index_sync() };
405         is($@, '', 'no error from reindexing without msgmap');
406         is_deeply(\@warn, [], 'no warnings');
407         $im->done;
408         my ($min, $max) = $ibx->mm->minmax;
409         is($min, 1, 'min as expected');
410         is($max, 10, 'max as expected');
411         is($ibx->mm->num_highwater, 10, 'num_highwater as expected');
412         is_deeply($ibx->mm->msg_range(\$min, $max),
413                   [
414                    [1, '1@example.com' ],
415                    [2, '2@example.com' ],
416                    [3, '3@example.com' ],
417                    [6, '6@example.com' ],
418                    [7, '7@example.com' ],
419                    [8, '8@example.com' ],
420                    [9, '9@example.com' ],
421                    [10, '10@example.com' ],
422                   ], 'msgmap as expected' );
423 }
424
425 my $check_rethread = sub {
426         my ($desc) = @_;
427         my @warn;
428         local $SIG{__WARN__} = sub { push @warn, @_ };
429         my %config = %$ibx_config;
430         my $ibx = PublicInbox::Inbox->new(\%config);
431         my $f = $ibx->over->{dbh}->sqlite_db_filename;
432         my $over = PublicInbox::OverIdx->new($f);
433         my $dbh = $over->dbh;
434         my $non_ghost_tids = sub {
435                 $dbh->selectall_arrayref(<<'');
436 SELECT tid FROM over WHERE num > 0 ORDER BY tid ASC
437
438         };
439         my $before = $non_ghost_tids->();
440
441         # mess up threading:
442         my $tid = PublicInbox::OverIdx::get_counter($dbh, 'thread');
443         my $nr = $dbh->do('UPDATE over SET tid = ?', undef, $tid);
444         diag "messing up all threads with tid=$tid";
445
446         my $v2w = PublicInbox::V2Writable->new($ibx);
447         my @pr;
448         my $pr = sub { push @pr, @_ };
449         $v2w->index_sync({reindex => 1, rethread => 1, -progress => $pr});
450         # diag "@pr"; # nobody cares
451         is_deeply(\@warn, [], 'no warnings on reindex + rethread');
452
453         my @n = $dbh->selectrow_array(<<EOS, undef, $tid);
454 SELECT COUNT(*) FROM over WHERE tid <= ?
455 EOS
456         is_deeply(\@n, [ 0 ], 'rethread dropped old threadids');
457         my $after = $non_ghost_tids->();
458         ok($after->[0]->[0] > $before->[-1]->[0],
459                 'all tids greater than before');
460         is(scalar @$after, scalar @$before, 'thread count unchanged');
461 };
462
463 $check_rethread->('no-monster');
464
465 # A real example from linux-renesas-soc on lore where a 3-headed monster
466 # of a message has 3 sets of common headers.  Another normal message
467 # previously existed with a single Message-ID that conflicts with one
468 # of the Message-IDs in the 3-headed monster.
469 {
470         my @warn;
471         local $SIG{__WARN__} = sub { push @warn, @_ };
472         my %config = %$ibx_config;
473         $config{indexlevel} = 'medium';
474         my $ibx = PublicInbox::Inbox->new(\%config);
475         my $im = PublicInbox::V2Writable->new($ibx);
476         my $m3 = PublicInbox::Eml->new(<<'EOF');
477 Date: Tue, 24 May 2016 14:34:22 -0700 (PDT)
478 Message-Id: <20160524.143422.552507610109476444.d@example.com>
479 To: t@example.com
480 Cc: c@example.com
481 Subject: Re: [PATCH v2 2/2] uno
482 From: <f@example.com>
483 In-Reply-To: <1463825855-7363-2-git-send-email-y@example.com>
484 References: <1463825855-7363-1-git-send-email-y@example.com>
485         <1463825855-7363-2-git-send-email-y@example.com>
486 Date: Wed, 25 May 2016 10:01:51 +0900
487 From: h@example.com
488 To: g@example.com
489 Cc: m@example.com
490 Subject: Re: [PATCH] dos
491 Message-ID: <20160525010150.GD7292@example.com>
492 References: <1463498133-23918-1-git-send-email-g+r@example.com>
493 In-Reply-To: <1463498133-23918-1-git-send-email-g+r@example.com>
494 From: s@example.com
495 To: h@example.com
496 Cc: m@example.com
497 Subject: [PATCH 12/13] tres
498 Date: Wed, 01 Jun 2016 01:32:35 +0300
499 Message-ID: <1923946.Jvi0TDUXFC@wasted.example.com>
500 In-Reply-To: <13205049.n7pM8utpHF@wasted.example.com>
501 References: <13205049.n7pM8utpHF@wasted.example.com>
502
503 Somehow we got a message with 3 sets of headers into one
504 message, could've been something broken on the archiver side.
505 EOF
506
507         my $m1 = PublicInbox::Eml->new(<<'EOF');
508 From: a@example.com
509 To: t@example.com
510 Subject: [PATCH 12/13]
511 Date: Wed, 01 Jun 2016 01:32:35 +0300
512 Message-ID: <1923946.Jvi0TDUXFC@wasted.example.com>
513 In-Reply-To: <13205049.n7pM8utpHF@wasted.example.com>
514 References: <13205049.n7pM8utpHF@wasted.example.com>
515
516 This is probably one of the original messages
517
518 EOF
519         $im->add($m1);
520         $im->add($m3);
521         $im->done;
522         remove_tree($xap);
523         eval { $im->index_sync() };
524         is($@, '', 'no error from initial indexing');
525         is_deeply(\@warn, [], 'no warnings from initial index');
526         eval { $im->index_sync({reindex=>1}) };
527         is($@, '', 'no error from reindexing after reused Message-ID (x3)');
528         is_deeply(\@warn, [], 'no warnings on reindex');
529
530         my %uniq;
531         for my $s (qw(uno dos tres)) {
532                 my $mset = $ibx->search->mset("s:$s");
533                 my $msgs = $ibx->search->mset_to_smsg($ibx, $mset);
534                 is(scalar(@$msgs), 1, "only one result for `$s'");
535                 $uniq{$msgs->[0]->{num}}++;
536         }
537         is_deeply([values %uniq], [3], 'search on different subjects');
538 }
539
540 # XXX: not deterministic when dealing with ambiguous messages, oh well
541 $check_rethread->('3-headed-monster once');
542 $check_rethread->('3-headed-monster twice');
543
544 my $rdr = { 2 => \(my $err = '') };
545 my $env = { PI_CONFIG => '/dev/null' };
546 ok(run_script([qw(-index --reindex --xapian-only), $inboxdir], $env, $rdr),
547         '--xapian-only works');
548 is($err, '', 'no errors from --xapian-only');
549 undef $for_destroy;
550 SKIP: {
551         skip 'only testing lsof(8) output on Linux', 1 if $^O ne 'linux';
552         my $lsof = require_cmd('lsof', 1) or skip 'no lsof in PATH', 1;
553         my $rdr = { 2 => \(my $null_err) };
554         my @d = grep(m!/xap[0-9]+/!, xqx([$lsof, '-p', $$], undef, $rdr));
555         is_deeply(\@d, [], 'no deleted index files') or diag explain(\@d);
556 }
557 done_testing();