Fix bug in split position handling
[xapian.git] / xapian-core / tests / api_backend.cc
blobb9ee4b614102dee0beded97ae5d57d4406979a2d
1 /** @file api_backend.cc
2 * @brief Backend-related tests.
3 */
4 /* Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015,2016,2018 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 "filetests.h"
32 #include "str.h"
33 #include "testrunner.h"
34 #include "testsuite.h"
35 #include "testutils.h"
36 #include "unixcmds.h"
38 #include "apitest.h"
40 #include "safefcntl.h"
41 #include "safesysstat.h"
42 #include "safeunistd.h"
43 #ifdef HAVE_SOCKETPAIR
44 # include "safesyssocket.h"
45 # include <signal.h>
46 # include "safesyswait.h"
47 #endif
49 #include <fstream>
50 #include <iterator>
52 using namespace std;
54 /// Regression test - lockfile should honour umask, was only user-readable.
55 DEFINE_TESTCASE(lockfileumask1, chert || glass) {
56 #if !defined __WIN32__ && !defined __CYGWIN__ && !defined __OS2__
57 mode_t old_umask = umask(022);
58 try {
59 Xapian::WritableDatabase db = get_named_writable_database("lockfileumask1");
61 string path = get_named_writable_database_path("lockfileumask1");
62 path += "/flintlock";
64 struct stat statbuf;
65 TEST(stat(path.c_str(), &statbuf) == 0);
66 TEST_EQUAL(statbuf.st_mode & 0777, 0644);
67 } catch (...) {
68 umask(old_umask);
69 throw;
72 umask(old_umask);
73 #endif
75 return true;
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);
108 return true;
111 // Check that exceeding 32bit in combined database doesn't cause a problem
112 // when using 64bit docids.
113 DEFINE_TESTCASE(exceed32bitcombineddb1, writable) {
114 // Test case is for 64-bit Xapian::docid.
115 // FIXME: Though we should check that the overflow is handled gracefully
116 // for 32-bit...
117 if (sizeof(Xapian::docid) == 4) return true;
119 // The InMemory backend uses a vector for the documents, so trying to add
120 // a document with the maximum docid is likely to fail because we can't
121 // allocate enough memory!
122 SKIP_TEST_FOR_BACKEND("inmemory");
124 Xapian::WritableDatabase db1 = get_writable_database();
125 Xapian::WritableDatabase db2 = get_writable_database();
126 Xapian::Document doc;
127 doc.set_data("prose");
128 doc.add_term("word");
130 Xapian::docid max_id = 0xffffffff;
132 db1.replace_document(max_id, doc);
133 db2.replace_document(max_id, doc);
135 Xapian::Database db;
136 db.add_database(db1);
137 db.add_database(db2);
139 Xapian::Enquire enquire(db);
140 enquire.set_query(Xapian::Query::MatchAll);
141 Xapian::MSet mymset = enquire.get_mset(0, 10);
143 TEST_EQUAL(2, mymset.size());
145 for (Xapian::MSetIterator i = mymset.begin(); i != mymset.end(); ++i) {
146 TEST_EQUAL("prose", i.get_document().get_data());
149 return true;
152 DEFINE_TESTCASE(dbstats1, backend) {
153 Xapian::Database db = get_database("etext");
155 // Use precalculated values to avoid expending CPU cycles to calculate
156 // these every time without improving test coverage.
157 const Xapian::termcount min_len = 2;
158 const Xapian::termcount max_len = 532;
159 const Xapian::termcount max_wdf = 22;
161 if (get_dbtype() != "inmemory") {
162 // Should be exact as no deletions have happened.
163 TEST_EQUAL(db.get_doclength_upper_bound(), max_len);
164 TEST_EQUAL(db.get_doclength_lower_bound(), min_len);
165 } else {
166 // For inmemory, we usually give rather loose bounds.
167 TEST_REL(db.get_doclength_upper_bound(),>=,max_len);
168 TEST_REL(db.get_doclength_lower_bound(),<=,min_len);
171 if (get_dbtype() != "inmemory" && !startswith(get_dbtype(), "remote")) {
172 TEST_EQUAL(db.get_wdf_upper_bound("the"), max_wdf);
173 } else {
174 // For inmemory and remote backends, we usually give rather loose
175 // bounds (remote matches use tighter bounds, but querying the
176 // wdf bound gives a looser one).
177 TEST_REL(db.get_wdf_upper_bound("the"),>=,max_wdf);
180 // This failed with an assertion during development between 1.3.1 and
181 // 1.3.2.
182 TEST_EQUAL(db.get_wdf_upper_bound(""), 0);
184 return true;
187 // Check stats with a single document. In a multi-database situation, this
188 // gave 0 for get-_doclength_lower_bound() in 1.3.2.
189 DEFINE_TESTCASE(dbstats2, backend) {
190 Xapian::Database db = get_database("apitest_onedoc");
192 // Use precalculated values to avoid expending CPU cycles to calculate
193 // these every time without improving test coverage.
194 const Xapian::termcount min_len = 15;
195 const Xapian::termcount max_len = 15;
196 const Xapian::termcount max_wdf = 7;
198 if (get_dbtype() != "inmemory") {
199 // Should be exact as no deletions have happened.
200 TEST_EQUAL(db.get_doclength_upper_bound(), max_len);
201 TEST_EQUAL(db.get_doclength_lower_bound(), min_len);
202 } else {
203 // For inmemory, we usually give rather loose bounds.
204 TEST_REL(db.get_doclength_upper_bound(),>=,max_len);
205 TEST_REL(db.get_doclength_lower_bound(),<=,min_len);
208 if (get_dbtype() != "inmemory" && !startswith(get_dbtype(), "remote")) {
209 TEST_EQUAL(db.get_wdf_upper_bound("word"), max_wdf);
210 } else {
211 // For inmemory and remote backends, we usually give rather loose
212 // bounds (remote matches use tighter bounds, but querying the
213 // wdf bound gives a looser one).
214 TEST_REL(db.get_wdf_upper_bound("word"),>=,max_wdf);
217 TEST_EQUAL(db.get_wdf_upper_bound(""), 0);
219 return true;
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()));
230 return true;
233 /// Regression test for bug#392 in ModifiedPostList iteration, fixed in 1.0.15.
234 DEFINE_TESTCASE(modifiedpostlist1, writable) {
235 Xapian::WritableDatabase db = get_writable_database();
236 Xapian::Document a, b;
237 Xapian::Enquire enq(db);
239 a.add_term("T");
240 enq.set_query(Xapian::Query("T"));
242 db.replace_document(2, a);
243 db.commit();
244 db.replace_document(1, a);
245 db.replace_document(1, b);
247 mset_expect_order(enq.get_mset(0, 2), 2);
249 return true;
252 /// Regression test for chert bug fixed in 1.1.3 (ticket#397).
253 DEFINE_TESTCASE(doclenaftercommit1, writable) {
254 Xapian::WritableDatabase db = get_writable_database();
255 TEST_EXCEPTION(Xapian::DocNotFoundError, db.get_doclength(1));
256 TEST_EXCEPTION(Xapian::DocNotFoundError, db.get_unique_terms(1));
257 db.replace_document(1, Xapian::Document());
258 db.commit();
259 TEST_EQUAL(db.get_doclength(1), 0);
260 TEST_EQUAL(db.get_unique_terms(1), 0);
261 return true;
264 DEFINE_TESTCASE(valuesaftercommit1, writable) {
265 Xapian::WritableDatabase db = get_writable_database();
266 Xapian::Document doc;
267 doc.add_value(0, "value");
268 db.replace_document(2, doc);
269 db.commit();
270 db.replace_document(1, doc);
271 db.replace_document(3, doc);
272 TEST_EQUAL(db.get_document(3).get_value(0), "value");
273 db.commit();
274 TEST_EQUAL(db.get_document(3).get_value(0), "value");
275 return true;
278 DEFINE_TESTCASE(lockfilefd0or1, chert || glass) {
279 #if !defined __WIN32__ && !defined __CYGWIN__ && !defined __OS2__
280 int old_stdin = dup(0);
281 int old_stdout = dup(1);
282 try {
283 // With fd 0 available.
284 close(0);
286 Xapian::WritableDatabase db = get_writable_database();
287 TEST_EXCEPTION(Xapian::DatabaseLockError,
288 (void)get_writable_database_again());
290 // With fd 0 and fd 1 available.
291 close(1);
293 Xapian::WritableDatabase db = get_writable_database();
294 TEST_EXCEPTION(Xapian::DatabaseLockError,
295 (void)get_writable_database_again());
297 // With fd 1 available.
298 dup2(old_stdin, 0);
300 Xapian::WritableDatabase db = get_writable_database();
301 TEST_EXCEPTION(Xapian::DatabaseLockError,
302 (void)get_writable_database_again());
304 } catch (...) {
305 dup2(old_stdin, 0);
306 dup2(old_stdout, 1);
307 close(old_stdin);
308 close(old_stdout);
309 throw;
312 dup2(old_stdout, 1);
313 close(old_stdin);
314 close(old_stdout);
315 #endif
317 return true;
320 /// Regression test for bug fixed in 1.2.13 and 1.3.1.
321 DEFINE_TESTCASE(lockfilealreadyopen1, chert || glass) {
322 // Ensure database has been created.
323 (void)get_named_writable_database("lockfilealreadyopen1");
324 string path = get_named_writable_database_path("lockfilealreadyopen1");
325 int fd = ::open((path + "/flintlock").c_str(), O_RDONLY);
326 TEST(fd != -1);
327 try {
328 Xapian::WritableDatabase db(path, Xapian::DB_CREATE_OR_OPEN);
329 TEST_EXCEPTION(Xapian::DatabaseLockError,
330 Xapian::WritableDatabase db2(path, Xapian::DB_CREATE_OR_OPEN)
332 } catch (...) {
333 close(fd);
334 throw;
336 close(fd);
338 return true;
341 /// Feature tests for Database::locked().
342 DEFINE_TESTCASE(testlock1, chert || glass) {
343 Xapian::Database rdb;
344 TEST(!rdb.locked());
346 Xapian::WritableDatabase db = get_named_writable_database("testlock1");
347 TEST(db.locked());
348 Xapian::Database db_as_database = db;
349 TEST(db_as_database.locked());
350 TEST(!rdb.locked());
351 rdb = get_writable_database_as_database();
352 TEST(db.locked());
353 TEST(db_as_database.locked());
354 try {
355 TEST(rdb.locked());
356 } catch (const Xapian::FeatureUnavailableError&) {
357 SKIP_TEST("Database::locked() not supported on this platform");
359 db_as_database = rdb;
360 TEST(db.locked());
361 TEST(db_as_database.locked());
362 TEST(rdb.locked());
363 db_as_database.close();
364 TEST(db.locked());
365 TEST(rdb.locked());
366 // After close(), locked() should either work as if close() hadn't been
367 // called or throw Xapian::DatabaseError.
368 try {
369 TEST(db_as_database.locked());
370 } catch (const Xapian::DatabaseError&) {
372 db.close();
373 TEST(!rdb.locked());
374 try {
375 TEST(!db_as_database.locked());
376 } catch (const Xapian::DatabaseError&) {
379 TEST(!rdb.locked());
380 return true;
383 /// Test that locked() returns false for backends which don't support update.
384 /// Regression test for bug fixed in 1.4.6.
385 DEFINE_TESTCASE(testlock2, backend && !writable && !multi) {
386 Xapian::Database db = get_database("apitest_simpledata");
387 TEST(!db.locked());
388 return true;
391 class CheckMatchDecider : public Xapian::MatchDecider {
392 mutable bool called;
394 public:
395 CheckMatchDecider() : called(false) { }
397 bool operator()(const Xapian::Document &) const {
398 called = true;
399 return true;
402 bool was_called() const { return called; }
405 /// Test Xapian::MatchDecider with remote backend fails.
406 DEFINE_TESTCASE(matchdecider4, remote) {
407 Xapian::Database db(get_database("apitest_simpledata"));
408 Xapian::Enquire enquire(db);
409 enquire.set_query(Xapian::Query("paragraph"));
411 CheckMatchDecider mdecider;
412 Xapian::MSet mset;
414 TEST_EXCEPTION(Xapian::UnimplementedError,
415 mset = enquire.get_mset(0, 10, NULL, &mdecider));
416 TEST(!mdecider.was_called());
418 return true;
421 /** Check that replacing an unmodified document doesn't increase the automatic
422 * flush counter. Regression test for bug fixed in 1.1.4/1.0.18.
424 DEFINE_TESTCASE(replacedoc7, writable && !inmemory && !remote) {
425 // The inmemory backend doesn't batch changes, so there's nothing to
426 // check there.
428 // The remote backend doesn't implement the lazy replacement of documents
429 // optimisation currently.
430 Xapian::WritableDatabase db(get_writable_database());
431 Xapian::Document doc;
432 doc.set_data("fish");
433 doc.add_term("Hlocalhost");
434 doc.add_posting("hello", 1);
435 doc.add_posting("world", 2);
436 doc.add_value(1, "myvalue");
437 db.add_document(doc);
438 db.commit();
440 // We add a second document, and then replace the first document with
441 // itself 10000 times. If the document count for the database reopened
442 // read-only is 2, then we triggered an automatic commit.
444 doc.add_term("XREV2");
445 db.add_document(doc);
447 for (int i = 0; i < 10000; ++i) {
448 doc = db.get_document(1);
449 db.replace_document(1, doc);
452 Xapian::Database rodb(get_writable_database_as_database());
453 TEST_EQUAL(rodb.get_doccount(), 1);
455 db.flush();
456 TEST(rodb.reopen());
458 TEST_EQUAL(rodb.get_doccount(), 2);
459 return true;
462 /** Check that replacing a document deleted since the last flush works.
463 * Prior to 1.1.4/1.0.18, this failed to update the collection frequency and
464 * wdf, and caused an assertion failure when assertions were enabled.
466 DEFINE_TESTCASE(replacedoc8, writable) {
467 Xapian::WritableDatabase db(get_writable_database());
469 Xapian::Document doc;
470 doc.set_data("fish");
471 doc.add_term("takeaway");
472 db.add_document(doc);
474 db.delete_document(1);
476 Xapian::Document doc;
477 doc.set_data("chips");
478 doc.add_term("takeaway", 2);
479 db.replace_document(1, doc);
481 db.flush();
482 TEST_EQUAL(db.get_collection_freq("takeaway"), 2);
483 Xapian::PostingIterator p = db.postlist_begin("takeaway");
484 TEST(p != db.postlist_end("takeaway"));
485 TEST_EQUAL(p.get_wdf(), 2);
486 return true;
489 /// Test coverage for DatabaseModifiedError.
490 DEFINE_TESTCASE(databasemodified1, writable && !inmemory && !remote && !multi) {
491 // The inmemory backend doesn't support revisions.
493 // The remote backend doesn't work as expected here, I think due to
494 // test harness issues.
496 // With multi, DatabaseModifiedError doesn't trigger as easily.
497 Xapian::WritableDatabase db(get_writable_database());
498 Xapian::Document doc;
499 doc.set_data("cargo");
500 doc.add_term("abc");
501 doc.add_term("def");
502 doc.add_term("ghi");
503 const int N = 500;
504 for (int i = 0; i < N; ++i) {
505 db.add_document(doc);
507 db.commit();
509 Xapian::Database rodb(get_writable_database_as_database());
510 db.add_document(doc);
511 db.commit();
513 db.add_document(doc);
514 db.commit();
516 db.add_document(doc);
517 try {
518 TEST_EQUAL(*rodb.termlist_begin(N - 1), "abc");
519 return false;
520 } catch (const Xapian::DatabaseModifiedError &) {
523 try {
524 Xapian::Enquire enq(rodb);
525 enq.set_query(Xapian::Query("abc"));
526 Xapian::MSet mset = enq.get_mset(0, 10);
527 return false;
528 } catch (const Xapian::DatabaseModifiedError &) {
531 return true;
534 /// Regression test for bug#462 fixed in 1.0.19 and 1.1.5.
535 DEFINE_TESTCASE(qpmemoryleak1, writable && !inmemory) {
536 // Inmemory never throws DatabaseModifiedError.
537 Xapian::WritableDatabase wdb(get_writable_database());
538 Xapian::Document doc;
540 doc.add_term("foo");
541 for (int i = 100; i < 120; ++i) {
542 doc.add_term(str(i));
545 for (int j = 0; j < 50; ++j) {
546 wdb.add_document(doc);
548 wdb.commit();
550 Xapian::Database database(get_writable_database_as_database());
551 Xapian::QueryParser queryparser;
552 queryparser.set_database(database);
553 TEST_EXCEPTION(Xapian::DatabaseModifiedError,
554 for (int k = 0; k < 1000; ++k) {
555 wdb.add_document(doc);
556 wdb.commit();
557 (void)queryparser.parse_query("1", queryparser.FLAG_PARTIAL);
559 SKIP_TEST("didn't manage to trigger DatabaseModifiedError");
562 return true;
565 static void
566 make_msize1_db(Xapian::WritableDatabase &db, const string &)
568 const char * value0 =
569 "ABBCDEFGHIJKLMMNOPQQRSTTUUVVWXYZZaabcdefghhijjkllmnopqrsttuvwxyz";
570 const char * value1 =
571 "EMLEMMMMMMMNMMLMELEDNLEDMLMLDMLMLMLMEDGFHPOPBAHJIQJNGRKCGF";
572 while (*value0) {
573 Xapian::Document doc;
574 doc.add_value(0, string(1, *value0++));
575 if (*value1) {
576 doc.add_value(1, string(1, *value1++));
577 doc.add_term("K1");
579 db.add_document(doc);
583 /// Regression test for ticket#464, fixed in 1.1.6 and 1.0.20.
584 DEFINE_TESTCASE(msize1, generated) {
585 Xapian::Database db = get_database("msize1", make_msize1_db);
586 Xapian::Enquire enq(db);
587 enq.set_sort_by_value(1, false);
588 enq.set_collapse_key(0);
589 enq.set_query(Xapian::Query("K1"));
591 Xapian::MSet mset = enq.get_mset(0, 60);
592 Xapian::doccount lb = mset.get_matches_lower_bound();
593 Xapian::doccount ub = mset.get_matches_upper_bound();
594 Xapian::doccount est = mset.get_matches_estimated();
595 TEST_EQUAL(lb, ub);
596 TEST_EQUAL(lb, est);
598 Xapian::MSet mset2 = enq.get_mset(50, 10, 1000);
599 Xapian::doccount lb2 = mset2.get_matches_lower_bound();
600 Xapian::doccount ub2 = mset2.get_matches_upper_bound();
601 Xapian::doccount est2 = mset2.get_matches_estimated();
602 TEST_EQUAL(lb2, ub2);
603 TEST_EQUAL(lb2, est2);
604 TEST_EQUAL(est, est2);
606 Xapian::MSet mset3 = enq.get_mset(0, 10, 1000);
607 Xapian::doccount lb3 = mset3.get_matches_lower_bound();
608 Xapian::doccount ub3 = mset3.get_matches_upper_bound();
609 Xapian::doccount est3 = mset3.get_matches_estimated();
610 TEST_EQUAL(lb3, ub3);
611 TEST_EQUAL(lb3, est3);
612 TEST_EQUAL(est, est3);
614 return true;
617 static void
618 make_msize2_db(Xapian::WritableDatabase &db, const string &)
620 const char * value0 = "AAABCDEEFGHIIJJKLLMNNOOPPQQRSTTUVWXYZ";
621 const char * value1 = "MLEMNMLMLMEDEDEMLEMLMLMLPOAHGF";
622 while (*value0) {
623 Xapian::Document doc;
624 doc.add_value(0, string(1, *value0++));
625 if (*value1) {
626 doc.add_value(1, string(1, *value1++));
627 doc.add_term("K1");
629 db.add_document(doc);
633 /// Regression test for bug related to ticket#464, fixed in 1.1.6 and 1.0.20.
634 DEFINE_TESTCASE(msize2, generated) {
635 Xapian::Database db = get_database("msize2", make_msize2_db);
636 Xapian::Enquire enq(db);
637 enq.set_sort_by_value(1, false);
638 enq.set_collapse_key(0);
639 enq.set_query(Xapian::Query("K1"));
641 Xapian::MSet mset = enq.get_mset(0, 60);
642 Xapian::doccount lb = mset.get_matches_lower_bound();
643 Xapian::doccount ub = mset.get_matches_upper_bound();
644 Xapian::doccount est = mset.get_matches_estimated();
645 TEST_EQUAL(lb, ub);
646 TEST_EQUAL(lb, est);
648 Xapian::MSet mset2 = enq.get_mset(50, 10, 1000);
649 Xapian::doccount lb2 = mset2.get_matches_lower_bound();
650 Xapian::doccount ub2 = mset2.get_matches_upper_bound();
651 Xapian::doccount est2 = mset2.get_matches_estimated();
652 TEST_EQUAL(lb2, ub2);
653 TEST_EQUAL(lb2, est2);
654 TEST_EQUAL(est, est2);
656 Xapian::MSet mset3 = enq.get_mset(0, 10, 1000);
657 Xapian::doccount lb3 = mset3.get_matches_lower_bound();
658 Xapian::doccount ub3 = mset3.get_matches_upper_bound();
659 Xapian::doccount est3 = mset3.get_matches_estimated();
660 TEST_EQUAL(lb3, ub3);
661 TEST_EQUAL(lb3, est3);
662 TEST_EQUAL(est, est3);
664 return true;
667 static void
668 make_xordecay1_db(Xapian::WritableDatabase &db, const string &)
670 for (int n = 1; n != 50; ++n) {
671 Xapian::Document doc;
672 for (int i = 1; i != 50; ++i) {
673 if (n % i == 0)
674 doc.add_term("N" + str(i));
676 db.add_document(doc);
680 /// Regression test for bug in decay of XOR, fixed in 1.2.1 and 1.0.21.
681 DEFINE_TESTCASE(xordecay1, generated) {
682 Xapian::Database db = get_database("xordecay1", make_xordecay1_db);
683 Xapian::Enquire enq(db);
684 enq.set_query(Xapian::Query(Xapian::Query::OP_XOR,
685 Xapian::Query("N10"),
686 Xapian::Query(Xapian::Query::OP_OR,
687 Xapian::Query("N2"),
688 Xapian::Query("N3"))));
689 Xapian::MSet mset1 = enq.get_mset(0, 1);
690 Xapian::MSet msetall = enq.get_mset(0, db.get_doccount());
692 TEST(mset_range_is_same(mset1, 0, msetall, 0, mset1.size()));
693 return true;
696 static void
697 make_ordecay_db(Xapian::WritableDatabase &db, const string &)
699 const char * p = "VJ=QC]LUNTaARLI;715RR^];A4O=P4ZG<2CS4EM^^VS[A6QENR";
700 for (int d = 0; p[d]; ++d) {
701 int l = int(p[d] - '0');
702 Xapian::Document doc;
703 for (int n = 1; n < l; ++n) {
704 doc.add_term("N" + str(n));
705 if (n % (d + 1) == 0) {
706 doc.add_term("M" + str(n));
709 db.add_document(doc);
713 /// Regression test for bug in decay of OR to AND, fixed in 1.2.1 and 1.0.21.
714 DEFINE_TESTCASE(ordecay1, generated) {
715 Xapian::Database db = get_database("ordecay", make_ordecay_db);
716 Xapian::Enquire enq(db);
717 enq.set_query(Xapian::Query(Xapian::Query::OP_OR,
718 Xapian::Query("N20"),
719 Xapian::Query("N21")));
721 Xapian::MSet msetall = enq.get_mset(0, db.get_doccount());
722 for (unsigned int i = 1; i < msetall.size(); ++i) {
723 Xapian::MSet submset = enq.get_mset(0, i);
724 TEST(mset_range_is_same(submset, 0, msetall, 0, submset.size()));
726 return true;
729 /** Regression test for bug in decay of OR to AND_MAYBE, fixed in 1.2.1 and
730 * 1.0.21.
732 DEFINE_TESTCASE(ordecay2, generated) {
733 Xapian::Database db = get_database("ordecay", make_ordecay_db);
734 Xapian::Enquire enq(db);
735 std::vector<Xapian::Query> q;
736 q.push_back(Xapian::Query("M20"));
737 q.push_back(Xapian::Query("N21"));
738 q.push_back(Xapian::Query("N22"));
739 enq.set_query(Xapian::Query(Xapian::Query::OP_OR,
740 Xapian::Query("N25"),
741 Xapian::Query(Xapian::Query::OP_AND,
742 q.begin(),
743 q.end())));
745 Xapian::MSet msetall = enq.get_mset(0, db.get_doccount());
746 for (unsigned int i = 1; i < msetall.size(); ++i) {
747 Xapian::MSet submset = enq.get_mset(0, i);
748 TEST(mset_range_is_same(submset, 0, msetall, 0, submset.size()));
750 return true;
753 static void
754 make_orcheck_db(Xapian::WritableDatabase &db, const string &)
756 static const unsigned t1[] = {2, 4, 6, 8, 10};
757 static const unsigned t2[] = {6, 7, 8, 11, 12, 13, 14, 15, 16, 17};
758 static const unsigned t3[] = {3, 7, 8, 11, 12, 13, 14, 15, 16, 17};
760 for (unsigned i = 1; i <= 17; ++i) {
761 Xapian::Document doc;
762 db.replace_document(i, doc);
764 for (unsigned i : t1) {
765 Xapian::Document doc(db.get_document(i));
766 doc.add_term("T1");
767 db.replace_document(i, doc);
769 for (unsigned i : t2) {
770 Xapian::Document doc(db.get_document(i));
771 doc.add_term("T2");
772 if (i < 17) {
773 doc.add_term("T2_lowfreq");
775 doc.add_value(2, "1");
776 db.replace_document(i, doc);
778 for (unsigned i : t3) {
779 Xapian::Document doc(db.get_document(i));
780 doc.add_term("T3");
781 if (i < 17) {
782 doc.add_term("T3_lowfreq");
784 doc.add_value(3, "1");
785 db.replace_document(i, doc);
789 /** Regression test for bugs in the check() method of OrPostList. (ticket #485)
790 * Bugs introduced and fixed between 1.2.0 and 1.2.1 (never in a release).
792 DEFINE_TESTCASE(orcheck1, generated) {
793 Xapian::Database db = get_database("orcheck1", make_orcheck_db);
794 Xapian::Enquire enq(db);
795 Xapian::Query q1("T1");
796 Xapian::Query q2("T2");
797 Xapian::Query q2l("T2_lowfreq");
798 Xapian::Query q3("T3");
799 Xapian::Query q3l("T3_lowfreq");
800 Xapian::Query v2(Xapian::Query::OP_VALUE_RANGE, 2, "0", "2");
801 Xapian::Query v3(Xapian::Query::OP_VALUE_RANGE, 3, "0", "2");
803 tout << "Checking q2 OR q3\n";
804 enq.set_query(Xapian::Query(Xapian::Query::OP_AND, q1,
805 Xapian::Query(Xapian::Query::OP_OR, q2, q3)));
806 mset_expect_order(enq.get_mset(0, db.get_doccount()), 8, 6);
808 tout << "Checking q2l OR q3\n";
809 enq.set_query(Xapian::Query(Xapian::Query::OP_AND, q1,
810 Xapian::Query(Xapian::Query::OP_OR, q2l, q3)));
811 mset_expect_order(enq.get_mset(0, db.get_doccount()), 8, 6);
813 tout << "Checking q2 OR q3l\n";
814 enq.set_query(Xapian::Query(Xapian::Query::OP_AND, q1,
815 Xapian::Query(Xapian::Query::OP_OR, q2, q3l)));
816 mset_expect_order(enq.get_mset(0, db.get_doccount()), 8, 6);
818 tout << "Checking v2 OR q3\n";
819 enq.set_query(Xapian::Query(Xapian::Query::OP_AND, q1,
820 Xapian::Query(Xapian::Query::OP_OR, v2, q3)));
821 mset_expect_order(enq.get_mset(0, db.get_doccount()), 8, 6);
823 tout << "Checking q2 OR v3\n";
824 enq.set_query(Xapian::Query(Xapian::Query::OP_AND, q1,
825 Xapian::Query(Xapian::Query::OP_OR, q2, v3)));
826 // Order of results in this one is different, because v3 gives no weight,
827 // both documents are in q2, and document 8 has a higher length.
828 mset_expect_order(enq.get_mset(0, db.get_doccount()), 6, 8);
830 return true;
833 /** Regression test for bug fixed in 1.2.1 and 1.0.21.
835 * We failed to mark the Btree as unmodified after cancel().
837 DEFINE_TESTCASE(failedreplace1, chert || glass) {
838 Xapian::WritableDatabase db(get_writable_database());
839 Xapian::Document doc;
840 doc.add_term("foo");
841 db.add_document(doc);
842 Xapian::docid did = db.add_document(doc);
843 doc.add_term("abc");
844 doc.add_term(string(1000, 'm'));
845 doc.add_term("xyz");
846 TEST_EXCEPTION(Xapian::InvalidArgumentError, db.replace_document(did, doc));
847 db.commit();
848 TEST_EQUAL(db.get_doccount(), 0);
849 TEST_EQUAL(db.get_termfreq("foo"), 0);
850 return true;
853 DEFINE_TESTCASE(failedreplace2, chert || glass) {
854 Xapian::WritableDatabase db(get_writable_database("apitest_simpledata"));
855 db.commit();
856 Xapian::doccount db_size = db.get_doccount();
857 Xapian::Document doc;
858 doc.set_data("wibble");
859 doc.add_term("foo");
860 doc.add_value(0, "seven");
861 db.add_document(doc);
862 Xapian::docid did = db.add_document(doc);
863 doc.add_term("abc");
864 doc.add_term(string(1000, 'm'));
865 doc.add_term("xyz");
866 doc.add_value(0, "six");
867 TEST_EXCEPTION(Xapian::InvalidArgumentError, db.replace_document(did, doc));
868 db.commit();
869 TEST_EQUAL(db.get_doccount(), db_size);
870 TEST_EQUAL(db.get_termfreq("foo"), 0);
871 return true;
874 /// Coverage for SelectPostList::skip_to().
875 DEFINE_TESTCASE(phrase3, positional) {
876 Xapian::Database db = get_database("apitest_phrase");
878 static const char * const phrase_words[] = { "phrase", "near" };
879 Xapian::Query q(Xapian::Query::OP_NEAR, phrase_words, phrase_words + 2, 12);
880 q = Xapian::Query(Xapian::Query::OP_AND_MAYBE, Xapian::Query("pad"), q);
882 Xapian::Enquire enquire(db);
883 enquire.set_query(q);
884 Xapian::MSet mset = enquire.get_mset(0, 5);
886 return true;
889 /// Check that get_mset(<large number>, 10) doesn't exhaust memory needlessly.
890 // Regression test for fix in 1.2.4.
891 DEFINE_TESTCASE(msetfirst2, backend) {
892 Xapian::Database db(get_database("apitest_simpledata"));
893 Xapian::Enquire enquire(db);
894 enquire.set_query(Xapian::Query("paragraph"));
895 Xapian::MSet mset;
896 // Before the fix, this tried to allocate too much memory.
897 mset = enquire.get_mset(0xfffffff0, 1);
898 TEST_EQUAL(mset.get_firstitem(), 0xfffffff0);
899 // Check that the number of documents gets clamped too.
900 mset = enquire.get_mset(1, 0xfffffff0);
901 TEST_EQUAL(mset.get_firstitem(), 1);
902 // Another regression test - MatchNothing used to give an MSet with
903 // get_firstitem() returning 0.
904 enquire.set_query(Xapian::Query::MatchNothing);
905 mset = enquire.get_mset(1, 1);
906 TEST_EQUAL(mset.get_firstitem(), 1);
907 return true;
910 DEFINE_TESTCASE(bm25weight2, backend) {
911 Xapian::Database db(get_database("etext"));
912 Xapian::Enquire enquire(db);
913 enquire.set_query(Xapian::Query("the"));
914 enquire.set_weighting_scheme(Xapian::BM25Weight(0, 0, 0, 0, 1));
915 Xapian::MSet mset = enquire.get_mset(0, 100);
916 TEST_REL(mset.size(),>=,2);
917 double weight0 = mset[0].get_weight();
918 for (size_t i = 1; i != mset.size(); ++i) {
919 TEST_EQUAL(weight0, mset[i].get_weight());
921 return true;
924 DEFINE_TESTCASE(unigramlmweight2, backend) {
925 Xapian::Database db(get_database("etext"));
926 Xapian::Enquire enquire(db);
927 enquire.set_query(Xapian::Query("the"));
928 enquire.set_weighting_scheme(Xapian::LMWeight());
929 Xapian::MSet mset = enquire.get_mset(0, 100);
930 TEST_REL(mset.size(),>=,2);
931 return true;
934 DEFINE_TESTCASE(tradweight2, backend) {
935 Xapian::Database db(get_database("etext"));
936 Xapian::Enquire enquire(db);
937 enquire.set_query(Xapian::Query("the"));
938 enquire.set_weighting_scheme(Xapian::TradWeight(0));
939 Xapian::MSet mset = enquire.get_mset(0, 100);
940 TEST_REL(mset.size(),>=,2);
941 double weight0 = mset[0].get_weight();
942 for (size_t i = 1; i != mset.size(); ++i) {
943 TEST_EQUAL(weight0, mset[i].get_weight());
945 return true;
948 // Regression test for bug fix in 1.2.9.
949 DEFINE_TESTCASE(emptydb1, backend) {
950 Xapian::Database db(get_database(string()));
951 static const Xapian::Query::op ops[] = {
952 Xapian::Query::OP_AND,
953 Xapian::Query::OP_OR,
954 Xapian::Query::OP_AND_NOT,
955 Xapian::Query::OP_XOR,
956 Xapian::Query::OP_AND_MAYBE,
957 Xapian::Query::OP_FILTER,
958 Xapian::Query::OP_NEAR,
959 Xapian::Query::OP_PHRASE,
960 Xapian::Query::OP_ELITE_SET
962 const Xapian::Query::op * p;
963 for (p = ops; p - ops != sizeof(ops) / sizeof(*ops); ++p) {
964 tout << *p << endl;
965 Xapian::Enquire enquire(db);
966 Xapian::Query query(*p, Xapian::Query("a"), Xapian::Query("b"));
967 enquire.set_query(query);
968 Xapian::MSet mset = enquire.get_mset(0, 10);
969 TEST_EQUAL(mset.get_matches_estimated(), 0);
970 TEST_EQUAL(mset.get_matches_upper_bound(), 0);
971 TEST_EQUAL(mset.get_matches_lower_bound(), 0);
973 return true;
976 /// Test error opening non-existent stub databases.
977 // Regression test for bug fixed in 1.3.1 and 1.2.11.
978 DEFINE_TESTCASE(stubdb7, !backend) {
979 TEST_EXCEPTION(Xapian::DatabaseOpeningError,
980 Xapian::Database("nosuchdirectory", Xapian::DB_BACKEND_STUB));
981 TEST_EXCEPTION(Xapian::DatabaseOpeningError,
982 Xapian::WritableDatabase("nosuchdirectory",
983 Xapian::DB_OPEN|Xapian::DB_BACKEND_STUB));
984 return true;
987 /// Test which checks the weights are as expected.
988 // This runs for multi_* too, so serves to check that we get the same weights
989 // with multiple databases as without.
990 DEFINE_TESTCASE(msetweights1, backend) {
991 Xapian::Database db = get_database("apitest_simpledata");
992 Xapian::Enquire enq(db);
993 Xapian::Query q(Xapian::Query::OP_OR,
994 Xapian::Query("paragraph"),
995 Xapian::Query("word"));
996 enq.set_query(q);
997 // 5 documents match, and the 4th and 5th have the same weight, so ask for
998 // 4 as that's a good test that we get the right one in this case.
999 Xapian::MSet mset = enq.get_mset(0, 4);
1001 static const struct { Xapian::docid did; double wt; } expected[] = {
1002 { 2, 1.2058248004573934864 },
1003 { 4, 0.81127876655507624726 },
1004 { 1, 0.17309550762546158098 },
1005 { 3, 0.14609528172558261527 }
1008 TEST_EQUAL(mset.size(), sizeof(expected) / sizeof(expected[0]));
1009 for (size_t i = 0; i < mset.size(); ++i) {
1010 TEST_EQUAL(*mset[i], expected[i].did);
1011 TEST_EQUAL_DOUBLE(mset[i].get_weight(), expected[i].wt);
1014 // Now test a query which matches only even docids, so in the multi case
1015 // one subdatabase doesn't match.
1016 enq.set_query(Xapian::Query("one"));
1017 mset = enq.get_mset(0, 3);
1019 static const struct { Xapian::docid did; double wt; } expected2[] = {
1020 { 6, 0.73354729848273669823 },
1021 { 2, 0.45626501034348893038 }
1024 TEST_EQUAL(mset.size(), sizeof(expected2) / sizeof(expected2[0]));
1025 for (size_t i = 0; i < mset.size(); ++i) {
1026 TEST_EQUAL(*mset[i], expected2[i].did);
1027 TEST_EQUAL_DOUBLE(mset[i].get_weight(), expected2[i].wt);
1030 return true;
1033 DEFINE_TESTCASE(itorskiptofromend1, backend) {
1034 Xapian::Database db = get_database("apitest_simpledata");
1036 Xapian::TermIterator t = db.termlist_begin(1);
1037 t.skip_to("zzzzz");
1038 TEST(t == db.termlist_end(1));
1039 // This worked in 1.2.x but segfaulted in 1.3.1.
1040 t.skip_to("zzzzzz");
1042 Xapian::PostingIterator p = db.postlist_begin("one");
1043 p.skip_to(99999);
1044 TEST(p == db.postlist_end("one"));
1045 // This segfaulted prior to 1.3.2.
1046 p.skip_to(999999);
1048 Xapian::PositionIterator i = db.positionlist_begin(6, "one");
1049 i.skip_to(99999);
1050 TEST(i == db.positionlist_end(6, "one"));
1051 // This segfaulted prior to 1.3.2.
1052 i.skip_to(999999);
1054 Xapian::ValueIterator v = db.valuestream_begin(1);
1055 v.skip_to(99999);
1056 TEST(v == db.valuestream_end(1));
1057 // These segfaulted prior to 1.3.2.
1058 v.skip_to(999999);
1059 v.check(9999999);
1061 return true;
1064 /// Check handling of invalid block sizes.
1065 // Regression test for bug fixed in 1.2.17 and 1.3.2 - the size gets fixed
1066 // but the uncorrected size was passed to the base file. Also, abort() was
1067 // called on 0.
1068 DEFINE_TESTCASE(blocksize1, chert || glass) {
1069 string db_dir = "." + get_dbtype();
1070 mkdir(db_dir.c_str(), 0755);
1071 db_dir += "/db__blocksize1";
1072 int flags;
1073 if (get_dbtype() == "chert") {
1074 flags = Xapian::DB_CREATE|Xapian::DB_BACKEND_CHERT;
1075 } else {
1076 flags = Xapian::DB_CREATE|Xapian::DB_BACKEND_GLASS;
1078 static const unsigned bad_sizes[] = {
1079 65537, 8000, 2000, 1024, 16, 7, 3, 1, 0
1081 for (size_t i = 0; i < sizeof(bad_sizes) / sizeof(bad_sizes[0]); ++i) {
1082 size_t block_size = bad_sizes[i];
1083 rm_rf(db_dir);
1084 Xapian::WritableDatabase db(db_dir, flags, block_size);
1085 Xapian::Document doc;
1086 doc.add_term("XYZ");
1087 doc.set_data("foo");
1088 db.add_document(doc);
1089 db.commit();
1091 return true;
1094 /// Feature test for Xapian::DB_NO_TERMLIST.
1095 DEFINE_TESTCASE(notermlist1, glass) {
1096 string db_dir = "." + get_dbtype();
1097 mkdir(db_dir.c_str(), 0755);
1098 db_dir += "/db__notermlist1";
1099 int flags = Xapian::DB_CREATE|Xapian::DB_NO_TERMLIST;
1100 if (get_dbtype() == "chert") {
1101 flags |= Xapian::DB_BACKEND_CHERT;
1102 } else {
1103 flags |= Xapian::DB_BACKEND_GLASS;
1105 rm_rf(db_dir);
1106 Xapian::WritableDatabase db(db_dir, flags);
1107 Xapian::Document doc;
1108 doc.add_term("hello");
1109 doc.add_value(42, "answer");
1110 db.add_document(doc);
1111 db.commit();
1112 TEST(!file_exists(db_dir + "/termlist.glass"));
1113 TEST_EXCEPTION(Xapian::FeatureUnavailableError, db.termlist_begin(1));
1114 return true;
1117 /// Regression test for bug starting a new glass freelist block.
1118 DEFINE_TESTCASE(newfreelistblock1, writable) {
1119 Xapian::Document doc;
1120 doc.add_term("foo");
1121 for (int i = 100; i < 120; ++i) {
1122 doc.add_term(str(i));
1125 Xapian::WritableDatabase wdb(get_writable_database());
1126 for (int j = 0; j < 50; ++j) {
1127 wdb.add_document(doc);
1129 wdb.commit();
1131 for (int k = 0; k < 1000; ++k) {
1132 wdb.add_document(doc);
1133 wdb.commit();
1136 return true;
1139 /** Check that the parent directory for the database doesn't need to be
1140 * writable. Regression test for early versions on the glass new btree
1141 * branch which failed to append a "/" when generating a temporary filename
1142 * from the database directory.
1144 DEFINE_TESTCASE(readonlyparentdir1, chert || glass) {
1145 #if !defined __WIN32__ && !defined __CYGWIN__ && !defined __OS2__
1146 string path = get_named_writable_database_path("readonlyparentdir1");
1147 // Fix permissions if the previous test was killed.
1148 (void)chmod(path.c_str(), 0700);
1149 mkdir(path.c_str(), 0777);
1150 mkdir((path + "/sub").c_str(), 0777);
1151 Xapian::WritableDatabase db = get_named_writable_database("readonlyparentdir1/sub");
1152 TEST(chmod(path.c_str(), 0500) == 0);
1153 try {
1154 Xapian::Document doc;
1155 doc.add_term("hello");
1156 doc.set_data("some text");
1157 db.add_document(doc);
1158 db.commit();
1159 } catch (...) {
1160 // Attempt to fix the permissions, otherwise things like "rm -rf" on
1161 // the source tree will fail.
1162 (void)chmod(path.c_str(), 0700);
1163 throw;
1165 TEST(chmod(path.c_str(), 0700) == 0);
1166 #endif
1167 return true;
1170 static void
1171 make_phrasebug1_db(Xapian::WritableDatabase &db, const string &)
1173 Xapian::Document doc;
1174 doc.add_posting("hurricane", 199881);
1175 doc.add_posting("hurricane", 203084);
1176 doc.add_posting("katrina", 199882);
1177 doc.add_posting("katrina", 202473);
1178 doc.add_posting("katrina", 203085);
1179 db.add_document(doc);
1182 /// Regression test for ticket#653, fixed in 1.3.2 and 1.2.19.
1183 DEFINE_TESTCASE(phrasebug1, generated && positional) {
1184 Xapian::Database db = get_database("phrasebug1", make_phrasebug1_db);
1185 static const char * const qterms[] = { "katrina", "hurricane" };
1186 Xapian::Enquire e(db);
1187 Xapian::Query q(Xapian::Query::OP_PHRASE, qterms, qterms + 2, 5);
1188 e.set_query(q);
1189 Xapian::MSet mset = e.get_mset(0, 100);
1190 TEST_EQUAL(mset.size(), 0);
1191 static const char * const qterms2[] = { "hurricane", "katrina" };
1192 Xapian::Query q2(Xapian::Query::OP_PHRASE, qterms2, qterms2 + 2, 5);
1193 e.set_query(q2);
1194 mset = e.get_mset(0, 100);
1195 TEST_EQUAL(mset.size(), 1);
1196 return true;
1199 /// Feature test for Xapian::DB_RETRY_LOCK
1200 DEFINE_TESTCASE(retrylock1, writable && !inmemory && !remote) {
1201 // FIXME: Can't see an easy way to test this for remote databases - the
1202 // harness doesn't seem to provide a suitable way to reopen a remote.
1203 #if defined HAVE_FORK && defined HAVE_SOCKETPAIR
1204 int fds[2];
1205 if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, PF_UNSPEC, fds) < 0) {
1206 FAIL_TEST("socketpair() failed");
1208 if (fcntl(fds[1], F_SETFL, O_NONBLOCK) < 0)
1209 FAIL_TEST("fcntl() failed to set O_NONBLOCK");
1210 pid_t child = fork();
1211 if (child == -1)
1212 FAIL_TEST("fork() failed");
1213 if (child == 0) {
1214 // Wait for signal that parent has opened the database.
1215 char ch;
1216 while (read(fds[0], &ch, 1) < 0) { }
1218 try {
1219 Xapian::WritableDatabase db2(get_named_writable_database_path("retrylock1"),
1220 Xapian::DB_OPEN|Xapian::DB_RETRY_LOCK);
1221 if (write(fds[0], "y", 1)) { }
1222 } catch (const Xapian::DatabaseLockError &) {
1223 if (write(fds[0], "l", 1)) { }
1224 } catch (const Xapian::Error &e) {
1225 const string & m = e.get_description();
1226 if (write(fds[0], m.data(), m.size())) { }
1227 } catch (...) {
1228 if (write(fds[0], "o", 1)) { }
1230 _exit(0);
1233 close(fds[0]);
1235 Xapian::WritableDatabase db = get_named_writable_database("retrylock1");
1236 if (write(fds[1], "", 1) != 1)
1237 FAIL_TEST("Failed to signal to child process");
1239 char result[256];
1240 int r = read(fds[1], result, sizeof(result));
1241 if (r == -1) {
1242 if (errno == EAGAIN) {
1243 // Good.
1244 result[0] = 'y';
1245 } else {
1246 // Error.
1247 tout << "errno=" << errno << ": " << strerror(errno) << endl;
1248 result[0] = 'e';
1250 r = 1;
1251 } else if (r >= 1) {
1252 if (result[0] == 'y') {
1253 // Child process managed to also get write lock!
1254 result[0] = '!';
1256 } else {
1257 // EOF.
1258 result[0] = 'z';
1259 r = 1;
1262 try {
1263 db.close();
1264 } catch (...) {
1265 kill(child, SIGKILL);
1266 int status;
1267 while (waitpid(child, &status, 0) < 0) {
1268 if (errno != EINTR) break;
1270 throw;
1273 if (result[0] == 'y') {
1274 retry:
1275 struct timeval tv;
1276 tv.tv_sec = 3;
1277 tv.tv_usec = 0;
1278 fd_set fr, fe;
1279 FD_ZERO(&fr);
1280 FD_SET(fds[1], &fr);
1281 FD_ZERO(&fe);
1282 FD_SET(fds[1], &fe);
1283 int sr = select(fds[1] + 1, &fr, NULL, &fe, &tv);
1284 if (sr == 0) {
1285 // Timed out.
1286 result[0] = 'T';
1287 r = 1;
1288 } else if (sr == -1) {
1289 if (errno == EINTR || errno == EAGAIN)
1290 goto retry;
1291 tout << "select() failed with errno=" << errno << ": " << strerror(errno) << endl;
1292 result[0] = 'S';
1293 r = 1;
1294 } else {
1295 r = read(fds[1], result, sizeof(result));
1296 if (r == -1) {
1297 // Error.
1298 tout << "read failed with errno=" << errno << ": " << strerror(errno) << endl;
1299 result[0] = 'R';
1300 r = 1;
1301 } else if (r == 0) {
1302 // EOF.
1303 result[0] = 'Z';
1304 r = 1;
1309 close(fds[1]);
1311 kill(child, SIGKILL);
1312 int status;
1313 while (waitpid(child, &status, 0) < 0) {
1314 if (errno != EINTR) break;
1317 tout << string(result, r) << endl;
1318 TEST_EQUAL(result[0], 'y');
1319 #endif
1321 return true;
1324 // Opening a WritableDatabase with low fds available - it should avoid them.
1325 DEFINE_TESTCASE(dbfilefd012, chert || glass) {
1326 #if !defined __WIN32__ && !defined __CYGWIN__ && !defined __OS2__
1327 int oldfds[3];
1328 for (int i = 0; i < 3; ++i) {
1329 oldfds[i] = dup(i);
1331 try {
1332 for (int j = 0; j < 3; ++j) {
1333 close(j);
1334 TEST_REL(lseek(j, 0, SEEK_CUR), <, 0);
1335 TEST_EQUAL(errno, EBADF);
1338 Xapian::WritableDatabase db = get_writable_database();
1340 // Check we didn't use any of those low fds for tables, as that risks
1341 // data corruption if some other code in the same process tries to
1342 // write to them (see #651).
1343 for (int fd = 0; fd < 3; ++fd) {
1344 // Check that the fd is still closed, or isn't open O_RDWR (the
1345 // lock file gets opened O_WRONLY), or it's a pipe (if we're using
1346 // a child process to hold a non-OFD fcntl lock).
1347 int flags = fcntl(fd, F_GETFL);
1348 if (flags == -1) {
1349 TEST_EQUAL(errno, EBADF);
1350 } else if ((flags & O_ACCMODE) != O_RDWR) {
1351 // OK.
1352 } else {
1353 struct stat sb;
1354 TEST_NOT_EQUAL(fstat(fd, &sb), -1);
1355 #ifdef S_ISSOCK
1356 TEST(S_ISSOCK(sb.st_mode));
1357 #else
1358 // If we can't check it is a socket, at least check it is not a
1359 // regular file.
1360 TEST(!S_ISREG(sb.st_mode));
1361 #endif
1364 } catch (...) {
1365 for (int j = 0; j < 3; ++j) {
1366 dup2(oldfds[j], j);
1367 close(oldfds[j]);
1369 throw;
1372 for (int j = 0; j < 3; ++j) {
1373 dup2(oldfds[j], j);
1374 close(oldfds[j]);
1376 #endif
1378 return true;
1381 /// Regression test for #675, fixed in 1.3.3 and 1.2.21.
1382 DEFINE_TESTCASE(cursorbug1, chert || glass) {
1383 Xapian::WritableDatabase wdb = get_writable_database();
1384 Xapian::Database db = get_writable_database_as_database();
1385 Xapian::Enquire enq(db);
1386 enq.set_query(Xapian::Query::MatchAll);
1387 Xapian::MSet mset;
1388 // The original problem triggers for chert and glass on repeat==7.
1389 for (int repeat = 0; repeat < 10; ++repeat) {
1390 tout.str(string());
1391 tout << "iteration #" << repeat << endl;
1393 const int ITEMS = 10;
1394 int free_id = db.get_doccount();
1395 int offset = max(free_id, ITEMS * 2) - (ITEMS * 2);
1396 int limit = offset + (ITEMS * 2);
1398 mset = enq.get_mset(offset, limit);
1399 for (Xapian::MSetIterator m1 = mset.begin(); m1 != mset.end(); ++m1) {
1400 (void)m1.get_document().get_value(0);
1403 for (int i = free_id; i <= free_id + ITEMS; ++i) {
1404 Xapian::Document doc;
1405 const string & id = str(i);
1406 string qterm = "Q" + id;
1407 doc.add_value(0, id);
1408 doc.add_boolean_term(qterm);
1409 wdb.replace_document(qterm, doc);
1411 wdb.commit();
1413 db.reopen();
1414 mset = enq.get_mset(offset, limit);
1415 for (Xapian::MSetIterator m2 = mset.begin(); m2 != mset.end(); ++m2) {
1416 (void)m2.get_document().get_value(0);
1420 return true;
1423 // Regression test for #674, fixed in 1.2.21 and 1.3.3.
1424 DEFINE_TESTCASE(sortvalue2, backend) {
1425 Xapian::Database db = get_database("apitest_simpledata");
1426 db.add_database(get_database("apitest_simpledata2"));
1427 Xapian::Enquire enq(db);
1428 enq.set_query(Xapian::Query::MatchAll);
1429 enq.set_sort_by_value(0, false);
1430 Xapian::MSet mset = enq.get_mset(0, 50);
1432 // Check all results are in key order - the bug was that they were sorted
1433 // by docid instead with multiple remote databases.
1434 string old_key;
1435 for (Xapian::MSetIterator i = mset.begin(); i != mset.end(); ++i) {
1436 string key = db.get_document(*i).get_value(0);
1437 TEST(old_key <= key);
1438 swap(old_key, key);
1440 return true;
1443 /// Check behaviour of Enquire::get_query().
1444 DEFINE_TESTCASE(enquiregetquery1, backend) {
1445 Xapian::Database db = get_database("apitest_simpledata");
1446 Xapian::Enquire enq(db);
1447 TEST_EQUAL(enq.get_query().get_description(), "Query()");
1448 return true;
1451 DEFINE_TESTCASE(embedded1, singlefile) {
1452 // In reality you should align the embedded database to a multiple of
1453 // database block size, but any offset is meant to work.
1454 off_t offset = 1234;
1456 Xapian::Database db = get_database("apitest_simpledata");
1457 const string & db_path = get_database_path("apitest_simpledata");
1458 const string & tmp_path = db_path + "-embedded";
1459 ofstream out(tmp_path, fstream::trunc|fstream::binary);
1460 out.seekp(offset);
1461 out << ifstream(db_path, fstream::binary).rdbuf();
1462 out.close();
1465 int fd = open(tmp_path.c_str(), O_RDONLY|O_BINARY);
1466 lseek(fd, offset, SEEK_SET);
1467 Xapian::Database db_embedded(fd);
1468 TEST_EQUAL(db.get_doccount(), db_embedded.get_doccount());
1472 int fd = open(tmp_path.c_str(), O_RDONLY|O_BINARY);
1473 lseek(fd, offset, SEEK_SET);
1474 size_t check_errors =
1475 Xapian::Database::check(fd, Xapian::DBCHECK_SHOW_STATS, &tout);
1476 TEST_EQUAL(check_errors, 0);
1479 return true;
1482 /// Regression test for bug fixed in 1.3.7.
1483 DEFINE_TESTCASE(exactxor1, backend) {
1484 Xapian::Database db = get_database("apitest_simpledata");
1485 Xapian::Enquire enq(db);
1487 static const char * const words[4] = {
1488 "blank", "test", "paragraph", "banana"
1490 Xapian::Query q(Xapian::Query::OP_XOR, words, words + 4);
1491 enq.set_query(q);
1492 enq.set_weighting_scheme(Xapian::BoolWeight());
1493 Xapian::MSet mset = enq.get_mset(0, 0);
1494 // A reversed conditional gave us 5 in this case.
1495 TEST_EQUAL(mset.get_matches_upper_bound(), 6);
1496 // Test improved lower bound in 1.3.7 (earlier versions gave 0).
1497 TEST_EQUAL(mset.get_matches_lower_bound(), 2);
1499 static const char * const words2[4] = {
1500 "queri", "test", "paragraph", "word"
1502 Xapian::Query q2(Xapian::Query::OP_XOR, words2, words2 + 4);
1503 enq.set_query(q2);
1504 enq.set_weighting_scheme(Xapian::BoolWeight());
1505 mset = enq.get_mset(0, 0);
1506 // A reversed conditional gave us 6 in this case.
1507 TEST_EQUAL(mset.get_matches_upper_bound(), 5);
1508 // Test improved lower bound in 1.3.7 (earlier versions gave 0).
1509 TEST_EQUAL(mset.get_matches_lower_bound(), 1);
1511 return true;
1514 /// Feature test for Database::get_revision().
1515 DEFINE_TESTCASE(getrevision1, chert || glass) {
1516 Xapian::WritableDatabase db = get_writable_database();
1517 TEST_EQUAL(db.get_revision(), 0);
1518 db.commit();
1519 TEST_EQUAL(db.get_revision(), 0);
1520 Xapian::Document doc;
1521 doc.add_term("hello");
1522 db.add_document(doc);
1523 TEST_EQUAL(db.get_revision(), 0);
1524 db.commit();
1525 TEST_EQUAL(db.get_revision(), 1);
1526 db.commit();
1527 TEST_EQUAL(db.get_revision(), 1);
1528 db.add_document(doc);
1529 db.commit();
1530 TEST_EQUAL(db.get_revision(), 2);
1531 return true;
1534 /// Feature test for DOC_ASSUME_VALID.
1535 DEFINE_TESTCASE(getdocumentlazy1, backend) {
1536 Xapian::Database db = get_database("apitest_simpledata");
1537 Xapian::Document doc_lazy = db.get_document(2, Xapian::DOC_ASSUME_VALID);
1538 Xapian::Document doc = db.get_document(2);
1539 TEST_EQUAL(doc.get_data(), doc_lazy.get_data());
1540 TEST_EQUAL(doc.get_value(0), doc_lazy.get_value(0));
1541 return true;
1544 /// Feature test for DOC_ASSUME_VALID for a docid that doesn't actually exist.
1545 DEFINE_TESTCASE(getdocumentlazy2, backend) {
1546 Xapian::Database db = get_database("apitest_simpledata");
1547 Xapian::Document doc;
1548 try {
1549 doc = db.get_document(db.get_lastdocid() + 1, Xapian::DOC_ASSUME_VALID);
1550 } catch (const Xapian::DocNotFoundError&) {
1551 // DOC_ASSUME_VALID is really just a hint, so ignoring is OK (the
1552 // remote backend currently does).
1554 TEST(doc.get_data().empty());
1555 TEST_EXCEPTION(Xapian::DocNotFoundError,
1556 doc = db.get_document(db.get_lastdocid() + 1);
1558 return true;
1561 static void
1562 gen_uniqterms_gt_doclen_db(Xapian::WritableDatabase& db, const string&)
1564 Xapian::Document doc;
1565 doc.add_term("foo");
1566 doc.add_boolean_term("bar");
1567 db.add_document(doc);
1568 Xapian::Document doc2;
1569 doc2.add_posting("foo", 0, 2);
1570 doc2.add_term("foo2");
1571 doc2.add_boolean_term("baz");
1572 doc2.add_boolean_term("baz2");
1573 db.add_document(doc2);
1576 DEFINE_TESTCASE(getuniqueterms1, generated) {
1577 Xapian::Database db =
1578 get_database("uniqterms_gt_doclen", gen_uniqterms_gt_doclen_db);
1580 auto unique1 = db.get_unique_terms(1);
1581 TEST_REL(unique1, <=, db.get_doclength(1));
1582 TEST_REL(unique1, <, db.get_document(1).termlist_count());
1583 // Ideally it'd be equal to 1, and in this case it is, but the current
1584 // backends can't always efficiently ensure an exact answer.
1585 TEST_REL(unique1, >=, 1);
1587 auto unique2 = db.get_unique_terms(2);
1588 TEST_REL(unique2, <=, db.get_doclength(2));
1589 TEST_REL(unique2, <, db.get_document(2).termlist_count());
1590 // Ideally it'd be equal to 2, but the current backends can't always
1591 // efficiently ensure an exact answer and here it is actually 3.
1592 TEST_REL(unique2, >=, 2);
1594 return true;
1597 /** Regression test for bug fixed in 1.4.6.
1599 * OP_NEAR would think a term without positional information occurred at
1600 * position 1 if it had the lowest term frequency amongst the OP_NEAR's
1601 * subqueries.
1603 DEFINE_TESTCASE(nopositionbug1, generated) {
1604 Xapian::Database db =
1605 get_database("uniqterms_gt_doclen", gen_uniqterms_gt_doclen_db);
1607 // Test both orders.
1608 static const char* const terms1[] = { "foo", "baz" };
1609 static const char* const terms2[] = { "baz", "foo" };
1611 Xapian::Enquire enq(db);
1612 enq.set_query(Xapian::Query(Xapian::Query::OP_NEAR,
1613 begin(terms1), end(terms1), 10));
1614 TEST_EQUAL(enq.get_mset(0, 5).size(), 0);
1616 enq.set_query(Xapian::Query(Xapian::Query::OP_NEAR,
1617 begin(terms2), end(terms2), 10));
1618 TEST_EQUAL(enq.get_mset(0, 5).size(), 0);
1620 enq.set_query(Xapian::Query(Xapian::Query::OP_PHRASE,
1621 begin(terms1), end(terms1), 10));
1622 TEST_EQUAL(enq.get_mset(0, 5).size(), 0);
1624 enq.set_query(Xapian::Query(Xapian::Query::OP_PHRASE,
1625 begin(terms2), end(terms2), 10));
1626 TEST_EQUAL(enq.get_mset(0, 5).size(), 0);
1628 // Exercise exact phrase case too.
1629 enq.set_query(Xapian::Query(Xapian::Query::OP_PHRASE,
1630 begin(terms1), end(terms1), 2));
1631 TEST_EQUAL(enq.get_mset(0, 5).size(), 0);
1633 enq.set_query(Xapian::Query(Xapian::Query::OP_PHRASE,
1634 begin(terms2), end(terms2), 2));
1635 TEST_EQUAL(enq.get_mset(0, 5).size(), 0);
1637 return true;
1640 /// Check estimate is rounded to suitable number of S.F. - new in 1.4.3.
1641 DEFINE_TESTCASE(estimaterounding1, backend) {
1642 Xapian::Database db = get_database("etext");
1643 Xapian::Enquire enquire(db);
1644 enquire.set_query(Xapian::Query("the") | Xapian::Query("road"));
1645 Xapian::MSet mset = enquire.get_mset(0, 10);
1646 // MSet::get_description() includes bounds and raw estimate.
1647 tout << mset.get_description() << endl;
1648 // Bounds are 411-439, raw estimate is 419.
1649 TEST_EQUAL(mset.get_matches_estimated() % 10, 0);
1650 enquire.set_query(Xapian::Query("king") | Xapian::Query("prussia"));
1651 mset = enquire.get_mset(0, 10);
1652 tout << mset.get_description() << endl;
1653 // Bounds are 111-138, raw estimate is 133.
1654 TEST_EQUAL(mset.get_matches_estimated() % 10, 0);
1655 return true;
1658 /** Regression test for bug with get_mset(0, 0, N) (N > 0).
1660 * Fixed in 1.5.0 and 1.4.6.
1662 DEFINE_TESTCASE(checkatleast4, backend) {
1663 Xapian::Database db = get_database("apitest_simpledata");
1664 Xapian::Enquire enq(db);
1665 enq.set_query(Xapian::Query("paragraph"));
1666 // This used to cause access to an element in an empty vector.
1667 Xapian::MSet mset = enq.get_mset(0, 0, 4);
1668 TEST_EQUAL(mset.size(), 0);
1669 return true;
1672 /// Regression test for glass bug fixed in 1.4.6 and 1.5.0.
1673 DEFINE_TESTCASE(nodocs1, transactions && !remote) {
1675 Xapian::WritableDatabase db = get_named_writable_database("nodocs1");
1676 db.set_metadata("foo", "bar");
1677 db.commit();
1678 Xapian::Document doc;
1679 doc.add_term("baz");
1680 db.add_document(doc);
1681 db.commit();
1684 size_t check_errors =
1685 Xapian::Database::check(get_named_writable_database_path("nodocs1"),
1686 Xapian::DBCHECK_SHOW_STATS, &tout);
1687 TEST_EQUAL(check_errors, 0);
1689 return true;
1692 /// Regression test for split position handling - broken in 1.4.8.
1693 DEFINE_TESTCASE(splitpostings1, writable) {
1694 Xapian::WritableDatabase db = get_writable_database();
1695 Xapian::Document doc;
1696 // Add postings to create a split internally.
1697 for (Xapian::termpos pos = 0; pos <= 100; pos += 10) {
1698 doc.add_posting("foo", pos);
1700 for (Xapian::termpos pos = 5; pos <= 100; pos += 20) {
1701 doc.add_posting("foo", pos);
1703 db.add_document(doc);
1704 db.commit();
1706 Xapian::termpos expect = 0;
1707 Xapian::termpos pos = 0;
1708 for (auto p = db.positionlist_begin(1, "foo");
1709 p != db.positionlist_end(1, "foo"); ++p) {
1710 TEST_REL(expect, <=, 100);
1711 pos = *p;
1712 TEST_EQUAL(pos, expect);
1713 expect += 5;
1714 if (expect % 20 == 15) expect += 5;
1716 TEST_EQUAL(pos, 100);
1718 return true;