2 * @brief Backend-related tests.
4 /* Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015,2016,2018,2019 Olly Betts
5 * Copyright (C) 2010 Richard Boulton
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License as
9 * published by the Free Software Foundation; either version 2 of the
10 * License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
25 #include "api_backend.h"
27 #define XAPIAN_DEPRECATED(X) X
30 #include "backendmanager.h"
31 #include "errno_to_string.h"
32 #include "filetests.h"
34 #include "testrunner.h"
35 #include "testsuite.h"
36 #include "testutils.h"
41 #include "safefcntl.h"
42 #include "safesysstat.h"
43 #include "safeunistd.h"
44 #ifdef HAVE_SOCKETPAIR
45 # include "safesyssocket.h"
47 # include "safesyswait.h"
56 /// Regression test - lockfile should honour umask, was only user-readable.
57 DEFINE_TESTCASE(lockfileumask1
, chert
|| glass
) {
58 #if !defined __WIN32__ && !defined __CYGWIN__ && !defined __OS2__
59 mode_t old_umask
= umask(022);
61 Xapian::WritableDatabase db
= get_named_writable_database("lockfileumask1");
63 string path
= get_named_writable_database_path("lockfileumask1");
67 TEST(stat(path
.c_str(), &statbuf
) == 0);
68 TEST_EQUAL(statbuf
.st_mode
& 0777, 0644);
78 /// Check that the backend handles total document length > 0xffffffff.
79 DEFINE_TESTCASE(totaldoclen1
, writable
) {
80 Xapian::WritableDatabase db
= get_writable_database();
82 doc
.add_posting("foo", 1, 2000000000);
84 Xapian::Document doc2
;
85 doc2
.add_posting("bar", 1, 2000000000);
86 db
.add_document(doc2
);
87 TEST_EQUAL(db
.get_avlength(), 2000000000);
88 TEST_EQUAL(db
.get_total_length(), 4000000000ull);
90 TEST_EQUAL(db
.get_avlength(), 2000000000);
91 TEST_EQUAL(db
.get_total_length(), 4000000000ull);
92 for (int i
= 0; i
!= 20; ++i
) {
93 Xapian::Document doc3
;
94 doc3
.add_posting("count" + str(i
), 1, 2000000000);
95 db
.add_document(doc3
);
97 TEST_EQUAL(db
.get_avlength(), 2000000000);
98 TEST_EQUAL(db
.get_total_length(), 44000000000ull);
100 TEST_EQUAL(db
.get_avlength(), 2000000000);
101 TEST_EQUAL(db
.get_total_length(), 44000000000ull);
102 if (get_dbtype() != "inmemory") {
103 // InMemory doesn't support get_writable_database_as_database().
104 Xapian::Database dbr
= get_writable_database_as_database();
105 TEST_EQUAL(dbr
.get_avlength(), 2000000000);
106 TEST_EQUAL(dbr
.get_total_length(), 44000000000ull);
110 // Check that exceeding 32bit in combined database doesn't cause a problem
111 // when using 64bit docids.
112 DEFINE_TESTCASE(exceed32bitcombineddb1
, writable
) {
113 // Test case is for 64-bit Xapian::docid.
114 // FIXME: Though we should check that the overflow is handled gracefully
116 if (sizeof(Xapian::docid
) == 4) return;
118 // The InMemory backend uses a vector for the documents, so trying to add
119 // a document with the maximum docid is likely to fail because we can't
120 // allocate enough memory!
121 SKIP_TEST_FOR_BACKEND("inmemory");
123 Xapian::WritableDatabase db1
= get_writable_database();
124 Xapian::Document doc
;
125 doc
.set_data("prose");
126 doc
.add_term("word");
127 Xapian::docid max_32bit_id
= 0xffffffff;
128 db1
.replace_document(max_32bit_id
, doc
);
131 Xapian::Database db2
= get_writable_database_as_database();
134 db
.add_database(db1
);
135 db
.add_database(db2
);
137 Xapian::Enquire
enquire(db
);
138 enquire
.set_query(Xapian::Query::MatchAll
);
139 Xapian::MSet mymset
= enquire
.get_mset(0, 10);
141 TEST_EQUAL(2, mymset
.size());
143 // We can't usefully check the shard docid if the testharness backend is
145 bool multi
= (db1
.size() > 1);
146 for (Xapian::MSetIterator i
= mymset
.begin(); i
!= mymset
.end(); ++i
) {
147 doc
= i
.get_document();
149 TEST_EQUAL(doc
.get_docid(), max_32bit_id
);
150 TEST_EQUAL(doc
.get_data(), "prose");
154 DEFINE_TESTCASE(dbstats1
, backend
) {
155 Xapian::Database db
= get_database("etext");
157 // Use precalculated values to avoid expending CPU cycles to calculate
158 // these every time without improving test coverage.
159 const Xapian::termcount min_len
= 2;
160 const Xapian::termcount max_len
= 532;
161 const Xapian::termcount max_wdf
= 22;
163 if (get_dbtype() != "inmemory") {
164 // Should be exact as no deletions have happened.
165 TEST_EQUAL(db
.get_doclength_upper_bound(), max_len
);
166 TEST_EQUAL(db
.get_doclength_lower_bound(), min_len
);
168 // For inmemory, we usually give rather loose bounds.
169 TEST_REL(db
.get_doclength_upper_bound(),>=,max_len
);
170 TEST_REL(db
.get_doclength_lower_bound(),<=,min_len
);
173 if (get_dbtype() != "inmemory" &&
174 get_dbtype().find("remote") == string::npos
) {
175 TEST_EQUAL(db
.get_wdf_upper_bound("the"), max_wdf
);
177 // For inmemory and remote backends, we usually give rather loose
178 // bounds (remote matches use tighter bounds, but querying the
179 // wdf bound gives a looser one).
180 TEST_REL(db
.get_wdf_upper_bound("the"),>=,max_wdf
);
183 // This failed with an assertion during development between 1.3.1 and
185 TEST_EQUAL(db
.get_wdf_upper_bound(""), 0);
188 // Check stats with a single document. In a multi-database situation, this
189 // gave 0 for get-_doclength_lower_bound() in 1.3.2.
190 DEFINE_TESTCASE(dbstats2
, backend
) {
191 Xapian::Database db
= get_database("apitest_onedoc");
193 // Use precalculated values to avoid expending CPU cycles to calculate
194 // these every time without improving test coverage.
195 const Xapian::termcount min_len
= 15;
196 const Xapian::termcount max_len
= 15;
197 const Xapian::termcount max_wdf
= 7;
199 if (get_dbtype() != "inmemory") {
200 // Should be exact as no deletions have happened.
201 TEST_EQUAL(db
.get_doclength_upper_bound(), max_len
);
202 TEST_EQUAL(db
.get_doclength_lower_bound(), min_len
);
204 // For inmemory, we usually give rather loose bounds.
205 TEST_REL(db
.get_doclength_upper_bound(),>=,max_len
);
206 TEST_REL(db
.get_doclength_lower_bound(),<=,min_len
);
209 if (get_dbtype() != "inmemory" &&
210 get_dbtype().find("remote") == string::npos
) {
211 TEST_EQUAL(db
.get_wdf_upper_bound("word"), max_wdf
);
213 // For inmemory and remote backends, we usually give rather loose
214 // bounds (remote matches use tighter bounds, but querying the
215 // wdf bound gives a looser one).
216 TEST_REL(db
.get_wdf_upper_bound("word"),>=,max_wdf
);
219 TEST_EQUAL(db
.get_wdf_upper_bound(""), 0);
222 /// Check handling of alldocs on an empty database.
223 DEFINE_TESTCASE(alldocspl3
, backend
) {
224 Xapian::Database db
= get_database(string());
226 TEST_EQUAL(db
.get_termfreq(string()), 0);
227 TEST_EQUAL(db
.get_collection_freq(string()), 0);
228 TEST(db
.postlist_begin(string()) == db
.postlist_end(string()));
231 /// Regression test for bug#392 in ModifiedPostList iteration, fixed in 1.0.15.
232 DEFINE_TESTCASE(modifiedpostlist1
, writable
) {
233 Xapian::WritableDatabase db
= get_writable_database();
234 Xapian::Document a
, b
;
235 Xapian::Enquire
enq(db
);
238 enq
.set_query(Xapian::Query("T"));
240 db
.replace_document(2, a
);
242 db
.replace_document(1, a
);
243 db
.replace_document(1, b
);
245 mset_expect_order(enq
.get_mset(0, 2), 2);
248 /// Regression test for chert bug fixed in 1.1.3 (ticket#397).
249 DEFINE_TESTCASE(doclenaftercommit1
, writable
) {
250 Xapian::WritableDatabase db
= get_writable_database();
251 TEST_EXCEPTION(Xapian::DocNotFoundError
, db
.get_doclength(1));
252 TEST_EXCEPTION(Xapian::DocNotFoundError
, db
.get_unique_terms(1));
253 db
.replace_document(1, Xapian::Document());
255 TEST_EQUAL(db
.get_doclength(1), 0);
256 TEST_EQUAL(db
.get_unique_terms(1), 0);
259 DEFINE_TESTCASE(valuesaftercommit1
, writable
) {
260 Xapian::WritableDatabase db
= get_writable_database();
261 Xapian::Document doc
;
262 doc
.add_value(0, "value");
263 db
.replace_document(2, doc
);
265 db
.replace_document(1, doc
);
266 db
.replace_document(3, doc
);
267 TEST_EQUAL(db
.get_document(3).get_value(0), "value");
269 TEST_EQUAL(db
.get_document(3).get_value(0), "value");
272 DEFINE_TESTCASE(lockfilefd0or1
, chert
|| glass
) {
273 #if !defined __WIN32__ && !defined __CYGWIN__ && !defined __OS2__
274 int old_stdin
= dup(0);
275 int old_stdout
= dup(1);
277 // With fd 0 available.
280 Xapian::WritableDatabase db
= get_writable_database();
281 TEST_EXCEPTION(Xapian::DatabaseLockError
,
282 (void)get_writable_database_again());
284 // With fd 0 and fd 1 available.
287 Xapian::WritableDatabase db
= get_writable_database();
288 TEST_EXCEPTION(Xapian::DatabaseLockError
,
289 (void)get_writable_database_again());
291 // With fd 1 available.
294 Xapian::WritableDatabase db
= get_writable_database();
295 TEST_EXCEPTION(Xapian::DatabaseLockError
,
296 (void)get_writable_database_again());
312 /// Regression test for bug fixed in 1.2.13 and 1.3.1.
313 DEFINE_TESTCASE(lockfilealreadyopen1
, chert
|| glass
) {
314 // Ensure database has been created.
315 (void)get_named_writable_database("lockfilealreadyopen1");
316 string path
= get_named_writable_database_path("lockfilealreadyopen1");
317 int fd
= ::open((path
+ "/flintlock").c_str(), O_RDONLY
);
320 Xapian::WritableDatabase
db(path
, Xapian::DB_CREATE_OR_OPEN
);
321 TEST_EXCEPTION(Xapian::DatabaseLockError
,
322 Xapian::WritableDatabase
db2(path
, Xapian::DB_CREATE_OR_OPEN
)
331 /// Feature tests for Database::locked().
332 DEFINE_TESTCASE(testlock1
, chert
|| glass
) {
333 Xapian::Database rdb
;
336 Xapian::WritableDatabase db
= get_named_writable_database("testlock1");
338 Xapian::Database db_as_database
= db
;
339 TEST(db_as_database
.locked());
341 rdb
= get_writable_database_as_database();
343 TEST(db_as_database
.locked());
346 } catch (const Xapian::FeatureUnavailableError
&) {
347 SKIP_TEST("Database::locked() not supported on this platform");
349 db_as_database
= rdb
;
351 TEST(db_as_database
.locked());
353 db_as_database
.close();
356 // After close(), locked() should either work as if close() hadn't been
357 // called or throw Xapian::DatabaseClosedError.
359 TEST(db_as_database
.locked());
360 } catch (const Xapian::DatabaseClosedError
&) {
365 TEST(!db_as_database
.locked());
366 } catch (const Xapian::DatabaseClosedError
&) {
372 /** Test that locked() returns false for backends which don't support update.
374 * Regression test for bug fixed in 1.4.6.
376 DEFINE_TESTCASE(testlock2
, backend
&& !writable
) {
377 Xapian::Database db
= get_database("apitest_simpledata");
383 /** Test locked() on inmemory Database objects.
385 * An inmemory Database is always actually a WritableDatabase viewed as a
386 * Database, so it should always report being locked for writing, unless
387 * close() has been called.
389 * Regression test for bug fixed in 1.4.14 - earlier versions always returned
390 * false for an inmemory Database here.
392 * Regression test for bug fixed in 1.4.15 - false should be returned after
393 * close() has been called.
395 DEFINE_TESTCASE(testlock3
, inmemory
) {
396 Xapian::Database db
= get_database("apitest_simpledata");
402 /// Test locked() on closed WritableDatabase.
403 DEFINE_TESTCASE(testlock4
, chert
|| glass
) {
404 Xapian::Database db
= get_writable_database("apitest_simpledata");
405 // Even if we don't have a way to test the lock on the current platform,
406 // this should know the database is locked because this object holds the
412 } catch (const Xapian::FeatureUnavailableError
&) {
413 SKIP_TEST("Database::locked() not supported on this platform");
417 class CheckMatchDecider
: public Xapian::MatchDecider
{
421 CheckMatchDecider() : called(false) { }
423 bool operator()(const Xapian::Document
&) const {
428 bool was_called() const { return called
; }
431 /// Test Xapian::MatchDecider with remote backend fails.
432 DEFINE_TESTCASE(matchdecider4
, remote
) {
433 Xapian::Database
db(get_database("apitest_simpledata"));
434 Xapian::Enquire
enquire(db
);
435 enquire
.set_query(Xapian::Query("paragraph"));
437 CheckMatchDecider mdecider
;
440 TEST_EXCEPTION(Xapian::UnimplementedError
,
441 mset
= enquire
.get_mset(0, 10, NULL
, &mdecider
));
442 TEST(!mdecider
.was_called());
445 /** Check that replacing an unmodified document doesn't increase the automatic
446 * flush counter. Regression test for bug fixed in 1.1.4/1.0.18.
448 DEFINE_TESTCASE(replacedoc7
, writable
&& !inmemory
&& !remote
) {
449 // The inmemory backend doesn't batch changes, so there's nothing to
452 // The remote backend doesn't implement the lazy replacement of documents
453 // optimisation currently.
454 Xapian::WritableDatabase
db(get_writable_database());
455 Xapian::Document doc
;
456 doc
.set_data("fish");
457 doc
.add_term("Hlocalhost");
458 doc
.add_posting("hello", 1);
459 doc
.add_posting("world", 2);
460 doc
.add_value(1, "myvalue");
461 db
.add_document(doc
);
464 // We add a second document, and then replace the first document with
465 // itself 10000 times. If the document count for the database reopened
466 // read-only is 2, then we triggered an automatic commit.
468 doc
.add_term("XREV2");
469 db
.add_document(doc
);
471 for (int i
= 0; i
< 10000; ++i
) {
472 doc
= db
.get_document(1);
473 db
.replace_document(1, doc
);
476 Xapian::Database
rodb(get_writable_database_as_database());
477 TEST_EQUAL(rodb
.get_doccount(), 1);
482 TEST_EQUAL(rodb
.get_doccount(), 2);
485 /** Check that replacing a document deleted since the last flush works.
486 * Prior to 1.1.4/1.0.18, this failed to update the collection frequency and
487 * wdf, and caused an assertion failure when assertions were enabled.
489 DEFINE_TESTCASE(replacedoc8
, writable
) {
490 Xapian::WritableDatabase
db(get_writable_database());
492 Xapian::Document doc
;
493 doc
.set_data("fish");
494 doc
.add_term("takeaway");
495 db
.add_document(doc
);
497 db
.delete_document(1);
499 Xapian::Document doc
;
500 doc
.set_data("chips");
501 doc
.add_term("takeaway", 2);
502 db
.replace_document(1, doc
);
505 TEST_EQUAL(db
.get_collection_freq("takeaway"), 2);
506 Xapian::PostingIterator p
= db
.postlist_begin("takeaway");
507 TEST(p
!= db
.postlist_end("takeaway"));
508 TEST_EQUAL(p
.get_wdf(), 2);
511 /** Check that replacing a document after clear_terms() still deletes old
512 * positional data. Regression test for bug introduced and fixed in
513 * development prior to 1.5.0.
515 DEFINE_TESTCASE(replacedoc9
, writable
) {
516 Xapian::WritableDatabase
db(get_named_writable_database("replacedoc9"));
518 Xapian::Document doc
;
519 doc
.set_data("food");
520 doc
.add_posting("falafel", 1);
521 db
.add_document(doc
);
524 Xapian::Document doc
= db
.get_document(1);
526 doc
.add_term("falafel");
527 db
.replace_document(1, doc
);
530 // The positions should have been removed, but the bug meant they weren't.
531 TEST_EQUAL(db
.positionlist_begin(1, "falafel"),
532 db
.positionlist_end(1, "falafel"));
535 /// Test coverage for DatabaseModifiedError.
536 DEFINE_TESTCASE(databasemodified1
, writable
&& !inmemory
&& !multi
) {
537 // The inmemory backend doesn't support revisions.
539 // With multi, DatabaseModifiedError doesn't trigger as easily.
540 Xapian::WritableDatabase
db(get_writable_database());
541 Xapian::Document doc
;
542 doc
.set_data("cargo");
547 for (int i
= 0; i
< N
; ++i
) {
548 db
.add_document(doc
);
552 Xapian::Database
rodb(get_writable_database_as_database());
553 db
.add_document(doc
);
556 db
.add_document(doc
);
559 db
.add_document(doc
);
561 TEST_EQUAL(*rodb
.termlist_begin(N
- 1), "abc");
562 FAIL_TEST("Expected DatabaseModifiedError wasn't thrown");
563 } catch (const Xapian::DatabaseModifiedError
&) {
567 Xapian::Enquire
enq(rodb
);
568 enq
.set_query(Xapian::Query("abc"));
569 Xapian::MSet mset
= enq
.get_mset(0, 10);
570 FAIL_TEST("Expected DatabaseModifiedError wasn't thrown");
571 } catch (const Xapian::DatabaseModifiedError
&) {
575 /// Regression test for bug#462 fixed in 1.0.19 and 1.1.5.
576 DEFINE_TESTCASE(qpmemoryleak1
, writable
&& !inmemory
) {
577 // Inmemory never throws DatabaseModifiedError.
578 Xapian::WritableDatabase
wdb(get_writable_database());
579 Xapian::Document doc
;
582 for (int i
= 100; i
< 120; ++i
) {
583 doc
.add_term(str(i
));
586 for (int j
= 0; j
< 50; ++j
) {
587 wdb
.add_document(doc
);
591 Xapian::Database
database(get_writable_database_as_database());
592 Xapian::QueryParser queryparser
;
593 queryparser
.set_database(database
);
594 TEST_EXCEPTION(Xapian::DatabaseModifiedError
,
595 for (int k
= 0; k
< 1000; ++k
) {
596 wdb
.add_document(doc
);
598 (void)queryparser
.parse_query("1", queryparser
.FLAG_PARTIAL
);
600 SKIP_TEST("didn't manage to trigger DatabaseModifiedError");
605 make_msize1_db(Xapian::WritableDatabase
&db
, const string
&)
607 const char * value0
=
608 "ABBCDEFGHIJKLMMNOPQQRSTTUUVVWXYZZaabcdefghhijjkllmnopqrsttuvwxyz";
609 const char * value1
=
610 "EMLEMMMMMMMNMMLMELEDNLEDMLMLDMLMLMLMEDGFHPOPBAHJIQJNGRKCGF";
612 Xapian::Document doc
;
613 doc
.add_value(0, string(1, *value0
++));
615 doc
.add_value(1, string(1, *value1
++));
618 db
.add_document(doc
);
622 /// Regression test for ticket#464, fixed in 1.1.6 and 1.0.20.
623 DEFINE_TESTCASE(msize1
, backend
) {
624 Xapian::Database db
= get_database("msize1", make_msize1_db
);
625 Xapian::Enquire
enq(db
);
626 enq
.set_sort_by_value(1, false);
627 enq
.set_collapse_key(0);
628 enq
.set_query(Xapian::Query("K1"));
630 Xapian::MSet mset
= enq
.get_mset(0, 60);
631 Xapian::doccount lb
= mset
.get_matches_lower_bound();
632 Xapian::doccount ub
= mset
.get_matches_upper_bound();
633 Xapian::doccount est
= mset
.get_matches_estimated();
637 Xapian::MSet mset2
= enq
.get_mset(50, 10, 1000);
638 Xapian::doccount lb2
= mset2
.get_matches_lower_bound();
639 Xapian::doccount ub2
= mset2
.get_matches_upper_bound();
640 Xapian::doccount est2
= mset2
.get_matches_estimated();
641 TEST_EQUAL(lb2
, ub2
);
642 TEST_EQUAL(lb2
, est2
);
643 TEST_EQUAL(est
, est2
);
645 Xapian::MSet mset3
= enq
.get_mset(0, 10, 1000);
646 Xapian::doccount lb3
= mset3
.get_matches_lower_bound();
647 Xapian::doccount ub3
= mset3
.get_matches_upper_bound();
648 Xapian::doccount est3
= mset3
.get_matches_estimated();
649 TEST_EQUAL(lb3
, ub3
);
650 TEST_EQUAL(lb3
, est3
);
651 TEST_EQUAL(est
, est3
);
655 make_msize2_db(Xapian::WritableDatabase
&db
, const string
&)
657 const char * value0
= "AAABCDEEFGHIIJJKLLMNNOOPPQQRSTTUVWXYZ";
658 const char * value1
= "MLEMNMLMLMEDEDEMLEMLMLMLPOAHGF";
660 Xapian::Document doc
;
661 doc
.add_value(0, string(1, *value0
++));
663 doc
.add_value(1, string(1, *value1
++));
666 db
.add_document(doc
);
670 /// Regression test for bug related to ticket#464, fixed in 1.1.6 and 1.0.20.
671 DEFINE_TESTCASE(msize2
, backend
) {
672 Xapian::Database db
= get_database("msize2", make_msize2_db
);
673 Xapian::Enquire
enq(db
);
674 enq
.set_sort_by_value(1, false);
675 enq
.set_collapse_key(0);
676 enq
.set_query(Xapian::Query("K1"));
678 Xapian::MSet mset
= enq
.get_mset(0, 60);
679 Xapian::doccount lb
= mset
.get_matches_lower_bound();
680 Xapian::doccount ub
= mset
.get_matches_upper_bound();
681 Xapian::doccount est
= mset
.get_matches_estimated();
685 Xapian::MSet mset2
= enq
.get_mset(50, 10, 1000);
686 Xapian::doccount lb2
= mset2
.get_matches_lower_bound();
687 Xapian::doccount ub2
= mset2
.get_matches_upper_bound();
688 Xapian::doccount est2
= mset2
.get_matches_estimated();
689 TEST_EQUAL(lb2
, ub2
);
690 TEST_EQUAL(lb2
, est2
);
691 TEST_EQUAL(est
, est2
);
693 Xapian::MSet mset3
= enq
.get_mset(0, 10, 1000);
694 Xapian::doccount lb3
= mset3
.get_matches_lower_bound();
695 Xapian::doccount ub3
= mset3
.get_matches_upper_bound();
696 Xapian::doccount est3
= mset3
.get_matches_estimated();
697 TEST_EQUAL(lb3
, ub3
);
698 TEST_EQUAL(lb3
, est3
);
699 TEST_EQUAL(est
, est3
);
703 make_xordecay1_db(Xapian::WritableDatabase
&db
, const string
&)
705 for (int n
= 1; n
!= 50; ++n
) {
706 Xapian::Document doc
;
707 for (int i
= 1; i
!= 50; ++i
) {
709 doc
.add_term("N" + str(i
));
711 db
.add_document(doc
);
715 /// Regression test for bug in decay of XOR, fixed in 1.2.1 and 1.0.21.
716 DEFINE_TESTCASE(xordecay1
, backend
) {
717 Xapian::Database db
= get_database("xordecay1", make_xordecay1_db
);
718 Xapian::Enquire
enq(db
);
719 enq
.set_query(Xapian::Query(Xapian::Query::OP_XOR
,
720 Xapian::Query("N10"),
721 Xapian::Query(Xapian::Query::OP_OR
,
723 Xapian::Query("N3"))));
724 Xapian::MSet mset1
= enq
.get_mset(0, 1);
725 Xapian::MSet msetall
= enq
.get_mset(0, db
.get_doccount());
727 TEST(mset_range_is_same(mset1
, 0, msetall
, 0, mset1
.size()));
731 make_ordecay_db(Xapian::WritableDatabase
&db
, const string
&)
733 const char * p
= "VJ=QC]LUNTaARLI;715RR^];A4O=P4ZG<2CS4EM^^VS[A6QENR";
734 for (int d
= 0; p
[d
]; ++d
) {
735 int l
= int(p
[d
] - '0');
736 Xapian::Document doc
;
737 for (int n
= 1; n
< l
; ++n
) {
738 doc
.add_term("N" + str(n
));
739 if (n
% (d
+ 1) == 0) {
740 doc
.add_term("M" + str(n
));
743 db
.add_document(doc
);
747 /// Regression test for bug in decay of OR to AND, fixed in 1.2.1 and 1.0.21.
748 DEFINE_TESTCASE(ordecay1
, backend
) {
749 Xapian::Database db
= get_database("ordecay", make_ordecay_db
);
750 Xapian::Enquire
enq(db
);
751 enq
.set_query(Xapian::Query(Xapian::Query::OP_OR
,
752 Xapian::Query("N20"),
753 Xapian::Query("N21")));
755 Xapian::MSet msetall
= enq
.get_mset(0, db
.get_doccount());
756 for (unsigned int i
= 1; i
< msetall
.size(); ++i
) {
757 Xapian::MSet submset
= enq
.get_mset(0, i
);
758 TEST(mset_range_is_same(submset
, 0, msetall
, 0, submset
.size()));
762 /** Regression test for bug in decay of OR to AND_MAYBE, fixed in 1.2.1 and
765 DEFINE_TESTCASE(ordecay2
, backend
) {
766 Xapian::Database db
= get_database("ordecay", make_ordecay_db
);
767 Xapian::Enquire
enq(db
);
768 std::vector
<Xapian::Query
> q
;
769 q
.push_back(Xapian::Query("M20"));
770 q
.push_back(Xapian::Query("N21"));
771 q
.push_back(Xapian::Query("N22"));
772 enq
.set_query(Xapian::Query(Xapian::Query::OP_OR
,
773 Xapian::Query("N25"),
774 Xapian::Query(Xapian::Query::OP_AND
,
778 Xapian::MSet msetall
= enq
.get_mset(0, db
.get_doccount());
779 for (unsigned int i
= 1; i
< msetall
.size(); ++i
) {
780 Xapian::MSet submset
= enq
.get_mset(0, i
);
781 TEST(mset_range_is_same(submset
, 0, msetall
, 0, submset
.size()));
786 make_orcheck_db(Xapian::WritableDatabase
&db
, const string
&)
788 static const unsigned t1
[] = {2, 4, 6, 8, 10};
789 static const unsigned t2
[] = {6, 7, 8, 11, 12, 13, 14, 15, 16, 17};
790 static const unsigned t3
[] = {3, 7, 8, 11, 12, 13, 14, 15, 16, 17};
792 for (unsigned i
= 1; i
<= 17; ++i
) {
793 Xapian::Document doc
;
794 db
.replace_document(i
, doc
);
796 for (unsigned i
: t1
) {
797 Xapian::Document
doc(db
.get_document(i
));
799 db
.replace_document(i
, doc
);
801 for (unsigned i
: t2
) {
802 Xapian::Document
doc(db
.get_document(i
));
805 doc
.add_term("T2_lowfreq");
807 doc
.add_value(2, "1");
808 db
.replace_document(i
, doc
);
810 for (unsigned i
: t3
) {
811 Xapian::Document
doc(db
.get_document(i
));
814 doc
.add_term("T3_lowfreq");
816 doc
.add_value(3, "1");
817 db
.replace_document(i
, doc
);
821 /** Regression test for bugs in the check() method of OrPostList. (ticket #485)
822 * Bugs introduced and fixed between 1.2.0 and 1.2.1 (never in a release).
824 DEFINE_TESTCASE(orcheck1
, backend
) {
825 Xapian::Database db
= get_database("orcheck1", make_orcheck_db
);
826 Xapian::Enquire
enq(db
);
827 Xapian::Query
q1("T1");
828 Xapian::Query
q2("T2");
829 Xapian::Query
q2l("T2_lowfreq");
830 Xapian::Query
q3("T3");
831 Xapian::Query
q3l("T3_lowfreq");
832 Xapian::Query
v2(Xapian::Query::OP_VALUE_RANGE
, 2, "0", "2");
833 Xapian::Query
v3(Xapian::Query::OP_VALUE_RANGE
, 3, "0", "2");
835 tout
<< "Checking q2 OR q3\n";
836 enq
.set_query(Xapian::Query(Xapian::Query::OP_AND
, q1
,
837 Xapian::Query(Xapian::Query::OP_OR
, q2
, q3
)));
838 mset_expect_order(enq
.get_mset(0, db
.get_doccount()), 8, 6);
840 tout
<< "Checking q2l OR q3\n";
841 enq
.set_query(Xapian::Query(Xapian::Query::OP_AND
, q1
,
842 Xapian::Query(Xapian::Query::OP_OR
, q2l
, q3
)));
843 mset_expect_order(enq
.get_mset(0, db
.get_doccount()), 8, 6);
845 tout
<< "Checking q2 OR q3l\n";
846 enq
.set_query(Xapian::Query(Xapian::Query::OP_AND
, q1
,
847 Xapian::Query(Xapian::Query::OP_OR
, q2
, q3l
)));
848 mset_expect_order(enq
.get_mset(0, db
.get_doccount()), 8, 6);
850 tout
<< "Checking v2 OR q3\n";
851 enq
.set_query(Xapian::Query(Xapian::Query::OP_AND
, q1
,
852 Xapian::Query(Xapian::Query::OP_OR
, v2
, q3
)));
853 mset_expect_order(enq
.get_mset(0, db
.get_doccount()), 8, 6);
855 tout
<< "Checking q2 OR v3\n";
856 enq
.set_query(Xapian::Query(Xapian::Query::OP_AND
, q1
,
857 Xapian::Query(Xapian::Query::OP_OR
, q2
, v3
)));
858 // Order of results in this one is different, because v3 gives no weight,
859 // both documents are in q2, and document 8 has a higher length.
860 mset_expect_order(enq
.get_mset(0, db
.get_doccount()), 6, 8);
864 /** Regression test for bug fixed in 1.2.1 and 1.0.21.
866 * We failed to mark the Btree as unmodified after cancel().
868 DEFINE_TESTCASE(failedreplace1
, chert
|| glass
) {
869 Xapian::WritableDatabase
db(get_writable_database());
870 Xapian::Document doc
;
872 db
.add_document(doc
);
873 Xapian::docid did
= db
.add_document(doc
);
875 doc
.add_term(string(1000, 'm'));
877 TEST_EXCEPTION(Xapian::InvalidArgumentError
, db
.replace_document(did
, doc
));
879 TEST_EQUAL(db
.get_doccount(), 0);
880 TEST_EQUAL(db
.get_termfreq("foo"), 0);
883 DEFINE_TESTCASE(failedreplace2
, chert
|| glass
) {
884 Xapian::WritableDatabase
db(get_writable_database("apitest_simpledata"));
886 Xapian::doccount db_size
= db
.get_doccount();
887 Xapian::Document doc
;
888 doc
.set_data("wibble");
890 doc
.add_value(0, "seven");
891 db
.add_document(doc
);
892 Xapian::docid did
= db
.add_document(doc
);
894 doc
.add_term(string(1000, 'm'));
896 doc
.add_value(0, "six");
897 TEST_EXCEPTION(Xapian::InvalidArgumentError
, db
.replace_document(did
, doc
));
899 TEST_EQUAL(db
.get_doccount(), db_size
);
900 TEST_EQUAL(db
.get_termfreq("foo"), 0);
903 /// Coverage for SelectPostList::skip_to().
904 DEFINE_TESTCASE(phrase3
, positional
) {
905 Xapian::Database db
= get_database("apitest_phrase");
907 static const char * const phrase_words
[] = { "phrase", "near" };
908 Xapian::Query
q(Xapian::Query::OP_NEAR
, phrase_words
, phrase_words
+ 2, 12);
909 q
= Xapian::Query(Xapian::Query::OP_AND_MAYBE
, Xapian::Query("pad"), q
);
911 Xapian::Enquire
enquire(db
);
912 enquire
.set_query(q
);
913 Xapian::MSet mset
= enquire
.get_mset(0, 5);
917 /// Check that get_mset(<large number>, 10) doesn't exhaust memory needlessly.
918 // Regression test for fix in 1.2.4.
919 DEFINE_TESTCASE(msetfirst2
, backend
) {
920 Xapian::Database
db(get_database("apitest_simpledata"));
921 Xapian::Enquire
enquire(db
);
922 enquire
.set_query(Xapian::Query("paragraph"));
924 // Before the fix, this tried to allocate too much memory.
925 mset
= enquire
.get_mset(0xfffffff0, 1);
926 TEST_EQUAL(mset
.get_firstitem(), 0xfffffff0);
927 // Check that the number of documents gets clamped too.
928 mset
= enquire
.get_mset(1, 0xfffffff0);
929 TEST_EQUAL(mset
.get_firstitem(), 1);
930 // Another regression test - MatchNothing used to give an MSet with
931 // get_firstitem() returning 0.
932 enquire
.set_query(Xapian::Query::MatchNothing
);
933 mset
= enquire
.get_mset(1, 1);
934 TEST_EQUAL(mset
.get_firstitem(), 1);
937 DEFINE_TESTCASE(bm25weight2
, backend
) {
938 Xapian::Database
db(get_database("etext"));
939 Xapian::Enquire
enquire(db
);
940 enquire
.set_query(Xapian::Query("the"));
941 enquire
.set_weighting_scheme(Xapian::BM25Weight(0, 0, 0, 0, 1));
942 Xapian::MSet mset
= enquire
.get_mset(0, 100);
943 TEST_REL(mset
.size(),>=,2);
944 double weight0
= mset
[0].get_weight();
945 for (Xapian::doccount i
= 1; i
!= mset
.size(); ++i
) {
946 TEST_EQUAL(weight0
, mset
[i
].get_weight());
950 DEFINE_TESTCASE(unigramlmweight2
, backend
) {
951 Xapian::Database
db(get_database("etext"));
952 Xapian::Enquire
enquire(db
);
953 enquire
.set_query(Xapian::Query("the"));
954 enquire
.set_weighting_scheme(Xapian::LMWeight());
955 Xapian::MSet mset
= enquire
.get_mset(0, 100);
956 TEST_REL(mset
.size(),>=,2);
959 DEFINE_TESTCASE(tradweight2
, backend
) {
960 Xapian::Database
db(get_database("etext"));
961 Xapian::Enquire
enquire(db
);
962 enquire
.set_query(Xapian::Query("the"));
963 enquire
.set_weighting_scheme(Xapian::TradWeight(0));
964 Xapian::MSet mset
= enquire
.get_mset(0, 100);
965 TEST_REL(mset
.size(),>=,2);
966 double weight0
= mset
[0].get_weight();
967 for (Xapian::doccount i
= 1; i
!= mset
.size(); ++i
) {
968 TEST_EQUAL(weight0
, mset
[i
].get_weight());
972 // Regression test for bug fix in 1.2.9.
973 DEFINE_TESTCASE(emptydb1
, backend
) {
974 Xapian::Database
db(get_database(string()));
975 static const Xapian::Query::op ops
[] = {
976 Xapian::Query::OP_AND
,
977 Xapian::Query::OP_OR
,
978 Xapian::Query::OP_AND_NOT
,
979 Xapian::Query::OP_XOR
,
980 Xapian::Query::OP_AND_MAYBE
,
981 Xapian::Query::OP_FILTER
,
982 Xapian::Query::OP_NEAR
,
983 Xapian::Query::OP_PHRASE
,
984 Xapian::Query::OP_ELITE_SET
,
985 Xapian::Query::OP_SYNONYM
,
986 Xapian::Query::OP_MAX
988 for (Xapian::Query::op op
: ops
) {
990 Xapian::Enquire
enquire(db
);
991 Xapian::Query
query(op
, Xapian::Query("a"), Xapian::Query("b"));
992 enquire
.set_query(query
);
993 Xapian::MSet mset
= enquire
.get_mset(0, 10);
994 TEST_EQUAL(mset
.get_matches_estimated(), 0);
995 TEST_EQUAL(mset
.get_matches_upper_bound(), 0);
996 TEST_EQUAL(mset
.get_matches_lower_bound(), 0);
1000 /** Test operators which should allow more than two arguments.
1002 * Regression test for bug with OP_FILTER fixed in 1.4.15, and also for bugs
1003 * with deleting the PostList which is currently set as the QueryOptimiser's
1004 * hint fixed in 1.4.15.
1006 DEFINE_TESTCASE(multiargop1
, backend
) {
1007 Xapian::Database
db(get_database("apitest_simpledata"));
1008 static const struct { unsigned hits
; Xapian::Query::op op
; } tests
[] = {
1009 { 0, Xapian::Query::OP_AND
},
1010 { 6, Xapian::Query::OP_OR
},
1011 { 0, Xapian::Query::OP_AND_NOT
},
1012 { 5, Xapian::Query::OP_XOR
},
1013 { 2, Xapian::Query::OP_AND_MAYBE
},
1014 { 0, Xapian::Query::OP_FILTER
},
1015 { 0, Xapian::Query::OP_NEAR
},
1016 { 0, Xapian::Query::OP_PHRASE
},
1017 { 6, Xapian::Query::OP_ELITE_SET
},
1018 { 6, Xapian::Query::OP_SYNONYM
},
1019 { 6, Xapian::Query::OP_MAX
}
1021 static const char* terms
[] = {"two", "all", "paragraph", "banana"};
1022 Xapian::Enquire
enquire(db
);
1023 for (auto& test
: tests
) {
1024 Xapian::Query::op op
= test
.op
;
1025 Xapian::doccount hits
= test
.hits
;
1026 tout
<< op
<< " should give " << hits
<< " hits\n";
1027 Xapian::Query
query(op
, terms
, terms
+ 4);
1028 enquire
.set_query(query
);
1029 Xapian::MSet mset
= enquire
.get_mset(0, 10);
1030 TEST_EQUAL(mset
.get_matches_estimated(), hits
);
1031 TEST_EQUAL(mset
.get_matches_upper_bound(), hits
);
1032 TEST_EQUAL(mset
.get_matches_lower_bound(), hits
);
1036 /// Test error opening non-existent stub databases.
1037 // Regression test for bug fixed in 1.3.1 and 1.2.11.
1038 DEFINE_TESTCASE(stubdb7
, !backend
) {
1039 TEST_EXCEPTION(Xapian::DatabaseNotFoundError
,
1040 Xapian::Database("nosuchdirectory", Xapian::DB_BACKEND_STUB
));
1041 TEST_EXCEPTION(Xapian::DatabaseNotFoundError
,
1042 Xapian::WritableDatabase("nosuchdirectory",
1043 Xapian::DB_OPEN
|Xapian::DB_BACKEND_STUB
));
1046 /// Test which checks the weights are as expected.
1047 // This runs for multi_* too, so serves to check that we get the same weights
1048 // with multiple databases as without.
1049 DEFINE_TESTCASE(msetweights1
, backend
) {
1050 Xapian::Database db
= get_database("apitest_simpledata");
1051 Xapian::Enquire
enq(db
);
1052 Xapian::Query
q(Xapian::Query::OP_OR
,
1053 Xapian::Query("paragraph"),
1054 Xapian::Query("word"));
1056 // 5 documents match, and the 4th and 5th have the same weight, so ask for
1057 // 4 as that's a good test that we get the right one in this case.
1058 Xapian::MSet mset
= enq
.get_mset(0, 4);
1060 static const struct { Xapian::docid did
; double wt
; } expected
[] = {
1061 { 2, 1.2058248004573934864 },
1062 { 4, 0.81127876655507624726 },
1063 { 1, 0.17309550762546158098 },
1064 { 3, 0.14609528172558261527 }
1067 TEST_EQUAL(mset
.size(), sizeof(expected
) / sizeof(expected
[0]));
1068 for (Xapian::doccount i
= 0; i
< mset
.size(); ++i
) {
1069 TEST_EQUAL(*mset
[i
], expected
[i
].did
);
1070 TEST_EQUAL_DOUBLE(mset
[i
].get_weight(), expected
[i
].wt
);
1073 // Now test a query which matches only even docids, so in the multi case
1074 // one subdatabase doesn't match.
1075 enq
.set_query(Xapian::Query("one"));
1076 mset
= enq
.get_mset(0, 3);
1078 static const struct { Xapian::docid did
; double wt
; } expected2
[] = {
1079 { 6, 0.73354729848273669823 },
1080 { 2, 0.45626501034348893038 }
1083 TEST_EQUAL(mset
.size(), sizeof(expected2
) / sizeof(expected2
[0]));
1084 for (Xapian::doccount i
= 0; i
< mset
.size(); ++i
) {
1085 TEST_EQUAL(*mset
[i
], expected2
[i
].did
);
1086 TEST_EQUAL_DOUBLE(mset
[i
].get_weight(), expected2
[i
].wt
);
1090 DEFINE_TESTCASE(itorskiptofromend1
, backend
) {
1091 Xapian::Database db
= get_database("apitest_simpledata");
1093 Xapian::TermIterator t
= db
.termlist_begin(1);
1095 TEST(t
== db
.termlist_end(1));
1096 // This worked in 1.2.x but segfaulted in 1.3.1.
1097 t
.skip_to("zzzzzz");
1099 Xapian::PostingIterator p
= db
.postlist_begin("one");
1101 TEST(p
== db
.postlist_end("one"));
1102 // This segfaulted prior to 1.3.2.
1105 Xapian::PositionIterator i
= db
.positionlist_begin(6, "one");
1107 TEST(i
== db
.positionlist_end(6, "one"));
1108 // This segfaulted prior to 1.3.2.
1111 Xapian::ValueIterator v
= db
.valuestream_begin(1);
1113 TEST(v
== db
.valuestream_end(1));
1114 // These segfaulted prior to 1.3.2.
1119 /// Check handling of invalid block sizes.
1120 // Regression test for bug fixed in 1.2.17 and 1.3.2 - the size gets fixed
1121 // but the uncorrected size was passed to the base file. Also, abort() was
1123 DEFINE_TESTCASE(blocksize1
, chert
|| glass
) {
1124 string db_dir
= "." + get_dbtype();
1125 mkdir(db_dir
.c_str(), 0755);
1126 db_dir
+= "/db__blocksize1";
1128 if (get_dbtype() == "chert") {
1129 flags
= Xapian::DB_CREATE
|Xapian::DB_BACKEND_CHERT
;
1131 flags
= Xapian::DB_CREATE
|Xapian::DB_BACKEND_GLASS
;
1133 static const unsigned bad_sizes
[] = {
1134 65537, 8000, 2000, 1024, 16, 7, 3, 1, 0
1136 for (size_t i
= 0; i
< sizeof(bad_sizes
) / sizeof(bad_sizes
[0]); ++i
) {
1137 size_t block_size
= bad_sizes
[i
];
1139 Xapian::WritableDatabase
db(db_dir
, flags
, block_size
);
1140 Xapian::Document doc
;
1141 doc
.add_term("XYZ");
1142 doc
.set_data("foo");
1143 db
.add_document(doc
);
1148 /// Feature test for Xapian::DB_NO_TERMLIST.
1149 DEFINE_TESTCASE(notermlist1
, glass
) {
1150 string db_dir
= "." + get_dbtype();
1151 mkdir(db_dir
.c_str(), 0755);
1152 db_dir
+= "/db__notermlist1";
1153 int flags
= Xapian::DB_CREATE
|Xapian::DB_NO_TERMLIST
;
1154 if (get_dbtype() == "chert") {
1155 flags
|= Xapian::DB_BACKEND_CHERT
;
1157 flags
|= Xapian::DB_BACKEND_GLASS
;
1160 Xapian::WritableDatabase
db(db_dir
, flags
);
1161 Xapian::Document doc
;
1162 doc
.add_term("hello");
1163 doc
.add_value(42, "answer");
1164 db
.add_document(doc
);
1166 TEST(!file_exists(db_dir
+ "/termlist.glass"));
1167 TEST_EXCEPTION(Xapian::FeatureUnavailableError
, db
.termlist_begin(1));
1170 /// Regression test for bug starting a new glass freelist block.
1171 DEFINE_TESTCASE(newfreelistblock1
, writable
) {
1172 Xapian::Document doc
;
1173 doc
.add_term("foo");
1174 for (int i
= 100; i
< 120; ++i
) {
1175 doc
.add_term(str(i
));
1178 Xapian::WritableDatabase
wdb(get_writable_database());
1179 for (int j
= 0; j
< 50; ++j
) {
1180 wdb
.add_document(doc
);
1184 for (int k
= 0; k
< 1000; ++k
) {
1185 wdb
.add_document(doc
);
1190 /** Check that the parent directory for the database doesn't need to be
1191 * writable. Regression test for early versions on the glass new btree
1192 * branch which failed to append a "/" when generating a temporary filename
1193 * from the database directory.
1195 DEFINE_TESTCASE(readonlyparentdir1
, chert
|| glass
) {
1196 #if !defined __WIN32__ && !defined __CYGWIN__ && !defined __OS2__
1197 string path
= get_named_writable_database_path("readonlyparentdir1");
1198 // Fix permissions if the previous test was killed.
1199 (void)chmod(path
.c_str(), 0700);
1200 mkdir(path
.c_str(), 0777);
1201 mkdir((path
+ "/sub").c_str(), 0777);
1202 Xapian::WritableDatabase db
= get_named_writable_database("readonlyparentdir1/sub");
1203 TEST(chmod(path
.c_str(), 0500) == 0);
1205 Xapian::Document doc
;
1206 doc
.add_term("hello");
1207 doc
.set_data("some text");
1208 db
.add_document(doc
);
1211 // Attempt to fix the permissions, otherwise things like "rm -rf" on
1212 // the source tree will fail.
1213 (void)chmod(path
.c_str(), 0700);
1216 TEST(chmod(path
.c_str(), 0700) == 0);
1221 make_phrasebug1_db(Xapian::WritableDatabase
&db
, const string
&)
1223 Xapian::Document doc
;
1224 doc
.add_posting("hurricane", 199881);
1225 doc
.add_posting("hurricane", 203084);
1226 doc
.add_posting("katrina", 199882);
1227 doc
.add_posting("katrina", 202473);
1228 doc
.add_posting("katrina", 203085);
1229 db
.add_document(doc
);
1232 /// Regression test for ticket#653, fixed in 1.3.2 and 1.2.19.
1233 DEFINE_TESTCASE(phrasebug1
, positional
) {
1234 Xapian::Database db
= get_database("phrasebug1", make_phrasebug1_db
);
1235 static const char * const qterms
[] = { "katrina", "hurricane" };
1236 Xapian::Enquire
e(db
);
1237 Xapian::Query
q(Xapian::Query::OP_PHRASE
, qterms
, qterms
+ 2, 5);
1239 Xapian::MSet mset
= e
.get_mset(0, 100);
1240 TEST_EQUAL(mset
.size(), 0);
1241 static const char * const qterms2
[] = { "hurricane", "katrina" };
1242 Xapian::Query
q2(Xapian::Query::OP_PHRASE
, qterms2
, qterms2
+ 2, 5);
1244 mset
= e
.get_mset(0, 100);
1245 TEST_EQUAL(mset
.size(), 1);
1248 /// Feature test for Xapian::DB_RETRY_LOCK
1249 DEFINE_TESTCASE(retrylock1
, writable
&& path
) {
1250 // FIXME: Can't see an easy way to test this for remote databases - the
1251 // harness doesn't seem to provide a suitable way to reopen a remote.
1252 #if defined HAVE_FORK && defined HAVE_SOCKETPAIR
1254 if (socketpair(AF_UNIX
, SOCK_STREAM
|SOCK_CLOEXEC
, PF_UNSPEC
, fds
) < 0) {
1255 FAIL_TEST("socketpair() failed");
1257 if (fds
[1] >= FD_SETSIZE
)
1258 SKIP_TEST("socketpair() gave fd >= FD_SETSIZE");
1259 if (fcntl(fds
[1], F_SETFL
, O_NONBLOCK
) < 0)
1260 FAIL_TEST("fcntl() failed to set O_NONBLOCK");
1261 pid_t child
= fork();
1263 FAIL_TEST("fork() failed");
1265 // Wait for signal that parent has opened the database.
1267 while (read(fds
[0], &ch
, 1) < 0) { }
1270 Xapian::WritableDatabase
db2(get_named_writable_database_path("retrylock1"),
1271 Xapian::DB_OPEN
|Xapian::DB_RETRY_LOCK
);
1272 if (write(fds
[0], "y", 1)) { }
1273 } catch (const Xapian::DatabaseLockError
&) {
1274 if (write(fds
[0], "l", 1)) { }
1275 } catch (const Xapian::Error
&e
) {
1276 const string
& m
= e
.get_description();
1277 if (write(fds
[0], m
.data(), m
.size())) { }
1279 if (write(fds
[0], "o", 1)) { }
1286 Xapian::WritableDatabase db
= get_named_writable_database("retrylock1");
1287 if (write(fds
[1], "", 1) != 1)
1288 FAIL_TEST("Failed to signal to child process");
1291 int r
= read(fds
[1], result
, sizeof(result
));
1293 if (errno
== EAGAIN
) {
1298 tout
<< "errno=" << errno
<< ": " << errno_to_string(errno
) << '\n';
1302 } else if (r
>= 1) {
1303 if (result
[0] == 'y') {
1304 // Child process managed to also get write lock!
1316 kill(child
, SIGKILL
);
1318 while (waitpid(child
, &status
, 0) < 0) {
1319 if (errno
!= EINTR
) break;
1324 if (result
[0] == 'y') {
1331 FD_SET(fds
[1], &fdset
);
1332 int sr
= select(fds
[1] + 1, &fdset
, NULL
, NULL
, &tv
);
1337 } else if (sr
== -1) {
1338 if (errno
== EINTR
|| errno
== EAGAIN
)
1340 tout
<< "select() failed with errno=" << errno
<< ": "
1341 << errno_to_string(errno
) << '\n';
1345 r
= read(fds
[1], result
, sizeof(result
));
1348 tout
<< "read() failed with errno=" << errno
<< ": "
1349 << errno_to_string(errno
) << '\n';
1352 } else if (r
== 0) {
1362 kill(child
, SIGKILL
);
1364 while (waitpid(child
, &status
, 0) < 0) {
1365 if (errno
!= EINTR
) break;
1368 tout
<< string(result
, r
) << '\n';
1369 TEST_EQUAL(result
[0], 'y');
1373 // Opening a WritableDatabase with low fds available - it should avoid them.
1374 DEFINE_TESTCASE(dbfilefd012
, writable
&& !remote
) {
1375 #if !defined __WIN32__ && !defined __CYGWIN__ && !defined __OS2__
1377 for (int i
= 0; i
< 3; ++i
) {
1381 for (int j
= 0; j
< 3; ++j
) {
1383 TEST_REL(lseek(j
, 0, SEEK_CUR
), <, 0);
1384 TEST_EQUAL(errno
, EBADF
);
1387 Xapian::WritableDatabase db
= get_writable_database();
1389 // Check we didn't use any of those low fds for tables, as that risks
1390 // data corruption if some other code in the same process tries to
1391 // write to them (see #651).
1392 for (int fd
= 0; fd
< 3; ++fd
) {
1393 // Check that the fd is still closed, or isn't open O_RDWR (the
1394 // lock file gets opened O_WRONLY), or it's a pipe (if we're using
1395 // a child process to hold a non-OFD fcntl lock).
1396 int flags
= fcntl(fd
, F_GETFL
);
1398 TEST_EQUAL(errno
, EBADF
);
1399 } else if ((flags
& O_ACCMODE
) != O_RDWR
) {
1403 TEST_NOT_EQUAL(fstat(fd
, &sb
), -1);
1405 TEST(S_ISSOCK(sb
.st_mode
));
1407 // If we can't check it is a socket, at least check it is not a
1409 TEST(!S_ISREG(sb
.st_mode
));
1414 for (int j
= 0; j
< 3; ++j
) {
1421 for (int j
= 0; j
< 3; ++j
) {
1428 /// Regression test for #675, fixed in 1.3.3 and 1.2.21.
1429 DEFINE_TESTCASE(cursorbug1
, writable
&& path
) {
1430 Xapian::WritableDatabase wdb
= get_writable_database();
1431 Xapian::Database db
= get_writable_database_as_database();
1432 Xapian::Enquire
enq(db
);
1433 enq
.set_query(Xapian::Query::MatchAll
);
1435 // The original problem triggers for chert and glass on repeat==7.
1436 for (int repeat
= 0; repeat
< 10; ++repeat
) {
1438 tout
<< "iteration #" << repeat
<< '\n';
1440 const int ITEMS
= 10;
1441 int free_id
= db
.get_doccount();
1442 int offset
= max(free_id
, ITEMS
* 2) - (ITEMS
* 2);
1443 int limit
= offset
+ (ITEMS
* 2);
1445 mset
= enq
.get_mset(offset
, limit
);
1446 for (Xapian::MSetIterator m1
= mset
.begin(); m1
!= mset
.end(); ++m1
) {
1447 (void)m1
.get_document().get_value(0);
1450 for (int i
= free_id
; i
<= free_id
+ ITEMS
; ++i
) {
1451 Xapian::Document doc
;
1452 const string
& id
= str(i
);
1453 string qterm
= "Q" + id
;
1454 doc
.add_value(0, id
);
1455 doc
.add_boolean_term(qterm
);
1456 wdb
.replace_document(qterm
, doc
);
1461 mset
= enq
.get_mset(offset
, limit
);
1462 for (Xapian::MSetIterator m2
= mset
.begin(); m2
!= mset
.end(); ++m2
) {
1463 (void)m2
.get_document().get_value(0);
1468 // Regression test for #674, fixed in 1.2.21 and 1.3.3.
1469 DEFINE_TESTCASE(sortvalue2
, backend
) {
1470 Xapian::Database db
= get_database("apitest_simpledata");
1471 db
.add_database(get_database("apitest_simpledata2"));
1472 Xapian::Enquire
enq(db
);
1473 enq
.set_query(Xapian::Query::MatchAll
);
1474 enq
.set_sort_by_value(0, false);
1475 Xapian::MSet mset
= enq
.get_mset(0, 50);
1477 // Check all results are in key order - the bug was that they were sorted
1478 // by docid instead with multiple remote databases.
1480 for (Xapian::MSetIterator i
= mset
.begin(); i
!= mset
.end(); ++i
) {
1481 string key
= db
.get_document(*i
).get_value(0);
1482 TEST(old_key
<= key
);
1487 /// Check behaviour of Enquire::get_query().
1488 DEFINE_TESTCASE(enquiregetquery1
, backend
) {
1489 Xapian::Database db
= get_database("apitest_simpledata");
1490 Xapian::Enquire
enq(db
);
1491 TEST_EQUAL(enq
.get_query().get_description(), "Query()");
1494 DEFINE_TESTCASE(embedded1
, singlefile
) {
1495 // In reality you should align the embedded database to a multiple of
1496 // database block size, but any offset is meant to work.
1497 off_t offset
= 1234;
1499 Xapian::Database db
= get_database("apitest_simpledata");
1500 const string
& db_path
= get_database_path("apitest_simpledata");
1501 const string
& tmp_path
= db_path
+ "-embedded";
1502 ofstream
out(tmp_path
, fstream::trunc
|fstream::binary
);
1504 out
<< ifstream(db_path
, fstream::binary
).rdbuf();
1508 int fd
= open(tmp_path
.c_str(), O_RDONLY
|O_BINARY
);
1509 lseek(fd
, offset
, SEEK_SET
);
1510 Xapian::Database
db_embedded(fd
);
1511 TEST_EQUAL(db
.get_doccount(), db_embedded
.get_doccount());
1515 int fd
= open(tmp_path
.c_str(), O_RDONLY
|O_BINARY
);
1516 lseek(fd
, offset
, SEEK_SET
);
1517 size_t check_errors
=
1518 Xapian::Database::check(fd
, Xapian::DBCHECK_SHOW_STATS
, &tout
);
1519 TEST_EQUAL(check_errors
, 0);
1523 /// Regression test for bug fixed in 1.3.7.
1524 DEFINE_TESTCASE(exactxor1
, backend
) {
1525 Xapian::Database db
= get_database("apitest_simpledata");
1526 Xapian::Enquire
enq(db
);
1528 static const char * const words
[4] = {
1529 "blank", "test", "paragraph", "banana"
1531 Xapian::Query
q(Xapian::Query::OP_XOR
, words
, words
+ 4);
1533 enq
.set_weighting_scheme(Xapian::BoolWeight());
1534 Xapian::MSet mset
= enq
.get_mset(0, 0);
1535 // A reversed conditional gave us 5 in this case.
1536 TEST_EQUAL(mset
.get_matches_upper_bound(), 6);
1537 // Test improved lower bound in 1.3.7 (earlier versions gave 0).
1538 TEST_EQUAL(mset
.get_matches_lower_bound(), 2);
1540 static const char * const words2
[4] = {
1541 "queri", "test", "paragraph", "word"
1543 Xapian::Query
q2(Xapian::Query::OP_XOR
, words2
, words2
+ 4);
1545 enq
.set_weighting_scheme(Xapian::BoolWeight());
1546 mset
= enq
.get_mset(0, 0);
1547 // A reversed conditional gave us 6 in this case.
1548 TEST_EQUAL(mset
.get_matches_upper_bound(), 5);
1549 // Test improved lower bound in 1.3.7 (earlier versions gave 0).
1550 TEST_EQUAL(mset
.get_matches_lower_bound(), 1);
1553 /// Feature test for Database::get_revision().
1554 DEFINE_TESTCASE(getrevision1
, chert
|| glass
) {
1555 Xapian::WritableDatabase db
= get_writable_database();
1556 TEST_EQUAL(db
.get_revision(), 0);
1558 TEST_EQUAL(db
.get_revision(), 0);
1559 Xapian::Document doc
;
1560 doc
.add_term("hello");
1561 db
.add_document(doc
);
1562 TEST_EQUAL(db
.get_revision(), 0);
1564 TEST_EQUAL(db
.get_revision(), 1);
1566 TEST_EQUAL(db
.get_revision(), 1);
1567 db
.add_document(doc
);
1569 TEST_EQUAL(db
.get_revision(), 2);
1572 /// Check get_revision() on an empty database reports 0. (Since 1.5.0)
1573 DEFINE_TESTCASE(getrevision2
, !backend
) {
1574 Xapian::Database db
;
1575 TEST_EQUAL(db
.get_revision(), 0);
1576 Xapian::Database wdb
;
1577 TEST_EQUAL(wdb
.get_revision(), 0);
1580 /// Feature test for DOC_ASSUME_VALID.
1581 DEFINE_TESTCASE(getdocumentlazy1
, backend
) {
1582 Xapian::Database db
= get_database("apitest_simpledata");
1583 Xapian::Document doc_lazy
= db
.get_document(2, Xapian::DOC_ASSUME_VALID
);
1584 Xapian::Document doc
= db
.get_document(2);
1585 TEST_EQUAL(doc
.get_data(), doc_lazy
.get_data());
1586 TEST_EQUAL(doc
.get_value(0), doc_lazy
.get_value(0));
1589 /// Feature test for DOC_ASSUME_VALID for a docid that doesn't actually exist.
1590 DEFINE_TESTCASE(getdocumentlazy2
, backend
) {
1591 Xapian::Database db
= get_database("apitest_simpledata");
1592 Xapian::Document doc
;
1594 doc
= db
.get_document(db
.get_lastdocid() + 1, Xapian::DOC_ASSUME_VALID
);
1595 } catch (const Xapian::DocNotFoundError
&) {
1596 // DOC_ASSUME_VALID is really just a hint, so ignoring is OK (the
1597 // remote backend currently does).
1599 TEST(doc
.get_data().empty());
1600 TEST_EXCEPTION(Xapian::DocNotFoundError
,
1601 doc
= db
.get_document(db
.get_lastdocid() + 1);
1606 gen_uniqterms_gt_doclen_db(Xapian::WritableDatabase
& db
, const string
&)
1608 Xapian::Document doc
;
1609 doc
.add_term("foo");
1610 doc
.add_boolean_term("bar");
1611 db
.add_document(doc
);
1612 Xapian::Document doc2
;
1613 doc2
.add_posting("foo", 0, 2);
1614 doc2
.add_term("foo2");
1615 doc2
.add_boolean_term("baz");
1616 doc2
.add_boolean_term("baz2");
1617 db
.add_document(doc2
);
1620 DEFINE_TESTCASE(getuniqueterms1
, backend
) {
1621 Xapian::Database db
=
1622 get_database("uniqterms_gt_doclen", gen_uniqterms_gt_doclen_db
);
1624 auto unique1
= db
.get_unique_terms(1);
1625 TEST_REL(unique1
, <=, db
.get_doclength(1));
1626 TEST_REL(unique1
, <, db
.get_document(1).termlist_count());
1627 // Ideally it'd be equal to 1, and in this case it is, but the current
1628 // backends can't always efficiently ensure an exact answer.
1629 TEST_REL(unique1
, >=, 1);
1631 auto unique2
= db
.get_unique_terms(2);
1632 TEST_REL(unique2
, <=, db
.get_doclength(2));
1633 TEST_REL(unique2
, <, db
.get_document(2).termlist_count());
1634 // Ideally it'd be equal to 2, but the current backends can't always
1635 // efficiently ensure an exact answer and here it is actually 3.
1636 TEST_REL(unique2
, >=, 2);
1639 /** Regression test for bug fixed in 1.4.6.
1641 * OP_NEAR would think a term without positional information occurred at
1642 * position 1 if it had the lowest term frequency amongst the OP_NEAR's
1645 DEFINE_TESTCASE(nopositionbug1
, backend
) {
1646 Xapian::Database db
=
1647 get_database("uniqterms_gt_doclen", gen_uniqterms_gt_doclen_db
);
1649 // Test both orders.
1650 static const char* const terms1
[] = { "foo", "baz" };
1651 static const char* const terms2
[] = { "baz", "foo" };
1653 Xapian::Enquire
enq(db
);
1654 enq
.set_query(Xapian::Query(Xapian::Query::OP_NEAR
,
1655 begin(terms1
), end(terms1
), 10));
1656 TEST_EQUAL(enq
.get_mset(0, 5).size(), 0);
1658 enq
.set_query(Xapian::Query(Xapian::Query::OP_NEAR
,
1659 begin(terms2
), end(terms2
), 10));
1660 TEST_EQUAL(enq
.get_mset(0, 5).size(), 0);
1662 enq
.set_query(Xapian::Query(Xapian::Query::OP_PHRASE
,
1663 begin(terms1
), end(terms1
), 10));
1664 TEST_EQUAL(enq
.get_mset(0, 5).size(), 0);
1666 enq
.set_query(Xapian::Query(Xapian::Query::OP_PHRASE
,
1667 begin(terms2
), end(terms2
), 10));
1668 TEST_EQUAL(enq
.get_mset(0, 5).size(), 0);
1670 // Exercise exact phrase case too.
1671 enq
.set_query(Xapian::Query(Xapian::Query::OP_PHRASE
,
1672 begin(terms1
), end(terms1
), 2));
1673 TEST_EQUAL(enq
.get_mset(0, 5).size(), 0);
1675 enq
.set_query(Xapian::Query(Xapian::Query::OP_PHRASE
,
1676 begin(terms2
), end(terms2
), 2));
1677 TEST_EQUAL(enq
.get_mset(0, 5).size(), 0);
1680 /** Regression test for bug with get_mset(0, 0, N) (N > 0).
1682 * Fixed in 1.5.0 and 1.4.6.
1684 DEFINE_TESTCASE(checkatleast4
, backend
) {
1685 Xapian::Database db
= get_database("apitest_simpledata");
1686 Xapian::Enquire
enq(db
);
1687 enq
.set_query(Xapian::Query("paragraph"));
1688 // This used to cause access to an element in an empty vector.
1689 Xapian::MSet mset
= enq
.get_mset(0, 0, 4);
1690 TEST_EQUAL(mset
.size(), 0);
1693 /// Regression test for glass freelist leak fixed in 1.4.6 and 1.5.0.
1694 DEFINE_TESTCASE(freelistleak1
, check
) {
1695 auto path
= get_database_path("freelistleak1",
1696 [](Xapian::WritableDatabase
& wdb
,
1699 wdb
.set_metadata("foo", "bar");
1701 Xapian::Document doc
;
1702 doc
.add_term("baz");
1703 wdb
.add_document(doc
);
1705 size_t check_errors
=
1706 Xapian::Database::check(path
, Xapian::DBCHECK_SHOW_STATS
, &tout
);
1707 TEST_EQUAL(check_errors
, 0);
1710 /// Regression test for split position handling - broken in 1.4.8.
1711 DEFINE_TESTCASE(splitpostings1
, writable
) {
1712 Xapian::WritableDatabase db
= get_writable_database();
1713 Xapian::Document doc
;
1714 // Add postings to create a split internally.
1715 for (Xapian::termpos pos
= 0; pos
<= 100; pos
+= 10) {
1716 doc
.add_posting("foo", pos
);
1718 for (Xapian::termpos pos
= 5; pos
<= 100; pos
+= 20) {
1719 doc
.add_posting("foo", pos
);
1721 db
.add_document(doc
);
1724 Xapian::termpos expect
= 0;
1725 Xapian::termpos pos
= 0;
1726 for (auto p
= db
.positionlist_begin(1, "foo");
1727 p
!= db
.positionlist_end(1, "foo"); ++p
) {
1728 TEST_REL(expect
, <=, 100);
1730 TEST_EQUAL(pos
, expect
);
1732 if (expect
% 20 == 15) expect
+= 5;
1734 TEST_EQUAL(pos
, 100);
1737 /// Feature tests for Database::size().
1738 DEFINE_TESTCASE(multidb1
, backend
) {
1739 Xapian::Database db
;
1740 TEST_EQUAL(db
.size(), 0);
1741 Xapian::Database db2
= get_database("apitest_simpledata");
1742 TEST(db2
.size() != 0);
1743 db
.add_database(db2
);
1744 TEST_EQUAL(db
.size(), db2
.size());
1745 db
.add_database(db2
);
1746 // Regression test for bug introduced and fixed in git master before 1.5.0.
1747 // Adding a multi database to an empty database incorrectly worked just
1748 // like assigning the database object. The list of shards is now copied
1750 TEST_EQUAL(db
.size(), db2
.size() * 2);
1751 db
.add_database(Xapian::Database());
1752 TEST_EQUAL(db
.size(), db2
.size() * 2);
1755 // Test that all the terms returned exist.
1756 DEFINE_TESTCASE(allterms7
, backend
) {
1757 Xapian::Database db
= get_database("etext");
1758 for (auto i
= db
.allterms_begin(); i
!= db
.allterms_end(); ++i
) {
1760 TEST(db
.get_termfreq(term
) > 0);
1761 TEST(db
.postlist_begin(term
) != db
.postlist_end(term
));
1765 /* Test searching for non-existent terms returns zero results.
1767 * Regression test for GlassTable::readahead_key() throwing "Key too long"
1768 * error if passed an oversized key.
1770 DEFINE_TESTCASE(nosuchterm
, backend
) {
1771 Xapian::Database db
= get_database("apitest_simpledata");
1772 Xapian::Enquire enquire
{db
};
1773 // Test up to a length longer than any backend supports.
1774 const unsigned MAX_LEN
= 300;
1776 term
.reserve(MAX_LEN
);
1777 while (term
.size() < MAX_LEN
) {
1779 enquire
.set_query(Xapian::Query(term
));
1780 TEST_EQUAL(enquire
.get_mset(0, 10).size(), 0);
1784 // Test exception for check() on remote via stub.
1785 DEFINE_TESTCASE(unsupportedcheck1
, path
) {
1786 mkdir(".stub", 0755);
1787 const char* stubpath
= ".stub/unsupportedcheck1";
1788 ofstream
out(stubpath
);
1789 TEST(out
.is_open());
1790 out
<< "remote :" << BackendManager::get_xapian_progsrv_command()
1791 << ' ' << get_database_path("apitest_simpledata") << '\n';
1794 TEST_EXCEPTION(Xapian::UnimplementedError
,
1795 Xapian::Database::check(stubpath
));
1798 // Test exception for check() on inmemory via stub.
1799 DEFINE_TESTCASE(unsupportedcheck2
, inmemory
) {
1800 mkdir(".stub", 0755);
1801 const char* stubpath
= ".stub/unsupportedcheck2";
1802 ofstream
out(stubpath
);
1803 TEST(out
.is_open());
1804 out
<< "inmemory\n";
1807 TEST_EXCEPTION(Xapian::UnimplementedError
,
1808 Xapian::Database::check(stubpath
));
1811 // Test exception for passing empty filename to check().
1812 DEFINE_TESTCASE(unsupportedcheck3
, !backend
) {
1813 // Regression test, exception was DatabaseOpeningError with description:
1814 // Failed to rewind file descriptor -1 (Bad file descriptor)
1816 Xapian::Database::check(string());
1817 } catch (const Xapian::DatabaseOpeningError
& e
) {
1818 string enoent_msg
= errno_to_string(ENOENT
);
1819 TEST_EQUAL(e
.get_error_string(), enoent_msg
);
1823 // Test handling of corrupt DB with out of range levels count.
1824 // Regression test for #824, fixed in 1.4.25.
1825 DEFINE_TESTCASE(corruptdblevels1
, glass
) {
1827 test_driver::get_srcdir() + "/testdata/glass_corrupt_level_db1";
1829 TEST_EXCEPTION(Xapian::DatabaseCorruptError
,
1830 Xapian::Database
db(db_path
));
1832 TEST_EXCEPTION(Xapian::DatabaseCorruptError
,
1833 Xapian::Database::check(db_path
));
1835 db_path
.back() = '2';
1837 TEST_EXCEPTION(Xapian::DatabaseCorruptError
,
1838 Xapian::Database
db(db_path
));
1840 TEST_EXCEPTION(Xapian::DatabaseCorruptError
,
1841 Xapian::Database::check(db_path
));