Improve fix for invalid Btree level count
[xapian.git] / xapian-core / tests / api_backend.cc
bloba5ccf1e7ce451c658fa0a87eba7d8cc36df65f6b
1 /** @file
2 * @brief Backend-related tests.
3 */
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
20 * USA
23 #include <config.h>
25 #include "api_backend.h"
27 #define XAPIAN_DEPRECATED(X) X
28 #include <xapian.h>
30 #include "backendmanager.h"
31 #include "errno_to_string.h"
32 #include "filetests.h"
33 #include "str.h"
34 #include "testrunner.h"
35 #include "testsuite.h"
36 #include "testutils.h"
37 #include "unixcmds.h"
39 #include "apitest.h"
41 #include "safefcntl.h"
42 #include "safesysstat.h"
43 #include "safeunistd.h"
44 #ifdef HAVE_SOCKETPAIR
45 # include "safesyssocket.h"
46 # include <signal.h>
47 # include "safesyswait.h"
48 #endif
50 #include <cerrno>
51 #include <fstream>
52 #include <iterator>
54 using namespace std;
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);
60 try {
61 Xapian::WritableDatabase db = get_named_writable_database("lockfileumask1");
63 string path = get_named_writable_database_path("lockfileumask1");
64 path += "/flintlock";
66 struct stat statbuf;
67 TEST(stat(path.c_str(), &statbuf) == 0);
68 TEST_EQUAL(statbuf.st_mode & 0777, 0644);
69 } catch (...) {
70 umask(old_umask);
71 throw;
74 umask(old_umask);
75 #endif
78 /// Check that the backend handles total document length > 0xffffffff.
79 DEFINE_TESTCASE(totaldoclen1, writable) {
80 Xapian::WritableDatabase db = get_writable_database();
81 Xapian::Document doc;
82 doc.add_posting("foo", 1, 2000000000);
83 db.add_document(doc);
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);
89 db.commit();
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);
99 db.commit();
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
115 // for 32-bit...
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);
129 db1.commit();
131 Xapian::Database db2 = get_writable_database_as_database();
133 Xapian::Database db;
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
144 // multi.
145 bool multi = (db1.size() > 1);
146 for (Xapian::MSetIterator i = mymset.begin(); i != mymset.end(); ++i) {
147 doc = i.get_document();
148 if (!multi)
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);
167 } else {
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);
176 } else {
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
184 // 1.3.2.
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);
203 } else {
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);
212 } else {
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);
237 a.add_term("T");
238 enq.set_query(Xapian::Query("T"));
240 db.replace_document(2, a);
241 db.commit();
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());
254 db.commit();
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);
264 db.commit();
265 db.replace_document(1, doc);
266 db.replace_document(3, doc);
267 TEST_EQUAL(db.get_document(3).get_value(0), "value");
268 db.commit();
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);
276 try {
277 // With fd 0 available.
278 close(0);
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.
285 close(1);
287 Xapian::WritableDatabase db = get_writable_database();
288 TEST_EXCEPTION(Xapian::DatabaseLockError,
289 (void)get_writable_database_again());
291 // With fd 1 available.
292 dup2(old_stdin, 0);
294 Xapian::WritableDatabase db = get_writable_database();
295 TEST_EXCEPTION(Xapian::DatabaseLockError,
296 (void)get_writable_database_again());
298 } catch (...) {
299 dup2(old_stdin, 0);
300 dup2(old_stdout, 1);
301 close(old_stdin);
302 close(old_stdout);
303 throw;
306 dup2(old_stdout, 1);
307 close(old_stdin);
308 close(old_stdout);
309 #endif
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);
318 TEST(fd != -1);
319 try {
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)
324 } catch (...) {
325 close(fd);
326 throw;
328 close(fd);
331 /// Feature tests for Database::locked().
332 DEFINE_TESTCASE(testlock1, chert || glass) {
333 Xapian::Database rdb;
334 TEST(!rdb.locked());
336 Xapian::WritableDatabase db = get_named_writable_database("testlock1");
337 TEST(db.locked());
338 Xapian::Database db_as_database = db;
339 TEST(db_as_database.locked());
340 TEST(!rdb.locked());
341 rdb = get_writable_database_as_database();
342 TEST(db.locked());
343 TEST(db_as_database.locked());
344 try {
345 TEST(rdb.locked());
346 } catch (const Xapian::FeatureUnavailableError&) {
347 SKIP_TEST("Database::locked() not supported on this platform");
349 db_as_database = rdb;
350 TEST(db.locked());
351 TEST(db_as_database.locked());
352 TEST(rdb.locked());
353 db_as_database.close();
354 TEST(db.locked());
355 TEST(rdb.locked());
356 // After close(), locked() should either work as if close() hadn't been
357 // called or throw Xapian::DatabaseClosedError.
358 try {
359 TEST(db_as_database.locked());
360 } catch (const Xapian::DatabaseClosedError&) {
362 db.close();
363 TEST(!rdb.locked());
364 try {
365 TEST(!db_as_database.locked());
366 } catch (const Xapian::DatabaseClosedError&) {
369 TEST(!rdb.locked());
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");
378 TEST(!db.locked());
379 db.close();
380 TEST(!db.locked());
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");
397 TEST(db.locked());
398 db.close();
399 TEST(!db.locked());
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
407 // lock.
408 TEST(db.locked());
409 db.close();
410 try {
411 TEST(!db.locked());
412 } catch (const Xapian::FeatureUnavailableError&) {
413 SKIP_TEST("Database::locked() not supported on this platform");
417 class CheckMatchDecider : public Xapian::MatchDecider {
418 mutable bool called;
420 public:
421 CheckMatchDecider() : called(false) { }
423 bool operator()(const Xapian::Document &) const {
424 called = true;
425 return true;
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;
438 Xapian::MSet mset;
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
450 // check there.
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);
462 db.commit();
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);
479 db.flush();
480 TEST(rodb.reopen());
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);
504 db.flush();
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);
523 db.commit();
524 Xapian::Document doc = db.get_document(1);
525 doc.clear_terms();
526 doc.add_term("falafel");
527 db.replace_document(1, doc);
528 db.commit();
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");
543 doc.add_term("abc");
544 doc.add_term("def");
545 doc.add_term("ghi");
546 const int N = 500;
547 for (int i = 0; i < N; ++i) {
548 db.add_document(doc);
550 db.commit();
552 Xapian::Database rodb(get_writable_database_as_database());
553 db.add_document(doc);
554 db.commit();
556 db.add_document(doc);
557 db.commit();
559 db.add_document(doc);
560 try {
561 TEST_EQUAL(*rodb.termlist_begin(N - 1), "abc");
562 FAIL_TEST("Expected DatabaseModifiedError wasn't thrown");
563 } catch (const Xapian::DatabaseModifiedError &) {
566 try {
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;
581 doc.add_term("foo");
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);
589 wdb.commit();
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);
597 wdb.commit();
598 (void)queryparser.parse_query("1", queryparser.FLAG_PARTIAL);
600 SKIP_TEST("didn't manage to trigger DatabaseModifiedError");
604 static void
605 make_msize1_db(Xapian::WritableDatabase &db, const string &)
607 const char * value0 =
608 "ABBCDEFGHIJKLMMNOPQQRSTTUUVVWXYZZaabcdefghhijjkllmnopqrsttuvwxyz";
609 const char * value1 =
610 "EMLEMMMMMMMNMMLMELEDNLEDMLMLDMLMLMLMEDGFHPOPBAHJIQJNGRKCGF";
611 while (*value0) {
612 Xapian::Document doc;
613 doc.add_value(0, string(1, *value0++));
614 if (*value1) {
615 doc.add_value(1, string(1, *value1++));
616 doc.add_term("K1");
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();
634 TEST_EQUAL(lb, ub);
635 TEST_EQUAL(lb, est);
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);
654 static void
655 make_msize2_db(Xapian::WritableDatabase &db, const string &)
657 const char * value0 = "AAABCDEEFGHIIJJKLLMNNOOPPQQRSTTUVWXYZ";
658 const char * value1 = "MLEMNMLMLMEDEDEMLEMLMLMLPOAHGF";
659 while (*value0) {
660 Xapian::Document doc;
661 doc.add_value(0, string(1, *value0++));
662 if (*value1) {
663 doc.add_value(1, string(1, *value1++));
664 doc.add_term("K1");
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();
682 TEST_EQUAL(lb, ub);
683 TEST_EQUAL(lb, est);
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);
702 static void
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) {
708 if (n % i == 0)
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,
722 Xapian::Query("N2"),
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()));
730 static void
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
763 * 1.0.21.
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,
775 q.begin(),
776 q.end())));
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()));
785 static void
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));
798 doc.add_term("T1");
799 db.replace_document(i, doc);
801 for (unsigned i : t2) {
802 Xapian::Document doc(db.get_document(i));
803 doc.add_term("T2");
804 if (i < 17) {
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));
812 doc.add_term("T3");
813 if (i < 17) {
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;
871 doc.add_term("foo");
872 db.add_document(doc);
873 Xapian::docid did = db.add_document(doc);
874 doc.add_term("abc");
875 doc.add_term(string(1000, 'm'));
876 doc.add_term("xyz");
877 TEST_EXCEPTION(Xapian::InvalidArgumentError, db.replace_document(did, doc));
878 db.commit();
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"));
885 db.commit();
886 Xapian::doccount db_size = db.get_doccount();
887 Xapian::Document doc;
888 doc.set_data("wibble");
889 doc.add_term("foo");
890 doc.add_value(0, "seven");
891 db.add_document(doc);
892 Xapian::docid did = db.add_document(doc);
893 doc.add_term("abc");
894 doc.add_term(string(1000, 'm'));
895 doc.add_term("xyz");
896 doc.add_value(0, "six");
897 TEST_EXCEPTION(Xapian::InvalidArgumentError, db.replace_document(did, doc));
898 db.commit();
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"));
923 Xapian::MSet mset;
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) {
989 tout << op << '\n';
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"));
1055 enq.set_query(q);
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);
1094 t.skip_to("zzzzz");
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");
1100 p.skip_to(99999);
1101 TEST(p == db.postlist_end("one"));
1102 // This segfaulted prior to 1.3.2.
1103 p.skip_to(999999);
1105 Xapian::PositionIterator i = db.positionlist_begin(6, "one");
1106 i.skip_to(99999);
1107 TEST(i == db.positionlist_end(6, "one"));
1108 // This segfaulted prior to 1.3.2.
1109 i.skip_to(999999);
1111 Xapian::ValueIterator v = db.valuestream_begin(1);
1112 v.skip_to(99999);
1113 TEST(v == db.valuestream_end(1));
1114 // These segfaulted prior to 1.3.2.
1115 v.skip_to(999999);
1116 v.check(9999999);
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
1122 // called on 0.
1123 DEFINE_TESTCASE(blocksize1, chert || glass) {
1124 string db_dir = "." + get_dbtype();
1125 mkdir(db_dir.c_str(), 0755);
1126 db_dir += "/db__blocksize1";
1127 int flags;
1128 if (get_dbtype() == "chert") {
1129 flags = Xapian::DB_CREATE|Xapian::DB_BACKEND_CHERT;
1130 } else {
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];
1138 rm_rf(db_dir);
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);
1144 db.commit();
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;
1156 } else {
1157 flags |= Xapian::DB_BACKEND_GLASS;
1159 rm_rf(db_dir);
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);
1165 db.commit();
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);
1182 wdb.commit();
1184 for (int k = 0; k < 1000; ++k) {
1185 wdb.add_document(doc);
1186 wdb.commit();
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);
1204 try {
1205 Xapian::Document doc;
1206 doc.add_term("hello");
1207 doc.set_data("some text");
1208 db.add_document(doc);
1209 db.commit();
1210 } catch (...) {
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);
1214 throw;
1216 TEST(chmod(path.c_str(), 0700) == 0);
1217 #endif
1220 static void
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);
1238 e.set_query(q);
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);
1243 e.set_query(q2);
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
1253 int fds[2];
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();
1262 if (child == -1)
1263 FAIL_TEST("fork() failed");
1264 if (child == 0) {
1265 // Wait for signal that parent has opened the database.
1266 char ch;
1267 while (read(fds[0], &ch, 1) < 0) { }
1269 try {
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())) { }
1278 } catch (...) {
1279 if (write(fds[0], "o", 1)) { }
1281 _exit(0);
1284 close(fds[0]);
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");
1290 char result[256];
1291 int r = read(fds[1], result, sizeof(result));
1292 if (r == -1) {
1293 if (errno == EAGAIN) {
1294 // Good.
1295 result[0] = 'y';
1296 } else {
1297 // Error.
1298 tout << "errno=" << errno << ": " << errno_to_string(errno) << '\n';
1299 result[0] = 'e';
1301 r = 1;
1302 } else if (r >= 1) {
1303 if (result[0] == 'y') {
1304 // Child process managed to also get write lock!
1305 result[0] = '!';
1307 } else {
1308 // EOF.
1309 result[0] = 'z';
1310 r = 1;
1313 try {
1314 db.close();
1315 } catch (...) {
1316 kill(child, SIGKILL);
1317 int status;
1318 while (waitpid(child, &status, 0) < 0) {
1319 if (errno != EINTR) break;
1321 throw;
1324 if (result[0] == 'y') {
1325 retry:
1326 struct timeval tv;
1327 tv.tv_sec = 3;
1328 tv.tv_usec = 0;
1329 fd_set fdset;
1330 FD_ZERO(&fdset);
1331 FD_SET(fds[1], &fdset);
1332 int sr = select(fds[1] + 1, &fdset, NULL, NULL, &tv);
1333 if (sr == 0) {
1334 // Timed out.
1335 result[0] = 'T';
1336 r = 1;
1337 } else if (sr == -1) {
1338 if (errno == EINTR || errno == EAGAIN)
1339 goto retry;
1340 tout << "select() failed with errno=" << errno << ": "
1341 << errno_to_string(errno) << '\n';
1342 result[0] = 'S';
1343 r = 1;
1344 } else {
1345 r = read(fds[1], result, sizeof(result));
1346 if (r == -1) {
1347 // Error.
1348 tout << "read() failed with errno=" << errno << ": "
1349 << errno_to_string(errno) << '\n';
1350 result[0] = 'R';
1351 r = 1;
1352 } else if (r == 0) {
1353 // EOF.
1354 result[0] = 'Z';
1355 r = 1;
1360 close(fds[1]);
1362 kill(child, SIGKILL);
1363 int status;
1364 while (waitpid(child, &status, 0) < 0) {
1365 if (errno != EINTR) break;
1368 tout << string(result, r) << '\n';
1369 TEST_EQUAL(result[0], 'y');
1370 #endif
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__
1376 int oldfds[3];
1377 for (int i = 0; i < 3; ++i) {
1378 oldfds[i] = dup(i);
1380 try {
1381 for (int j = 0; j < 3; ++j) {
1382 close(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);
1397 if (flags == -1) {
1398 TEST_EQUAL(errno, EBADF);
1399 } else if ((flags & O_ACCMODE) != O_RDWR) {
1400 // OK.
1401 } else {
1402 struct stat sb;
1403 TEST_NOT_EQUAL(fstat(fd, &sb), -1);
1404 #ifdef S_ISSOCK
1405 TEST(S_ISSOCK(sb.st_mode));
1406 #else
1407 // If we can't check it is a socket, at least check it is not a
1408 // regular file.
1409 TEST(!S_ISREG(sb.st_mode));
1410 #endif
1413 } catch (...) {
1414 for (int j = 0; j < 3; ++j) {
1415 dup2(oldfds[j], j);
1416 close(oldfds[j]);
1418 throw;
1421 for (int j = 0; j < 3; ++j) {
1422 dup2(oldfds[j], j);
1423 close(oldfds[j]);
1425 #endif
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);
1434 Xapian::MSet mset;
1435 // The original problem triggers for chert and glass on repeat==7.
1436 for (int repeat = 0; repeat < 10; ++repeat) {
1437 tout.str(string());
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);
1458 wdb.commit();
1460 db.reopen();
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.
1479 string old_key;
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);
1483 swap(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);
1503 out.seekp(offset);
1504 out << ifstream(db_path, fstream::binary).rdbuf();
1505 out.close();
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);
1532 enq.set_query(q);
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);
1544 enq.set_query(q2);
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);
1557 db.commit();
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);
1563 db.commit();
1564 TEST_EQUAL(db.get_revision(), 1);
1565 db.commit();
1566 TEST_EQUAL(db.get_revision(), 1);
1567 db.add_document(doc);
1568 db.commit();
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;
1593 try {
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);
1605 static void
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
1643 * subqueries.
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,
1697 const string&)
1699 wdb.set_metadata("foo", "bar");
1700 wdb.commit();
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);
1722 db.commit();
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);
1729 pos = *p;
1730 TEST_EQUAL(pos, expect);
1731 expect += 5;
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
1749 // instead.
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) {
1759 string term = *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;
1775 string term;
1776 term.reserve(MAX_LEN);
1777 while (term.size() < MAX_LEN) {
1778 term += 'x';
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';
1792 out.close();
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";
1805 out.close();
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)
1815 try {
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) {
1826 string db_path =
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));