Support "generated" testcases for inmemory backend
[xapian.git] / xapian-core / tests / api_backend.cc
blob165e990e968ff8bfcb114088d9fb0de518eb7f91
1 /** @file api_backend.cc
2 * @brief Backend-related tests.
3 */
4 /* Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015,2016,2017 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 #include <xapian.h>
29 #include "backendmanager.h"
30 #include "filetests.h"
31 #include "str.h"
32 #include "testrunner.h"
33 #include "testsuite.h"
34 #include "testutils.h"
35 #include "unixcmds.h"
37 #include "apitest.h"
39 #include "safefcntl.h"
40 #include "safesysstat.h"
41 #include "safeunistd.h"
42 #ifdef HAVE_SOCKETPAIR
43 # include "safesyssocket.h"
44 # include <signal.h>
45 # include "safesyswait.h"
46 #endif
48 #include <fstream>
49 #include <iterator>
51 using namespace std;
53 /// Regression test - lockfile should honour umask, was only user-readable.
54 DEFINE_TESTCASE(lockfileumask1, glass) {
55 #if !defined __WIN32__ && !defined __CYGWIN__ && !defined __OS2__
56 mode_t old_umask = umask(022);
57 try {
58 Xapian::WritableDatabase db = get_named_writable_database("lockfileumask1");
60 string path = get_named_writable_database_path("lockfileumask1");
61 path += "/flintlock";
63 struct stat statbuf;
64 TEST(stat(path.c_str(), &statbuf) == 0);
65 TEST_EQUAL(statbuf.st_mode & 0777, 0644);
66 } catch (...) {
67 umask(old_umask);
68 throw;
71 umask(old_umask);
72 #endif
74 return true;
77 /// Check that the backend handles total document length > 0xffffffff.
78 DEFINE_TESTCASE(totaldoclen1, writable) {
79 Xapian::WritableDatabase db = get_writable_database();
80 Xapian::Document doc;
81 doc.add_posting("foo", 1, 2000000000);
82 db.add_document(doc);
83 Xapian::Document doc2;
84 doc2.add_posting("bar", 1, 2000000000);
85 db.add_document(doc2);
86 TEST_EQUAL(db.get_avlength(), 2000000000);
87 TEST_EQUAL(db.get_total_length(), 4000000000ull);
88 db.commit();
89 TEST_EQUAL(db.get_avlength(), 2000000000);
90 TEST_EQUAL(db.get_total_length(), 4000000000ull);
91 for (int i = 0; i != 20; ++i) {
92 Xapian::Document doc3;
93 doc3.add_posting("count" + str(i), 1, 2000000000);
94 db.add_document(doc3);
96 TEST_EQUAL(db.get_avlength(), 2000000000);
97 TEST_EQUAL(db.get_total_length(), 44000000000ull);
98 db.commit();
99 TEST_EQUAL(db.get_avlength(), 2000000000);
100 TEST_EQUAL(db.get_total_length(), 44000000000ull);
101 if (get_dbtype() != "inmemory") {
102 // InMemory doesn't support get_writable_database_as_database().
103 Xapian::Database dbr = get_writable_database_as_database();
104 TEST_EQUAL(dbr.get_avlength(), 2000000000);
105 TEST_EQUAL(dbr.get_total_length(), 44000000000ull);
107 return true;
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 true;
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::WritableDatabase db2 = get_writable_database();
125 Xapian::Document doc;
126 doc.set_data("prose");
127 doc.add_term("word");
129 Xapian::docid max_id = 0xffffffff;
131 db1.replace_document(max_id, doc);
132 db2.replace_document(max_id, doc);
134 Xapian::Database db;
135 db.add_database(db1);
136 db.add_database(db2);
138 Xapian::Enquire enquire(db);
139 enquire.set_query(Xapian::Query::MatchAll);
140 Xapian::MSet mymset = enquire.get_mset(0, 10);
142 TEST_EQUAL(2, mymset.size());
144 for (Xapian::MSetIterator i = mymset.begin(); i != mymset.end(); ++i) {
145 TEST_EQUAL("prose", i.get_document().get_data());
148 return true;
151 DEFINE_TESTCASE(dbstats1, backend) {
152 Xapian::Database db = get_database("etext");
154 // Use precalculated values to avoid expending CPU cycles to calculate
155 // these every time without improving test coverage.
156 const Xapian::termcount min_len = 2;
157 const Xapian::termcount max_len = 532;
158 const Xapian::termcount max_wdf = 22;
160 if (get_dbtype() != "inmemory") {
161 // Should be exact as no deletions have happened.
162 TEST_EQUAL(db.get_doclength_upper_bound(), max_len);
163 TEST_EQUAL(db.get_doclength_lower_bound(), min_len);
164 } else {
165 // For inmemory, we usually give rather loose bounds.
166 TEST_REL(db.get_doclength_upper_bound(),>=,max_len);
167 TEST_REL(db.get_doclength_lower_bound(),<=,min_len);
170 if (get_dbtype() != "inmemory" && !startswith(get_dbtype(), "remote")) {
171 TEST_EQUAL(db.get_wdf_upper_bound("the"), max_wdf);
172 } else {
173 // For inmemory and remote backends, we usually give rather loose
174 // bounds (remote matches use tighter bounds, but querying the
175 // wdf bound gives a looser one).
176 TEST_REL(db.get_wdf_upper_bound("the"),>=,max_wdf);
179 // This failed with an assertion during development between 1.3.1 and
180 // 1.3.2.
181 TEST_EQUAL(db.get_wdf_upper_bound(""), 0);
183 return true;
186 // Check stats with a single document. In a multi-database situation, this
187 // gave 0 for get-_doclength_lower_bound() in 1.3.2.
188 DEFINE_TESTCASE(dbstats2, backend) {
189 Xapian::Database db = get_database("apitest_onedoc");
191 // Use precalculated values to avoid expending CPU cycles to calculate
192 // these every time without improving test coverage.
193 const Xapian::termcount min_len = 15;
194 const Xapian::termcount max_len = 15;
195 const Xapian::termcount max_wdf = 7;
197 if (get_dbtype() != "inmemory") {
198 // Should be exact as no deletions have happened.
199 TEST_EQUAL(db.get_doclength_upper_bound(), max_len);
200 TEST_EQUAL(db.get_doclength_lower_bound(), min_len);
201 } else {
202 // For inmemory, we usually give rather loose bounds.
203 TEST_REL(db.get_doclength_upper_bound(),>=,max_len);
204 TEST_REL(db.get_doclength_lower_bound(),<=,min_len);
207 if (get_dbtype() != "inmemory" && !startswith(get_dbtype(), "remote")) {
208 TEST_EQUAL(db.get_wdf_upper_bound("word"), max_wdf);
209 } else {
210 // For inmemory and remote backends, we usually give rather loose
211 // bounds (remote matches use tighter bounds, but querying the
212 // wdf bound gives a looser one).
213 TEST_REL(db.get_wdf_upper_bound("word"),>=,max_wdf);
216 TEST_EQUAL(db.get_wdf_upper_bound(""), 0);
218 return true;
221 /// Check handling of alldocs on an empty database.
222 DEFINE_TESTCASE(alldocspl3, backend) {
223 Xapian::Database db = get_database(string());
225 TEST_EQUAL(db.get_termfreq(string()), 0);
226 TEST_EQUAL(db.get_collection_freq(string()), 0);
227 TEST(db.postlist_begin(string()) == db.postlist_end(string()));
229 return true;
232 /// Regression test for bug#392 in ModifiedPostList iteration, fixed in 1.0.15.
233 DEFINE_TESTCASE(modifiedpostlist1, writable) {
234 Xapian::WritableDatabase db = get_writable_database();
235 Xapian::Document a, b;
236 Xapian::Enquire enq(db);
238 a.add_term("T");
239 enq.set_query(Xapian::Query("T"));
241 db.replace_document(2, a);
242 db.commit();
243 db.replace_document(1, a);
244 db.replace_document(1, b);
246 mset_expect_order(enq.get_mset(0, 2), 2);
248 return true;
251 /// Regression test for chert bug fixed in 1.1.3 (ticket#397).
252 DEFINE_TESTCASE(doclenaftercommit1, writable) {
253 Xapian::WritableDatabase db = get_writable_database();
254 TEST_EXCEPTION(Xapian::DocNotFoundError, db.get_doclength(1));
255 TEST_EXCEPTION(Xapian::DocNotFoundError, db.get_unique_terms(1));
256 db.replace_document(1, Xapian::Document());
257 db.commit();
258 TEST_EQUAL(db.get_doclength(1), 0);
259 TEST_EQUAL(db.get_unique_terms(1), 0);
260 return true;
263 DEFINE_TESTCASE(valuesaftercommit1, writable) {
264 Xapian::WritableDatabase db = get_writable_database();
265 Xapian::Document doc;
266 doc.add_value(0, "value");
267 db.replace_document(2, doc);
268 db.commit();
269 db.replace_document(1, doc);
270 db.replace_document(3, doc);
271 TEST_EQUAL(db.get_document(3).get_value(0), "value");
272 db.commit();
273 TEST_EQUAL(db.get_document(3).get_value(0), "value");
274 return true;
277 DEFINE_TESTCASE(lockfilefd0or1, glass) {
278 #if !defined __WIN32__ && !defined __CYGWIN__ && !defined __OS2__
279 int old_stdin = dup(0);
280 int old_stdout = dup(1);
281 try {
282 // With fd 0 available.
283 close(0);
285 Xapian::WritableDatabase db = get_writable_database();
286 TEST_EXCEPTION(Xapian::DatabaseLockError,
287 (void)get_writable_database_again());
289 // With fd 0 and fd 1 available.
290 close(1);
292 Xapian::WritableDatabase db = get_writable_database();
293 TEST_EXCEPTION(Xapian::DatabaseLockError,
294 (void)get_writable_database_again());
296 // With fd 1 available.
297 dup2(old_stdin, 0);
299 Xapian::WritableDatabase db = get_writable_database();
300 TEST_EXCEPTION(Xapian::DatabaseLockError,
301 (void)get_writable_database_again());
303 } catch (...) {
304 dup2(old_stdin, 0);
305 dup2(old_stdout, 1);
306 close(old_stdin);
307 close(old_stdout);
308 throw;
311 dup2(old_stdout, 1);
312 close(old_stdin);
313 close(old_stdout);
314 #endif
316 return true;
319 /// Regression test for bug fixed in 1.2.13 and 1.3.1.
320 DEFINE_TESTCASE(lockfilealreadyopen1, glass) {
321 // Ensure database has been created.
322 (void)get_named_writable_database("lockfilealreadyopen1");
323 string path = get_named_writable_database_path("lockfilealreadyopen1");
324 int fd = ::open((path + "/flintlock").c_str(), O_RDONLY);
325 TEST(fd != -1);
326 try {
327 Xapian::WritableDatabase db(path, Xapian::DB_CREATE_OR_OPEN);
328 TEST_EXCEPTION(Xapian::DatabaseLockError,
329 Xapian::WritableDatabase db2(path, Xapian::DB_CREATE_OR_OPEN)
331 } catch (...) {
332 close(fd);
333 throw;
335 close(fd);
337 return true;
340 /// Feature tests for Database::locked().
341 DEFINE_TESTCASE(testlock1, glass) {
342 Xapian::Database rdb;
343 TEST(!rdb.locked());
345 Xapian::WritableDatabase db = get_named_writable_database("testlock1");
346 TEST(db.locked());
347 Xapian::Database db_as_database = db;
348 TEST(db_as_database.locked());
349 TEST(!rdb.locked());
350 rdb = get_writable_database_as_database();
351 TEST(db.locked());
352 TEST(db_as_database.locked());
353 try {
354 TEST(rdb.locked());
355 } catch (const Xapian::FeatureUnavailableError&) {
356 SKIP_TEST("Database::locked() not supported on this platform");
358 db_as_database = rdb;
359 TEST(db.locked());
360 TEST(db_as_database.locked());
361 TEST(rdb.locked());
362 db_as_database.close();
363 TEST(db.locked());
364 TEST(rdb.locked());
365 // After close(), locked() should either work as if close() hadn't been
366 // called or throw Xapian::DatabaseError.
367 try {
368 TEST(db_as_database.locked());
369 } catch (const Xapian::DatabaseError&) {
371 db.close();
372 TEST(!rdb.locked());
373 try {
374 TEST(!db_as_database.locked());
375 } catch (const Xapian::DatabaseError&) {
378 TEST(!rdb.locked());
379 return true;
382 /// Test that locked() returns false for backends which don't support update.
383 /// Regression test for bug fixed in 1.4.6.
384 DEFINE_TESTCASE(testlock2, backend && !writable && !multi) {
385 Xapian::Database db = get_database("apitest_simpledata");
386 TEST(!db.locked());
387 return true;
390 class CheckMatchDecider : public Xapian::MatchDecider {
391 mutable bool called;
393 public:
394 CheckMatchDecider() : called(false) { }
396 bool operator()(const Xapian::Document &) const {
397 called = true;
398 return true;
401 bool was_called() const { return called; }
404 /// Test Xapian::MatchDecider with remote backend fails.
405 DEFINE_TESTCASE(matchdecider4, remote) {
406 Xapian::Database db(get_database("apitest_simpledata"));
407 Xapian::Enquire enquire(db);
408 enquire.set_query(Xapian::Query("paragraph"));
410 CheckMatchDecider mdecider;
411 Xapian::MSet mset;
413 TEST_EXCEPTION(Xapian::UnimplementedError,
414 mset = enquire.get_mset(0, 10, NULL, &mdecider));
415 TEST(!mdecider.was_called());
417 return true;
420 /** Check that replacing an unmodified document doesn't increase the automatic
421 * commit counter. Regression test for bug fixed in 1.1.4/1.0.18.
423 DEFINE_TESTCASE(replacedoc7, writable && !inmemory && !remote) {
424 // The inmemory backend doesn't batch changes, so there's nothing to
425 // check there.
427 // The remote backend doesn't implement the lazy replacement of documents
428 // optimisation currently.
429 Xapian::WritableDatabase db(get_writable_database());
430 Xapian::Document doc;
431 doc.set_data("fish");
432 doc.add_term("Hlocalhost");
433 doc.add_posting("hello", 1);
434 doc.add_posting("world", 2);
435 doc.add_value(1, "myvalue");
436 db.add_document(doc);
437 db.commit();
439 // We add a second document, and then replace the first document with
440 // itself 10000 times. If the document count for the database reopened
441 // read-only is 2, then we triggered an automatic commit.
443 doc.add_term("XREV2");
444 db.add_document(doc);
446 for (int i = 0; i < 10000; ++i) {
447 doc = db.get_document(1);
448 db.replace_document(1, doc);
451 Xapian::Database rodb(get_writable_database_as_database());
452 TEST_EQUAL(rodb.get_doccount(), 1);
454 db.commit();
455 TEST(rodb.reopen());
457 TEST_EQUAL(rodb.get_doccount(), 2);
458 return true;
461 /** Check that replacing a document deleted since the last commit works.
462 * Prior to 1.1.4/1.0.18, this failed to update the collection frequency and
463 * wdf, and caused an assertion failure when assertions were enabled.
465 DEFINE_TESTCASE(replacedoc8, writable) {
466 Xapian::WritableDatabase db(get_writable_database());
468 Xapian::Document doc;
469 doc.set_data("fish");
470 doc.add_term("takeaway");
471 db.add_document(doc);
473 db.delete_document(1);
475 Xapian::Document doc;
476 doc.set_data("chips");
477 doc.add_term("takeaway", 2);
478 db.replace_document(1, doc);
480 db.commit();
481 TEST_EQUAL(db.get_collection_freq("takeaway"), 2);
482 Xapian::PostingIterator p = db.postlist_begin("takeaway");
483 TEST(p != db.postlist_end("takeaway"));
484 TEST_EQUAL(p.get_wdf(), 2);
485 return true;
488 /// Test coverage for DatabaseModifiedError.
489 DEFINE_TESTCASE(databasemodified1, writable && !inmemory && !remote && !multi) {
490 // The inmemory backend doesn't support revisions.
492 // The remote backend doesn't work as expected here, I think due to
493 // test harness issues.
495 // With multi, DatabaseModifiedError doesn't trigger as easily.
496 Xapian::WritableDatabase db(get_writable_database());
497 Xapian::Document doc;
498 doc.set_data("cargo");
499 doc.add_term("abc");
500 doc.add_term("def");
501 doc.add_term("ghi");
502 const int N = 500;
503 for (int i = 0; i < N; ++i) {
504 db.add_document(doc);
506 db.commit();
508 Xapian::Database rodb(get_writable_database_as_database());
509 db.add_document(doc);
510 db.commit();
512 db.add_document(doc);
513 db.commit();
515 db.add_document(doc);
516 try {
517 TEST_EQUAL(*rodb.termlist_begin(N - 1), "abc");
518 return false;
519 } catch (const Xapian::DatabaseModifiedError &) {
522 try {
523 Xapian::Enquire enq(rodb);
524 enq.set_query(Xapian::Query("abc"));
525 Xapian::MSet mset = enq.get_mset(0, 10);
526 return false;
527 } catch (const Xapian::DatabaseModifiedError &) {
530 return true;
533 /// Regression test for bug#462 fixed in 1.0.19 and 1.1.5.
534 DEFINE_TESTCASE(qpmemoryleak1, writable && !inmemory) {
535 // Inmemory never throws DatabaseModifiedError.
536 Xapian::WritableDatabase wdb(get_writable_database());
537 Xapian::Document doc;
539 doc.add_term("foo");
540 for (int i = 100; i < 120; ++i) {
541 doc.add_term(str(i));
544 for (int j = 0; j < 50; ++j) {
545 wdb.add_document(doc);
547 wdb.commit();
549 Xapian::Database database(get_writable_database_as_database());
550 Xapian::QueryParser queryparser;
551 queryparser.set_database(database);
552 TEST_EXCEPTION(Xapian::DatabaseModifiedError,
553 for (int k = 0; k < 1000; ++k) {
554 wdb.add_document(doc);
555 wdb.commit();
556 (void)queryparser.parse_query("1", queryparser.FLAG_PARTIAL);
558 SKIP_TEST("didn't manage to trigger DatabaseModifiedError");
561 return true;
564 static void
565 make_msize1_db(Xapian::WritableDatabase &db, const string &)
567 const char * value0 =
568 "ABBCDEFGHIJKLMMNOPQQRSTTUUVVWXYZZaabcdefghhijjkllmnopqrsttuvwxyz";
569 const char * value1 =
570 "EMLEMMMMMMMNMMLMELEDNLEDMLMLDMLMLMLMEDGFHPOPBAHJIQJNGRKCGF";
571 while (*value0) {
572 Xapian::Document doc;
573 doc.add_value(0, string(1, *value0++));
574 if (*value1) {
575 doc.add_value(1, string(1, *value1++));
576 doc.add_term("K1");
578 db.add_document(doc);
582 /// Regression test for ticket#464, fixed in 1.1.6 and 1.0.20.
583 DEFINE_TESTCASE(msize1, generated) {
584 Xapian::Database db = get_database("msize1", make_msize1_db);
585 Xapian::Enquire enq(db);
586 enq.set_sort_by_value(1, false);
587 enq.set_collapse_key(0);
588 enq.set_query(Xapian::Query("K1"));
590 Xapian::MSet mset = enq.get_mset(0, 60);
591 Xapian::doccount lb = mset.get_matches_lower_bound();
592 Xapian::doccount ub = mset.get_matches_upper_bound();
593 Xapian::doccount est = mset.get_matches_estimated();
594 TEST_EQUAL(lb, ub);
595 TEST_EQUAL(lb, est);
597 Xapian::MSet mset2 = enq.get_mset(50, 10, 1000);
598 Xapian::doccount lb2 = mset2.get_matches_lower_bound();
599 Xapian::doccount ub2 = mset2.get_matches_upper_bound();
600 Xapian::doccount est2 = mset2.get_matches_estimated();
601 TEST_EQUAL(lb2, ub2);
602 TEST_EQUAL(lb2, est2);
603 TEST_EQUAL(est, est2);
605 Xapian::MSet mset3 = enq.get_mset(0, 10, 1000);
606 Xapian::doccount lb3 = mset3.get_matches_lower_bound();
607 Xapian::doccount ub3 = mset3.get_matches_upper_bound();
608 Xapian::doccount est3 = mset3.get_matches_estimated();
609 TEST_EQUAL(lb3, ub3);
610 TEST_EQUAL(lb3, est3);
611 TEST_EQUAL(est, est3);
613 return true;
616 static void
617 make_msize2_db(Xapian::WritableDatabase &db, const string &)
619 const char * value0 = "AAABCDEEFGHIIJJKLLMNNOOPPQQRSTTUVWXYZ";
620 const char * value1 = "MLEMNMLMLMEDEDEMLEMLMLMLPOAHGF";
621 while (*value0) {
622 Xapian::Document doc;
623 doc.add_value(0, string(1, *value0++));
624 if (*value1) {
625 doc.add_value(1, string(1, *value1++));
626 doc.add_term("K1");
628 db.add_document(doc);
632 /// Regression test for bug related to ticket#464, fixed in 1.1.6 and 1.0.20.
633 DEFINE_TESTCASE(msize2, generated) {
634 Xapian::Database db = get_database("msize2", make_msize2_db);
635 Xapian::Enquire enq(db);
636 enq.set_sort_by_value(1, false);
637 enq.set_collapse_key(0);
638 enq.set_query(Xapian::Query("K1"));
640 Xapian::MSet mset = enq.get_mset(0, 60);
641 Xapian::doccount lb = mset.get_matches_lower_bound();
642 Xapian::doccount ub = mset.get_matches_upper_bound();
643 Xapian::doccount est = mset.get_matches_estimated();
644 TEST_EQUAL(lb, ub);
645 TEST_EQUAL(lb, est);
647 Xapian::MSet mset2 = enq.get_mset(50, 10, 1000);
648 Xapian::doccount lb2 = mset2.get_matches_lower_bound();
649 Xapian::doccount ub2 = mset2.get_matches_upper_bound();
650 Xapian::doccount est2 = mset2.get_matches_estimated();
651 TEST_EQUAL(lb2, ub2);
652 TEST_EQUAL(lb2, est2);
653 TEST_EQUAL(est, est2);
655 Xapian::MSet mset3 = enq.get_mset(0, 10, 1000);
656 Xapian::doccount lb3 = mset3.get_matches_lower_bound();
657 Xapian::doccount ub3 = mset3.get_matches_upper_bound();
658 Xapian::doccount est3 = mset3.get_matches_estimated();
659 TEST_EQUAL(lb3, ub3);
660 TEST_EQUAL(lb3, est3);
661 TEST_EQUAL(est, est3);
663 return true;
666 static void
667 make_xordecay1_db(Xapian::WritableDatabase &db, const string &)
669 for (int n = 1; n != 50; ++n) {
670 Xapian::Document doc;
671 for (int i = 1; i != 50; ++i) {
672 if (n % i == 0)
673 doc.add_term("N" + str(i));
675 db.add_document(doc);
679 /// Regression test for bug in decay of XOR, fixed in 1.2.1 and 1.0.21.
680 DEFINE_TESTCASE(xordecay1, generated) {
681 Xapian::Database db = get_database("xordecay1", make_xordecay1_db);
682 Xapian::Enquire enq(db);
683 enq.set_query(Xapian::Query(Xapian::Query::OP_XOR,
684 Xapian::Query("N10"),
685 Xapian::Query(Xapian::Query::OP_OR,
686 Xapian::Query("N2"),
687 Xapian::Query("N3"))));
688 Xapian::MSet mset1 = enq.get_mset(0, 1);
689 Xapian::MSet msetall = enq.get_mset(0, db.get_doccount());
691 TEST(mset_range_is_same(mset1, 0, msetall, 0, mset1.size()));
692 return true;
695 static void
696 make_ordecay_db(Xapian::WritableDatabase &db, const string &)
698 const char * p = "VJ=QC]LUNTaARLI;715RR^];A4O=P4ZG<2CS4EM^^VS[A6QENR";
699 for (int d = 0; p[d]; ++d) {
700 int l = int(p[d] - '0');
701 Xapian::Document doc;
702 for (int n = 1; n < l; ++n) {
703 doc.add_term("N" + str(n));
704 if (n % (d + 1) == 0) {
705 doc.add_term("M" + str(n));
708 db.add_document(doc);
712 /// Regression test for bug in decay of OR to AND, fixed in 1.2.1 and 1.0.21.
713 DEFINE_TESTCASE(ordecay1, generated) {
714 Xapian::Database db = get_database("ordecay", make_ordecay_db);
715 Xapian::Enquire enq(db);
716 enq.set_query(Xapian::Query(Xapian::Query::OP_OR,
717 Xapian::Query("N20"),
718 Xapian::Query("N21")));
720 Xapian::MSet msetall = enq.get_mset(0, db.get_doccount());
721 for (unsigned int i = 1; i < msetall.size(); ++i) {
722 Xapian::MSet submset = enq.get_mset(0, i);
723 TEST(mset_range_is_same(submset, 0, msetall, 0, submset.size()));
725 return true;
728 /** Regression test for bug in decay of OR to AND_MAYBE, fixed in 1.2.1 and
729 * 1.0.21.
731 DEFINE_TESTCASE(ordecay2, generated) {
732 Xapian::Database db = get_database("ordecay", make_ordecay_db);
733 Xapian::Enquire enq(db);
734 std::vector<Xapian::Query> q;
735 q.push_back(Xapian::Query("M20"));
736 q.push_back(Xapian::Query("N21"));
737 q.push_back(Xapian::Query("N22"));
738 enq.set_query(Xapian::Query(Xapian::Query::OP_OR,
739 Xapian::Query("N25"),
740 Xapian::Query(Xapian::Query::OP_AND,
741 q.begin(),
742 q.end())));
744 Xapian::MSet msetall = enq.get_mset(0, db.get_doccount());
745 for (unsigned int i = 1; i < msetall.size(); ++i) {
746 Xapian::MSet submset = enq.get_mset(0, i);
747 TEST(mset_range_is_same(submset, 0, msetall, 0, submset.size()));
749 return true;
752 static void
753 make_orcheck_db(Xapian::WritableDatabase &db, const string &)
755 static const unsigned t1[] = {2, 4, 6, 8, 10};
756 static const unsigned t2[] = {6, 7, 8, 11, 12, 13, 14, 15, 16, 17};
757 static const unsigned t3[] = {3, 7, 8, 11, 12, 13, 14, 15, 16, 17};
759 for (unsigned i = 1; i <= 17; ++i) {
760 Xapian::Document doc;
761 db.replace_document(i, doc);
763 for (unsigned i : t1) {
764 Xapian::Document doc(db.get_document(i));
765 doc.add_term("T1");
766 db.replace_document(i, doc);
768 for (unsigned i : t2) {
769 Xapian::Document doc(db.get_document(i));
770 doc.add_term("T2");
771 if (i < 17) {
772 doc.add_term("T2_lowfreq");
774 doc.add_value(2, "1");
775 db.replace_document(i, doc);
777 for (unsigned i : t3) {
778 Xapian::Document doc(db.get_document(i));
779 doc.add_term("T3");
780 if (i < 17) {
781 doc.add_term("T3_lowfreq");
783 doc.add_value(3, "1");
784 db.replace_document(i, doc);
788 /** Regression test for bugs in the check() method of OrPostList. (ticket #485)
789 * Bugs introduced and fixed between 1.2.0 and 1.2.1 (never in a release).
791 DEFINE_TESTCASE(orcheck1, generated) {
792 // Currently fails for inmemory.
793 SKIP_TEST_FOR_BACKEND("inmemory");
794 Xapian::Database db = get_database("orcheck1", make_orcheck_db);
795 Xapian::Enquire enq(db);
796 Xapian::Query q1("T1");
797 Xapian::Query q2("T2");
798 Xapian::Query q2l("T2_lowfreq");
799 Xapian::Query q3("T3");
800 Xapian::Query q3l("T3_lowfreq");
801 Xapian::Query v2(Xapian::Query::OP_VALUE_RANGE, 2, "0", "2");
802 Xapian::Query v3(Xapian::Query::OP_VALUE_RANGE, 3, "0", "2");
804 tout << "Checking q2 OR q3\n";
805 enq.set_query(Xapian::Query(Xapian::Query::OP_AND, q1,
806 Xapian::Query(Xapian::Query::OP_OR, q2, q3)));
807 mset_expect_order(enq.get_mset(0, db.get_doccount()), 8, 6);
809 tout << "Checking q2l OR q3\n";
810 enq.set_query(Xapian::Query(Xapian::Query::OP_AND, q1,
811 Xapian::Query(Xapian::Query::OP_OR, q2l, q3)));
812 mset_expect_order(enq.get_mset(0, db.get_doccount()), 8, 6);
814 tout << "Checking q2 OR q3l\n";
815 enq.set_query(Xapian::Query(Xapian::Query::OP_AND, q1,
816 Xapian::Query(Xapian::Query::OP_OR, q2, q3l)));
817 mset_expect_order(enq.get_mset(0, db.get_doccount()), 8, 6);
819 tout << "Checking v2 OR q3\n";
820 enq.set_query(Xapian::Query(Xapian::Query::OP_AND, q1,
821 Xapian::Query(Xapian::Query::OP_OR, v2, q3)));
822 mset_expect_order(enq.get_mset(0, db.get_doccount()), 8, 6);
824 tout << "Checking q2 OR v3\n";
825 enq.set_query(Xapian::Query(Xapian::Query::OP_AND, q1,
826 Xapian::Query(Xapian::Query::OP_OR, q2, v3)));
827 // Order of results in this one is different, because v3 gives no weight,
828 // both documents are in q2, and document 8 has a higher length.
829 mset_expect_order(enq.get_mset(0, db.get_doccount()), 6, 8);
831 return true;
834 /** Regression test for bug fixed in 1.2.1 and 1.0.21.
836 * We failed to mark the Btree as unmodified after cancel().
838 DEFINE_TESTCASE(failedreplace1, glass) {
839 Xapian::WritableDatabase db(get_writable_database());
840 Xapian::Document doc;
841 doc.add_term("foo");
842 db.add_document(doc);
843 Xapian::docid did = db.add_document(doc);
844 doc.add_term("abc");
845 doc.add_term(string(1000, 'm'));
846 doc.add_term("xyz");
847 TEST_EXCEPTION(Xapian::InvalidArgumentError, db.replace_document(did, doc));
848 db.commit();
849 TEST_EQUAL(db.get_doccount(), 0);
850 TEST_EQUAL(db.get_termfreq("foo"), 0);
851 return true;
854 DEFINE_TESTCASE(failedreplace2, glass) {
855 Xapian::WritableDatabase db(get_writable_database("apitest_simpledata"));
856 db.commit();
857 Xapian::doccount db_size = db.get_doccount();
858 Xapian::Document doc;
859 doc.set_data("wibble");
860 doc.add_term("foo");
861 doc.add_value(0, "seven");
862 db.add_document(doc);
863 Xapian::docid did = db.add_document(doc);
864 doc.add_term("abc");
865 doc.add_term(string(1000, 'm'));
866 doc.add_term("xyz");
867 doc.add_value(0, "six");
868 TEST_EXCEPTION(Xapian::InvalidArgumentError, db.replace_document(did, doc));
869 db.commit();
870 TEST_EQUAL(db.get_doccount(), db_size);
871 TEST_EQUAL(db.get_termfreq("foo"), 0);
872 return true;
875 /// Coverage for SelectPostList::skip_to().
876 DEFINE_TESTCASE(phrase3, positional) {
877 Xapian::Database db = get_database("apitest_phrase");
879 static const char * const phrase_words[] = { "phrase", "near" };
880 Xapian::Query q(Xapian::Query::OP_NEAR, phrase_words, phrase_words + 2, 12);
881 q = Xapian::Query(Xapian::Query::OP_AND_MAYBE, Xapian::Query("pad"), q);
883 Xapian::Enquire enquire(db);
884 enquire.set_query(q);
885 Xapian::MSet mset = enquire.get_mset(0, 5);
887 return true;
890 /// Check that get_mset(<large number>, 10) doesn't exhaust memory needlessly.
891 // Regression test for fix in 1.2.4.
892 DEFINE_TESTCASE(msetfirst2, backend) {
893 Xapian::Database db(get_database("apitest_simpledata"));
894 Xapian::Enquire enquire(db);
895 enquire.set_query(Xapian::Query("paragraph"));
896 Xapian::MSet mset;
897 // Before the fix, this tried to allocate too much memory.
898 mset = enquire.get_mset(0xfffffff0, 1);
899 TEST_EQUAL(mset.get_firstitem(), 0xfffffff0);
900 // Check that the number of documents gets clamped too.
901 mset = enquire.get_mset(1, 0xfffffff0);
902 TEST_EQUAL(mset.get_firstitem(), 1);
903 // Another regression test - MatchNothing used to give an MSet with
904 // get_firstitem() returning 0.
905 enquire.set_query(Xapian::Query::MatchNothing);
906 mset = enquire.get_mset(1, 1);
907 TEST_EQUAL(mset.get_firstitem(), 1);
908 return true;
911 DEFINE_TESTCASE(bm25weight2, backend) {
912 Xapian::Database db(get_database("etext"));
913 Xapian::Enquire enquire(db);
914 enquire.set_query(Xapian::Query("the"));
915 enquire.set_weighting_scheme(Xapian::BM25Weight(0, 0, 0, 0, 1));
916 Xapian::MSet mset = enquire.get_mset(0, 100);
917 TEST_REL(mset.size(),>=,2);
918 double weight0 = mset[0].get_weight();
919 for (size_t i = 1; i != mset.size(); ++i) {
920 TEST_EQUAL(weight0, mset[i].get_weight());
922 return true;
925 DEFINE_TESTCASE(unigramlmweight2, backend) {
926 Xapian::Database db(get_database("etext"));
927 Xapian::Enquire enquire(db);
928 enquire.set_query(Xapian::Query("the"));
929 enquire.set_weighting_scheme(Xapian::LMWeight());
930 Xapian::MSet mset = enquire.get_mset(0, 100);
931 TEST_REL(mset.size(),>=,2);
932 return true;
935 DEFINE_TESTCASE(tradweight2, backend) {
936 Xapian::Database db(get_database("etext"));
937 Xapian::Enquire enquire(db);
938 enquire.set_query(Xapian::Query("the"));
939 enquire.set_weighting_scheme(Xapian::TradWeight(0));
940 Xapian::MSet mset = enquire.get_mset(0, 100);
941 TEST_REL(mset.size(),>=,2);
942 double weight0 = mset[0].get_weight();
943 for (size_t i = 1; i != mset.size(); ++i) {
944 TEST_EQUAL(weight0, mset[i].get_weight());
946 return true;
949 // Regression test for bug fix in 1.2.9.
950 DEFINE_TESTCASE(emptydb1, backend) {
951 Xapian::Database db(get_database(string()));
952 static const Xapian::Query::op ops[] = {
953 Xapian::Query::OP_AND,
954 Xapian::Query::OP_OR,
955 Xapian::Query::OP_AND_NOT,
956 Xapian::Query::OP_XOR,
957 Xapian::Query::OP_AND_MAYBE,
958 Xapian::Query::OP_FILTER,
959 Xapian::Query::OP_NEAR,
960 Xapian::Query::OP_PHRASE,
961 Xapian::Query::OP_ELITE_SET
963 const Xapian::Query::op * p;
964 for (p = ops; p - ops != sizeof(ops) / sizeof(*ops); ++p) {
965 tout << *p << endl;
966 Xapian::Enquire enquire(db);
967 Xapian::Query query(*p, Xapian::Query("a"), Xapian::Query("b"));
968 enquire.set_query(query);
969 Xapian::MSet mset = enquire.get_mset(0, 10);
970 TEST_EQUAL(mset.get_matches_estimated(), 0);
971 TEST_EQUAL(mset.get_matches_upper_bound(), 0);
972 TEST_EQUAL(mset.get_matches_lower_bound(), 0);
974 return true;
977 /// Test error opening non-existent stub databases.
978 // Regression test for bug fixed in 1.3.1 and 1.2.11.
979 DEFINE_TESTCASE(stubdb7, !backend) {
980 TEST_EXCEPTION(Xapian::DatabaseOpeningError,
981 Xapian::Database("nosuchdirectory", Xapian::DB_BACKEND_STUB));
982 TEST_EXCEPTION(Xapian::DatabaseOpeningError,
983 Xapian::WritableDatabase("nosuchdirectory",
984 Xapian::DB_OPEN|Xapian::DB_BACKEND_STUB));
985 return true;
988 /// Test which checks the weights are as expected.
989 // This runs for multi_* too, so serves to check that we get the same weights
990 // with multiple databases as without.
991 DEFINE_TESTCASE(msetweights1, backend) {
992 Xapian::Database db = get_database("apitest_simpledata");
993 Xapian::Enquire enq(db);
994 Xapian::Query q(Xapian::Query::OP_OR,
995 Xapian::Query("paragraph"),
996 Xapian::Query("word"));
997 enq.set_query(q);
998 // 5 documents match, and the 4th and 5th have the same weight, so ask for
999 // 4 as that's a good test that we get the right one in this case.
1000 Xapian::MSet mset = enq.get_mset(0, 4);
1002 static const struct { Xapian::docid did; double wt; } expected[] = {
1003 { 2, 1.2058248004573934864 },
1004 { 4, 0.81127876655507624726 },
1005 { 1, 0.17309550762546158098 },
1006 { 3, 0.14609528172558261527 }
1009 TEST_EQUAL(mset.size(), sizeof(expected) / sizeof(expected[0]));
1010 for (size_t i = 0; i < mset.size(); ++i) {
1011 TEST_EQUAL(*mset[i], expected[i].did);
1012 TEST_EQUAL_DOUBLE(mset[i].get_weight(), expected[i].wt);
1015 // Now test a query which matches only even docids, so in the multi case
1016 // one subdatabase doesn't match.
1017 enq.set_query(Xapian::Query("one"));
1018 mset = enq.get_mset(0, 3);
1020 static const struct { Xapian::docid did; double wt; } expected2[] = {
1021 { 6, 0.73354729848273669823 },
1022 { 2, 0.45626501034348893038 }
1025 TEST_EQUAL(mset.size(), sizeof(expected2) / sizeof(expected2[0]));
1026 for (size_t i = 0; i < mset.size(); ++i) {
1027 TEST_EQUAL(*mset[i], expected2[i].did);
1028 TEST_EQUAL_DOUBLE(mset[i].get_weight(), expected2[i].wt);
1031 return true;
1034 DEFINE_TESTCASE(itorskiptofromend1, backend) {
1035 Xapian::Database db = get_database("apitest_simpledata");
1037 Xapian::TermIterator t = db.termlist_begin(1);
1038 t.skip_to("zzzzz");
1039 TEST(t == db.termlist_end(1));
1040 // This worked in 1.2.x but segfaulted in 1.3.1.
1041 t.skip_to("zzzzzz");
1043 Xapian::PostingIterator p = db.postlist_begin("one");
1044 p.skip_to(99999);
1045 TEST(p == db.postlist_end("one"));
1046 // This segfaulted prior to 1.3.2.
1047 p.skip_to(999999);
1049 Xapian::PositionIterator i = db.positionlist_begin(6, "one");
1050 i.skip_to(99999);
1051 TEST(i == db.positionlist_end(6, "one"));
1052 // This segfaulted prior to 1.3.2.
1053 i.skip_to(999999);
1055 Xapian::ValueIterator v = db.valuestream_begin(1);
1056 v.skip_to(99999);
1057 TEST(v == db.valuestream_end(1));
1058 // These segfaulted prior to 1.3.2.
1059 v.skip_to(999999);
1060 v.check(9999999);
1062 return true;
1065 /// Check handling of invalid block sizes.
1066 // Regression test for bug fixed in 1.2.17 and 1.3.2 - the size gets fixed
1067 // but the uncorrected size was passed to the base file. Also, abort() was
1068 // called on 0.
1069 DEFINE_TESTCASE(blocksize1, glass) {
1070 string db_dir = "." + get_dbtype();
1071 mkdir(db_dir.c_str(), 0755);
1072 db_dir += "/db__blocksize1";
1073 int flags;
1074 if (get_dbtype() == "glass") {
1075 flags = Xapian::DB_CREATE|Xapian::DB_BACKEND_GLASS;
1076 } else {
1077 FAIL_TEST("Unhandled backend type");
1079 static const unsigned bad_sizes[] = {
1080 65537, 8000, 2000, 1024, 16, 7, 3, 1, 0
1082 for (size_t i = 0; i < sizeof(bad_sizes) / sizeof(bad_sizes[0]); ++i) {
1083 size_t block_size = bad_sizes[i];
1084 rm_rf(db_dir);
1085 Xapian::WritableDatabase db(db_dir, flags, block_size);
1086 Xapian::Document doc;
1087 doc.add_term("XYZ");
1088 doc.set_data("foo");
1089 db.add_document(doc);
1090 db.commit();
1092 return true;
1095 /// Feature test for Xapian::DB_NO_TERMLIST.
1096 DEFINE_TESTCASE(notermlist1, glass) {
1097 string db_dir = "." + get_dbtype();
1098 mkdir(db_dir.c_str(), 0755);
1099 db_dir += "/db__notermlist1";
1100 int flags = Xapian::DB_CREATE|Xapian::DB_NO_TERMLIST;
1101 if (get_dbtype() == "glass") {
1102 flags |= Xapian::DB_BACKEND_GLASS;
1104 rm_rf(db_dir);
1105 Xapian::WritableDatabase db(db_dir, flags);
1106 Xapian::Document doc;
1107 doc.add_term("hello");
1108 doc.add_value(42, "answer");
1109 db.add_document(doc);
1110 db.commit();
1111 TEST(!file_exists(db_dir + "/termlist.glass"));
1112 TEST_EXCEPTION(Xapian::FeatureUnavailableError, db.termlist_begin(1));
1113 return true;
1116 /// Regression test for bug starting a new glass freelist block.
1117 DEFINE_TESTCASE(newfreelistblock1, writable) {
1118 Xapian::Document doc;
1119 doc.add_term("foo");
1120 for (int i = 100; i < 120; ++i) {
1121 doc.add_term(str(i));
1124 Xapian::WritableDatabase wdb(get_writable_database());
1125 for (int j = 0; j < 50; ++j) {
1126 wdb.add_document(doc);
1128 wdb.commit();
1130 for (int k = 0; k < 1000; ++k) {
1131 wdb.add_document(doc);
1132 wdb.commit();
1135 return true;
1138 /** Check that the parent directory for the database doesn't need to be
1139 * writable. Regression test for early versions on the glass new btree
1140 * branch which failed to append a "/" when generating a temporary filename
1141 * from the database directory.
1143 DEFINE_TESTCASE(readonlyparentdir1, glass) {
1144 #if !defined __WIN32__ && !defined __CYGWIN__ && !defined __OS2__
1145 string path = get_named_writable_database_path("readonlyparentdir1");
1146 // Fix permissions if the previous test was killed.
1147 (void)chmod(path.c_str(), 0700);
1148 mkdir(path.c_str(), 0777);
1149 mkdir((path + "/sub").c_str(), 0777);
1150 Xapian::WritableDatabase db = get_named_writable_database("readonlyparentdir1/sub");
1151 TEST(chmod(path.c_str(), 0500) == 0);
1152 try {
1153 Xapian::Document doc;
1154 doc.add_term("hello");
1155 doc.set_data("some text");
1156 db.add_document(doc);
1157 db.commit();
1158 } catch (...) {
1159 // Attempt to fix the permissions, otherwise things like "rm -rf" on
1160 // the source tree will fail.
1161 (void)chmod(path.c_str(), 0700);
1162 throw;
1164 TEST(chmod(path.c_str(), 0700) == 0);
1165 #endif
1166 return true;
1169 static void
1170 make_phrasebug1_db(Xapian::WritableDatabase &db, const string &)
1172 Xapian::Document doc;
1173 doc.add_posting("hurricane", 199881);
1174 doc.add_posting("hurricane", 203084);
1175 doc.add_posting("katrina", 199882);
1176 doc.add_posting("katrina", 202473);
1177 doc.add_posting("katrina", 203085);
1178 db.add_document(doc);
1181 /// Regression test for ticket#653, fixed in 1.3.2 and 1.2.19.
1182 DEFINE_TESTCASE(phrasebug1, generated && positional) {
1183 Xapian::Database db = get_database("phrasebug1", make_phrasebug1_db);
1184 static const char * const qterms[] = { "katrina", "hurricane" };
1185 Xapian::Enquire e(db);
1186 Xapian::Query q(Xapian::Query::OP_PHRASE, qterms, qterms + 2, 5);
1187 e.set_query(q);
1188 Xapian::MSet mset = e.get_mset(0, 100);
1189 TEST_EQUAL(mset.size(), 0);
1190 static const char * const qterms2[] = { "hurricane", "katrina" };
1191 Xapian::Query q2(Xapian::Query::OP_PHRASE, qterms2, qterms2 + 2, 5);
1192 e.set_query(q2);
1193 mset = e.get_mset(0, 100);
1194 TEST_EQUAL(mset.size(), 1);
1195 return true;
1198 /// Feature test for Xapian::DB_RETRY_LOCK
1199 DEFINE_TESTCASE(retrylock1, writable && !inmemory && !remote) {
1200 // FIXME: Can't see an easy way to test this for remote databases - the
1201 // harness doesn't seem to provide a suitable way to reopen a remote.
1202 #if defined HAVE_FORK && defined HAVE_SOCKETPAIR
1203 int fds[2];
1204 if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, PF_UNSPEC, fds) < 0) {
1205 FAIL_TEST("socketpair() failed");
1207 if (fcntl(fds[1], F_SETFL, O_NONBLOCK) < 0)
1208 FAIL_TEST("fcntl() failed to set O_NONBLOCK");
1209 pid_t child = fork();
1210 if (child == -1)
1211 FAIL_TEST("fork() failed");
1212 if (child == 0) {
1213 // Wait for signal that parent has opened the database.
1214 char ch;
1215 while (read(fds[0], &ch, 1) < 0) { }
1217 try {
1218 Xapian::WritableDatabase db2(get_named_writable_database_path("retrylock1"),
1219 Xapian::DB_OPEN|Xapian::DB_RETRY_LOCK);
1220 if (write(fds[0], "y", 1)) { }
1221 } catch (const Xapian::DatabaseLockError &) {
1222 if (write(fds[0], "l", 1)) { }
1223 } catch (const Xapian::Error &e) {
1224 const string & m = e.get_description();
1225 if (write(fds[0], m.data(), m.size())) { }
1226 } catch (...) {
1227 if (write(fds[0], "o", 1)) { }
1229 _exit(0);
1232 close(fds[0]);
1234 Xapian::WritableDatabase db = get_named_writable_database("retrylock1");
1235 if (write(fds[1], "", 1) != 1)
1236 FAIL_TEST("Failed to signal to child process");
1238 char result[256];
1239 int r = read(fds[1], result, sizeof(result));
1240 if (r == -1) {
1241 if (errno == EAGAIN) {
1242 // Good.
1243 result[0] = 'y';
1244 } else {
1245 // Error.
1246 tout << "errno=" << errno << ": " << strerror(errno) << endl;
1247 result[0] = 'e';
1249 r = 1;
1250 } else if (r >= 1) {
1251 if (result[0] == 'y') {
1252 // Child process managed to also get write lock!
1253 result[0] = '!';
1255 } else {
1256 // EOF.
1257 result[0] = 'z';
1258 r = 1;
1261 try {
1262 db.close();
1263 } catch (...) {
1264 kill(child, SIGKILL);
1265 int status;
1266 while (waitpid(child, &status, 0) < 0) {
1267 if (errno != EINTR) break;
1269 throw;
1272 if (result[0] == 'y') {
1273 retry:
1274 struct timeval tv;
1275 tv.tv_sec = 3;
1276 tv.tv_usec = 0;
1277 fd_set f;
1278 FD_ZERO(&f);
1279 FD_SET(fds[1], &f);
1280 int sr = select(fds[1] + 1, &f, NULL, &f, &tv);
1281 if (sr == 0) {
1282 // Timed out.
1283 result[0] = 'T';
1284 r = 1;
1285 } else if (sr == -1) {
1286 if (errno == EINTR || errno == EAGAIN)
1287 goto retry;
1288 tout << "select() failed with errno=" << errno << ": " << strerror(errno) << endl;
1289 result[0] = 'S';
1290 r = 1;
1291 } else {
1292 r = read(fds[1], result, sizeof(result));
1293 if (r == -1) {
1294 // Error.
1295 tout << "read failed with errno=" << errno << ": " << strerror(errno) << endl;
1296 result[0] = 'R';
1297 r = 1;
1298 } else if (r == 0) {
1299 // EOF.
1300 result[0] = 'Z';
1301 r = 1;
1306 close(fds[1]);
1308 kill(child, SIGKILL);
1309 int status;
1310 while (waitpid(child, &status, 0) < 0) {
1311 if (errno != EINTR) break;
1314 tout << string(result, r) << endl;
1315 TEST_EQUAL(result[0], 'y');
1316 #endif
1318 return true;
1321 // Opening a WritableDatabase with low fds available - it should avoid them.
1322 DEFINE_TESTCASE(dbfilefd012, glass) {
1323 #if !defined __WIN32__ && !defined __CYGWIN__ && !defined __OS2__
1324 int oldfds[3];
1325 for (int i = 0; i < 3; ++i) {
1326 oldfds[i] = dup(i);
1328 try {
1329 for (int j = 0; j < 3; ++j) {
1330 close(j);
1331 TEST_REL(lseek(j, 0, SEEK_CUR), <, 0);
1332 TEST_EQUAL(errno, EBADF);
1335 Xapian::WritableDatabase db = get_writable_database();
1337 // Check we didn't use any of those low fds for tables, as that risks
1338 // data corruption if some other code in the same process tries to
1339 // write to them (see #651).
1340 for (int fd = 0; fd < 3; ++fd) {
1341 // Check that the fd is still closed, or isn't open O_RDWR (the
1342 // lock file gets opened O_WRONLY), or it's a pipe (if we're using
1343 // a child process to hold a non-OFD fcntl lock).
1344 int flags = fcntl(fd, F_GETFL);
1345 if (flags == -1) {
1346 TEST_EQUAL(errno, EBADF);
1347 } else if ((flags & O_ACCMODE) != O_RDWR) {
1348 // OK.
1349 } else {
1350 struct stat sb;
1351 TEST_NOT_EQUAL(fstat(fd, &sb), -1);
1352 #ifdef S_ISSOCK
1353 TEST(S_ISSOCK(sb.st_mode));
1354 #else
1355 // If we can't check it is a socket, at least check it is not a
1356 // regular file.
1357 TEST(!S_ISREG(sb.st_mode));
1358 #endif
1361 } catch (...) {
1362 for (int j = 0; j < 3; ++j) {
1363 dup2(oldfds[j], j);
1364 close(oldfds[j]);
1366 throw;
1369 for (int j = 0; j < 3; ++j) {
1370 dup2(oldfds[j], j);
1371 close(oldfds[j]);
1373 #endif
1375 return true;
1378 /// Regression test for #675, fixed in 1.3.3 and 1.2.21.
1379 DEFINE_TESTCASE(cursorbug1, glass) {
1380 Xapian::WritableDatabase wdb = get_writable_database();
1381 Xapian::Database db = get_writable_database_as_database();
1382 Xapian::Enquire enq(db);
1383 enq.set_query(Xapian::Query::MatchAll);
1384 Xapian::MSet mset;
1385 // The original problem triggered for chert and glass on repeat==7.
1386 for (int repeat = 0; repeat < 10; ++repeat) {
1387 tout.str(string());
1388 tout << "iteration #" << repeat << endl;
1390 const int ITEMS = 10;
1391 int free_id = db.get_doccount();
1392 int offset = max(free_id, ITEMS * 2) - (ITEMS * 2);
1393 int limit = offset + (ITEMS * 2);
1395 mset = enq.get_mset(offset, limit);
1396 for (Xapian::MSetIterator m1 = mset.begin(); m1 != mset.end(); ++m1) {
1397 (void)m1.get_document().get_value(0);
1400 for (int i = free_id; i <= free_id + ITEMS; ++i) {
1401 Xapian::Document doc;
1402 const string & id = str(i);
1403 string qterm = "Q" + id;
1404 doc.add_value(0, id);
1405 doc.add_boolean_term(qterm);
1406 wdb.replace_document(qterm, doc);
1408 wdb.commit();
1410 db.reopen();
1411 mset = enq.get_mset(offset, limit);
1412 for (Xapian::MSetIterator m2 = mset.begin(); m2 != mset.end(); ++m2) {
1413 (void)m2.get_document().get_value(0);
1417 return true;
1420 // Regression test for #674, fixed in 1.2.21 and 1.3.3.
1421 DEFINE_TESTCASE(sortvalue2, backend) {
1422 Xapian::Database db = get_database("apitest_simpledata");
1423 db.add_database(get_database("apitest_simpledata2"));
1424 Xapian::Enquire enq(db);
1425 enq.set_query(Xapian::Query::MatchAll);
1426 enq.set_sort_by_value(0, false);
1427 Xapian::MSet mset = enq.get_mset(0, 50);
1429 // Check all results are in key order - the bug was that they were sorted
1430 // by docid instead with multiple remote databases.
1431 string old_key;
1432 for (Xapian::MSetIterator i = mset.begin(); i != mset.end(); ++i) {
1433 string key = db.get_document(*i).get_value(0);
1434 TEST(old_key <= key);
1435 swap(old_key, key);
1437 return true;
1440 /// Check behaviour of Enquire::get_query().
1441 DEFINE_TESTCASE(enquiregetquery1, backend) {
1442 Xapian::Database db = get_database("apitest_simpledata");
1443 Xapian::Enquire enq(db);
1444 TEST_EQUAL(enq.get_query().get_description(), "Query()");
1445 return true;
1448 DEFINE_TESTCASE(embedded1, singlefile) {
1449 // In reality you should align the embedded database to a multiple of
1450 // database block size, but any offset is meant to work.
1451 off_t offset = 1234;
1453 Xapian::Database db = get_database("apitest_simpledata");
1454 const string & db_path = get_database_path("apitest_simpledata");
1455 const string & tmp_path = db_path + "-embedded";
1456 ofstream out(tmp_path, fstream::trunc|fstream::binary);
1457 out.seekp(offset);
1458 out << ifstream(db_path, fstream::binary).rdbuf();
1459 out.close();
1462 int fd = open(tmp_path.c_str(), O_RDONLY|O_BINARY);
1463 lseek(fd, offset, SEEK_SET);
1464 Xapian::Database db_embedded(fd);
1465 TEST_EQUAL(db.get_doccount(), db_embedded.get_doccount());
1469 int fd = open(tmp_path.c_str(), O_RDONLY|O_BINARY);
1470 lseek(fd, offset, SEEK_SET);
1471 size_t check_errors =
1472 Xapian::Database::check(fd, Xapian::DBCHECK_SHOW_STATS, &tout);
1473 TEST_EQUAL(check_errors, 0);
1476 return true;
1479 /// Regression test for bug fixed in 1.3.7.
1480 DEFINE_TESTCASE(exactxor1, backend) {
1481 Xapian::Database db = get_database("apitest_simpledata");
1482 Xapian::Enquire enq(db);
1484 static const char * const words[4] = {
1485 "blank", "test", "paragraph", "banana"
1487 Xapian::Query q(Xapian::Query::OP_XOR, words, words + 4);
1488 enq.set_query(q);
1489 enq.set_weighting_scheme(Xapian::BoolWeight());
1490 Xapian::MSet mset = enq.get_mset(0, 0);
1491 // A reversed conditional gave us 5 in this case.
1492 TEST_EQUAL(mset.get_matches_upper_bound(), 6);
1493 // Test improved lower bound in 1.3.7 (earlier versions gave 0).
1494 TEST_EQUAL(mset.get_matches_lower_bound(), 2);
1496 static const char * const words2[4] = {
1497 "queri", "test", "paragraph", "word"
1499 Xapian::Query q2(Xapian::Query::OP_XOR, words2, words2 + 4);
1500 enq.set_query(q2);
1501 enq.set_weighting_scheme(Xapian::BoolWeight());
1502 mset = enq.get_mset(0, 0);
1503 // A reversed conditional gave us 6 in this case.
1504 TEST_EQUAL(mset.get_matches_upper_bound(), 5);
1505 // Test improved lower bound in 1.3.7 (earlier versions gave 0).
1506 TEST_EQUAL(mset.get_matches_lower_bound(), 1);
1508 return true;
1511 /// Feature test for Database::get_revision().
1512 DEFINE_TESTCASE(getrevision1, glass) {
1513 Xapian::WritableDatabase db = get_writable_database();
1514 TEST_EQUAL(db.get_revision(), 0);
1515 db.commit();
1516 TEST_EQUAL(db.get_revision(), 0);
1517 Xapian::Document doc;
1518 doc.add_term("hello");
1519 db.add_document(doc);
1520 TEST_EQUAL(db.get_revision(), 0);
1521 db.commit();
1522 TEST_EQUAL(db.get_revision(), 1);
1523 db.commit();
1524 TEST_EQUAL(db.get_revision(), 1);
1525 db.add_document(doc);
1526 db.commit();
1527 TEST_EQUAL(db.get_revision(), 2);
1528 return true;
1531 /// Feature test for DOC_ASSUME_VALID.
1532 DEFINE_TESTCASE(getdocumentlazy1, backend) {
1533 Xapian::Database db = get_database("apitest_simpledata");
1534 Xapian::Document doc_lazy = db.get_document(2, Xapian::DOC_ASSUME_VALID);
1535 Xapian::Document doc = db.get_document(2);
1536 TEST_EQUAL(doc.get_data(), doc_lazy.get_data());
1537 TEST_EQUAL(doc.get_value(0), doc_lazy.get_value(0));
1538 return true;
1541 /// Feature test for DOC_ASSUME_VALID for a docid that doesn't actually exist.
1542 DEFINE_TESTCASE(getdocumentlazy2, backend) {
1543 Xapian::Database db = get_database("apitest_simpledata");
1544 Xapian::Document doc;
1545 try {
1546 doc = db.get_document(db.get_lastdocid() + 1, Xapian::DOC_ASSUME_VALID);
1547 } catch (const Xapian::DocNotFoundError&) {
1548 // DOC_ASSUME_VALID is really just a hint, so ignoring is OK (the
1549 // remote backend currently does).
1551 TEST(doc.get_data().empty());
1552 TEST_EXCEPTION(Xapian::DocNotFoundError,
1553 doc = db.get_document(db.get_lastdocid() + 1);
1555 return true;
1558 static void
1559 gen_uniqterms_gt_doclen_db(Xapian::WritableDatabase& db, const string&)
1561 Xapian::Document doc;
1562 doc.add_term("foo");
1563 doc.add_boolean_term("bar");
1564 db.add_document(doc);
1565 Xapian::Document doc2;
1566 doc2.add_posting("foo", 0, 2);
1567 doc2.add_term("foo2");
1568 doc2.add_boolean_term("baz");
1569 doc2.add_boolean_term("baz2");
1570 db.add_document(doc2);
1573 DEFINE_TESTCASE(getuniqueterms1, generated) {
1574 Xapian::Database db =
1575 get_database("uniqterms_gt_doclen", gen_uniqterms_gt_doclen_db);
1577 auto unique1 = db.get_unique_terms(1);
1578 TEST_REL(unique1, <=, db.get_doclength(1));
1579 TEST_REL(unique1, <, db.get_document(1).termlist_count());
1580 // Ideally it'd be equal to 1, and in this case it is, but the current
1581 // backends can't always efficiently ensure an exact answer.
1582 TEST_REL(unique1, >=, 1);
1584 auto unique2 = db.get_unique_terms(2);
1585 TEST_REL(unique2, <=, db.get_doclength(2));
1586 TEST_REL(unique2, <, db.get_document(2).termlist_count());
1587 // Ideally it'd be equal to 2, but the current backends can't always
1588 // efficiently ensure an exact answer and here it is actually 3.
1589 TEST_REL(unique2, >=, 2);
1591 return true;
1594 /** Regression test for bug fixed in 1.4.6.
1596 * OP_NEAR would think a term without positional information occurred at
1597 * position 1 if it had the lowest term frequency amongst the OP_NEAR's
1598 * subqueries.
1600 DEFINE_TESTCASE(nopositionbug1, generated) {
1601 Xapian::Database db =
1602 get_database("uniqterms_gt_doclen", gen_uniqterms_gt_doclen_db);
1604 // Test both orders.
1605 static const char* const terms1[] = { "foo", "baz" };
1606 static const char* const terms2[] = { "baz", "foo" };
1608 Xapian::Enquire enq(db);
1609 enq.set_query(Xapian::Query(Xapian::Query::OP_NEAR,
1610 begin(terms1), end(terms1), 10));
1611 TEST_EQUAL(enq.get_mset(0, 5).size(), 0);
1613 enq.set_query(Xapian::Query(Xapian::Query::OP_NEAR,
1614 begin(terms2), end(terms2), 10));
1615 TEST_EQUAL(enq.get_mset(0, 5).size(), 0);
1617 enq.set_query(Xapian::Query(Xapian::Query::OP_PHRASE,
1618 begin(terms1), end(terms1), 10));
1619 TEST_EQUAL(enq.get_mset(0, 5).size(), 0);
1621 enq.set_query(Xapian::Query(Xapian::Query::OP_PHRASE,
1622 begin(terms2), end(terms2), 10));
1623 TEST_EQUAL(enq.get_mset(0, 5).size(), 0);
1625 // Exercise exact phrase case too.
1626 enq.set_query(Xapian::Query(Xapian::Query::OP_PHRASE,
1627 begin(terms1), end(terms1), 2));
1628 TEST_EQUAL(enq.get_mset(0, 5).size(), 0);
1630 enq.set_query(Xapian::Query(Xapian::Query::OP_PHRASE,
1631 begin(terms2), end(terms2), 2));
1632 TEST_EQUAL(enq.get_mset(0, 5).size(), 0);
1634 return true;
1637 /// Check estimate is rounded to suitable number of S.F. - new in 1.4.3.
1638 DEFINE_TESTCASE(estimaterounding1, backend) {
1639 Xapian::Database db = get_database("etext");
1640 Xapian::Enquire enquire(db);
1641 enquire.set_query(Xapian::Query("the") | Xapian::Query("road"));
1642 Xapian::MSet mset = enquire.get_mset(0, 10);
1643 // MSet::get_description() includes bounds and raw estimate.
1644 tout << mset.get_description() << endl;
1645 // Bounds are 411-439, raw estimate is 419.
1646 TEST_EQUAL(mset.get_matches_estimated() % 10, 0);
1647 enquire.set_query(Xapian::Query("king") | Xapian::Query("prussia"));
1648 mset = enquire.get_mset(0, 10);
1649 tout << mset.get_description() << endl;
1650 // Bounds are 111-138, raw estimate is 133.
1651 TEST_EQUAL(mset.get_matches_estimated() % 10, 0);
1652 return true;
1655 /** Check that a TermIterator returns the correct termfreqs.
1657 * Prior to 1.5.0, the termfreq was approximated in the multidatabase case.
1659 DEFINE_TESTCASE(termitertf1, backend) {
1660 Xapian::Database db = get_database("apitest_simpledata");
1661 Xapian::TermIterator t = db.termlist_begin(2);
1663 t.skip_to("mset");
1664 TEST_EQUAL(*t, "mset");
1665 TEST_EQUAL(t.get_termfreq(), 1);
1667 t.skip_to("paragraph");
1668 TEST_EQUAL(*t, "paragraph");
1669 TEST_EQUAL(t.get_termfreq(), 5);
1671 t.skip_to("queri");
1672 TEST_EQUAL(*t, "queri");
1673 TEST_EQUAL(t.get_termfreq(), 3);
1675 return true;