Fix java examples and related docs on how to run them
[xapian.git] / xapian-core / tests / api_backend.cc
blobeb2bf66484d075f7887f453994079587b0c5c960
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 #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>
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 db.commit();
88 TEST_EQUAL(db.get_avlength(), 2000000000);
89 for (int i = 0; i != 20; ++i) {
90 Xapian::Document doc3;
91 doc3.add_posting("count" + str(i), 1, 2000000000);
92 db.add_document(doc3);
94 TEST_EQUAL(db.get_avlength(), 2000000000);
95 db.commit();
96 TEST_EQUAL(db.get_avlength(), 2000000000);
97 if (get_dbtype() != "inmemory") {
98 // InMemory doesn't support get_writable_database_as_database().
99 Xapian::Database dbr = get_writable_database_as_database();
100 TEST_EQUAL(dbr.get_avlength(), 2000000000);
102 return true;
105 // Check that exceeding 32bit in combined database doesn't cause a problem
106 // when using 64bit docids.
107 DEFINE_TESTCASE(exceed32bitcombineddb1, writable) {
108 // Test case is for 64-bit Xapian::docid.
109 // FIXME: Though we should check that the overflow is handled gracefully
110 // for 32-bit...
111 if (sizeof(Xapian::docid) == 4) return true;
113 // The InMemory backend uses a vector for the documents, so trying to add
114 // a document with the maximum docid is likely to fail because we can't
115 // allocate enough memory!
116 SKIP_TEST_FOR_BACKEND("inmemory");
118 Xapian::WritableDatabase db1 = get_writable_database();
119 Xapian::WritableDatabase db2 = get_writable_database();
120 Xapian::Document doc;
121 doc.set_data("prose");
122 doc.add_term("word");
124 Xapian::docid max_id = 0xffffffff;
126 db1.replace_document(max_id, doc);
127 db2.replace_document(max_id, doc);
129 Xapian::Database db;
130 db.add_database(db1);
131 db.add_database(db2);
133 Xapian::Enquire enquire(db);
134 enquire.set_query(Xapian::Query::MatchAll);
135 Xapian::MSet mymset = enquire.get_mset(0, 10);
137 TEST_EQUAL(2, mymset.size());
139 for (Xapian::MSetIterator i = mymset.begin(); i != mymset.end(); ++i) {
140 TEST_EQUAL("prose", i.get_document().get_data());
143 return true;
146 DEFINE_TESTCASE(dbstats1, backend) {
147 Xapian::Database db = get_database("etext");
149 // Use precalculated values to avoid expending CPU cycles to calculate
150 // these every time without improving test coverage.
151 const Xapian::termcount min_len = 2;
152 const Xapian::termcount max_len = 532;
153 const Xapian::termcount max_wdf = 22;
155 if (get_dbtype() != "inmemory") {
156 // Should be exact as no deletions have happened.
157 TEST_EQUAL(db.get_doclength_upper_bound(), max_len);
158 TEST_EQUAL(db.get_doclength_lower_bound(), min_len);
159 } else {
160 // For inmemory, we usually give rather loose bounds.
161 TEST_REL(db.get_doclength_upper_bound(),>=,max_len);
162 TEST_REL(db.get_doclength_lower_bound(),<=,min_len);
165 if (get_dbtype() != "inmemory" && !startswith(get_dbtype(), "remote")) {
166 TEST_EQUAL(db.get_wdf_upper_bound("the"), max_wdf);
167 } else {
168 // For inmemory and remote backends, we usually give rather loose
169 // bounds (remote matches use tighter bounds, but querying the
170 // wdf bound gives a looser one).
171 TEST_REL(db.get_wdf_upper_bound("the"),>=,max_wdf);
174 // This failed with an assertion during development between 1.3.1 and
175 // 1.3.2.
176 TEST_EQUAL(db.get_wdf_upper_bound(""), 0);
178 return true;
181 // Check stats with a single document. In a multi-database situation, this
182 // gave 0 for get-_doclength_lower_bound() in 1.3.2.
183 DEFINE_TESTCASE(dbstats2, backend) {
184 Xapian::Database db = get_database("apitest_onedoc");
186 // Use precalculated values to avoid expending CPU cycles to calculate
187 // these every time without improving test coverage.
188 const Xapian::termcount min_len = 15;
189 const Xapian::termcount max_len = 15;
190 const Xapian::termcount max_wdf = 7;
192 if (get_dbtype() != "inmemory") {
193 // Should be exact as no deletions have happened.
194 TEST_EQUAL(db.get_doclength_upper_bound(), max_len);
195 TEST_EQUAL(db.get_doclength_lower_bound(), min_len);
196 } else {
197 // For inmemory, we usually give rather loose bounds.
198 TEST_REL(db.get_doclength_upper_bound(),>=,max_len);
199 TEST_REL(db.get_doclength_lower_bound(),<=,min_len);
202 if (get_dbtype() != "inmemory" && !startswith(get_dbtype(), "remote")) {
203 TEST_EQUAL(db.get_wdf_upper_bound("word"), max_wdf);
204 } else {
205 // For inmemory and remote backends, we usually give rather loose
206 // bounds (remote matches use tighter bounds, but querying the
207 // wdf bound gives a looser one).
208 TEST_REL(db.get_wdf_upper_bound("word"),>=,max_wdf);
211 TEST_EQUAL(db.get_wdf_upper_bound(""), 0);
213 return true;
216 /// Check handling of alldocs on an empty database.
217 DEFINE_TESTCASE(alldocspl3, backend) {
218 Xapian::Database db = get_database(string());
220 TEST_EQUAL(db.get_termfreq(string()), 0);
221 TEST_EQUAL(db.get_collection_freq(string()), 0);
222 TEST(db.postlist_begin(string()) == db.postlist_end(string()));
224 return true;
227 /// Regression test for bug#392 in ModifiedPostList iteration, fixed in 1.0.15.
228 DEFINE_TESTCASE(modifiedpostlist1, writable) {
229 Xapian::WritableDatabase db = get_writable_database();
230 Xapian::Document a, b;
231 Xapian::Enquire enq(db);
233 a.add_term("T");
234 enq.set_query(Xapian::Query("T"));
236 db.replace_document(2, a);
237 db.commit();
238 db.replace_document(1, a);
239 db.replace_document(1, b);
241 mset_expect_order(enq.get_mset(0, 2), 2);
243 return true;
246 /// Regression test for chert bug fixed in 1.1.3 (ticket#397).
247 DEFINE_TESTCASE(doclenaftercommit1, writable) {
248 Xapian::WritableDatabase db = get_writable_database();
249 TEST_EXCEPTION(Xapian::DocNotFoundError, db.get_doclength(1));
250 TEST_EXCEPTION(Xapian::DocNotFoundError, db.get_unique_terms(1));
251 db.replace_document(1, Xapian::Document());
252 db.commit();
253 TEST_EQUAL(db.get_doclength(1), 0);
254 TEST_EQUAL(db.get_unique_terms(1), 0);
255 return true;
258 DEFINE_TESTCASE(valuesaftercommit1, writable) {
259 Xapian::WritableDatabase db = get_writable_database();
260 Xapian::Document doc;
261 doc.add_value(0, "value");
262 db.replace_document(2, doc);
263 db.commit();
264 db.replace_document(1, doc);
265 db.replace_document(3, doc);
266 TEST_EQUAL(db.get_document(3).get_value(0), "value");
267 db.commit();
268 TEST_EQUAL(db.get_document(3).get_value(0), "value");
269 return true;
272 DEFINE_TESTCASE(lockfilefd0or1, glass) {
273 #if !defined __WIN32__ && !defined __CYGWIN__ && !defined __OS2__
274 int old_stdin = dup(0);
275 int old_stdout = dup(1);
276 try {
277 // With fd 0 available.
278 close(0);
280 Xapian::WritableDatabase db = get_writable_database();
281 TEST_EXCEPTION(Xapian::DatabaseLockError,
282 (void)get_writable_database_again());
284 // With fd 0 and fd 1 available.
285 close(1);
287 Xapian::WritableDatabase db = get_writable_database();
288 TEST_EXCEPTION(Xapian::DatabaseLockError,
289 (void)get_writable_database_again());
291 // With fd 1 available.
292 dup2(old_stdin, 0);
294 Xapian::WritableDatabase db = get_writable_database();
295 TEST_EXCEPTION(Xapian::DatabaseLockError,
296 (void)get_writable_database_again());
298 } catch (...) {
299 dup2(old_stdin, 0);
300 dup2(old_stdout, 1);
301 close(old_stdin);
302 close(old_stdout);
303 throw;
306 dup2(old_stdout, 1);
307 close(old_stdin);
308 close(old_stdout);
309 #endif
311 return true;
314 /// Regression test for bug fixed in 1.2.13 and 1.3.1.
315 DEFINE_TESTCASE(lockfilealreadyopen1, glass) {
316 string path = get_named_writable_database_path("lockfilealreadyopen1");
317 int fd = ::open((path + "/flintlock").c_str(), O_RDONLY);
318 try {
319 Xapian::WritableDatabase db(path, Xapian::DB_CREATE_OR_OPEN);
320 TEST_EXCEPTION(Xapian::DatabaseLockError,
321 Xapian::WritableDatabase db2(path, Xapian::DB_CREATE_OR_OPEN)
323 } catch (...) {
324 close(fd);
325 throw;
327 close(fd);
329 return true;
332 /// Feature tests for Database::locked().
333 DEFINE_TESTCASE(testlock1, glass) {
334 Xapian::Database rdb;
335 TEST(!rdb.locked());
337 Xapian::WritableDatabase db = get_named_writable_database("testlock1");
338 TEST(db.locked());
339 Xapian::Database db_as_database = db;
340 TEST(db_as_database.locked());
341 TEST(!rdb.locked());
342 rdb = get_writable_database_as_database();
343 TEST(db.locked());
344 TEST(db_as_database.locked());
345 TEST(rdb.locked());
346 db_as_database = rdb;
347 TEST(db.locked());
348 TEST(db_as_database.locked());
349 TEST(rdb.locked());
350 db_as_database.close();
351 TEST(db.locked());
352 TEST(rdb.locked());
353 // After close(), locked() should either work as if close() hadn't been
354 // called or throw Xapian::DatabaseError.
355 try {
356 TEST(db_as_database.locked());
357 } catch (const Xapian::DatabaseError&) {
359 db.close();
360 TEST(!rdb.locked());
361 try {
362 TEST(!db_as_database.locked());
363 } catch (const Xapian::DatabaseError&) {
366 TEST(!rdb.locked());
367 return true;
370 struct MyMatchDecider : public Xapian::MatchDecider {
371 mutable bool called;
373 MyMatchDecider() : called(false) { }
375 bool operator()(const Xapian::Document &) const {
376 called = true;
377 return true;
381 /// Test Xapian::MatchDecider with remote backend fails.
382 DEFINE_TESTCASE(matchdecider4, remote) {
383 Xapian::Database db(get_database("apitest_simpledata"));
384 Xapian::Enquire enquire(db);
385 enquire.set_query(Xapian::Query("paragraph"));
387 MyMatchDecider mdecider;
388 Xapian::MSet mset;
390 TEST_EXCEPTION(Xapian::UnimplementedError,
391 mset = enquire.get_mset(0, 10, NULL, &mdecider));
392 TEST(!mdecider.called);
394 return true;
397 /** Check that replacing an unmodified document doesn't increase the automatic
398 * flush counter. Regression test for bug fixed in 1.1.4/1.0.18.
400 DEFINE_TESTCASE(replacedoc7, writable && !inmemory && !remote) {
401 // The inmemory backend doesn't batch changes, so there's nothing to
402 // check there.
404 // The remote backend doesn't implement the lazy replacement of documents
405 // optimisation currently.
406 Xapian::WritableDatabase db(get_writable_database());
407 Xapian::Document doc;
408 doc.set_data("fish");
409 doc.add_term("Hlocalhost");
410 doc.add_posting("hello", 1);
411 doc.add_posting("world", 2);
412 doc.add_value(1, "myvalue");
413 db.add_document(doc);
414 db.commit();
416 // We add a second document, and then replace the first document with
417 // itself 10000 times. If the document count for the database reopened
418 // read-only is 2, then we triggered an automatic commit.
420 doc.add_term("XREV2");
421 db.add_document(doc);
423 for (int i = 0; i < 10000; ++i) {
424 doc = db.get_document(1);
425 db.replace_document(1, doc);
428 Xapian::Database rodb(get_writable_database_as_database());
429 TEST_EQUAL(rodb.get_doccount(), 1);
431 db.flush();
432 TEST(rodb.reopen());
434 TEST_EQUAL(rodb.get_doccount(), 2);
435 return true;
438 /** Check that replacing a document deleted since the last flush works.
439 * Prior to 1.1.4/1.0.18, this failed to update the collection frequency and
440 * wdf, and caused an assertion failure when assertions were enabled.
442 DEFINE_TESTCASE(replacedoc8, writable) {
443 Xapian::WritableDatabase db(get_writable_database());
445 Xapian::Document doc;
446 doc.set_data("fish");
447 doc.add_term("takeaway");
448 db.add_document(doc);
450 db.delete_document(1);
452 Xapian::Document doc;
453 doc.set_data("chips");
454 doc.add_term("takeaway", 2);
455 db.replace_document(1, doc);
457 db.flush();
458 TEST_EQUAL(db.get_collection_freq("takeaway"), 2);
459 Xapian::PostingIterator p = db.postlist_begin("takeaway");
460 TEST(p != db.postlist_end("takeaway"));
461 TEST_EQUAL(p.get_wdf(), 2);
462 return true;
465 /// Test coverage for DatabaseModifiedError.
466 DEFINE_TESTCASE(databasemodified1, writable && !inmemory && !remote) {
467 // The inmemory backend doesn't support revisions.
469 // The remote backend doesn't work as expected here, I think due to
470 // test harness issues.
471 Xapian::WritableDatabase db(get_writable_database());
472 Xapian::Document doc;
473 doc.set_data("cargo");
474 doc.add_term("abc");
475 doc.add_term("def");
476 doc.add_term("ghi");
477 const int N = 500;
478 for (int i = 0; i < N; ++i) {
479 db.add_document(doc);
481 db.commit();
483 Xapian::Database rodb(get_writable_database_as_database());
484 db.add_document(doc);
485 db.commit();
487 db.add_document(doc);
488 db.commit();
490 db.add_document(doc);
491 try {
492 TEST_EQUAL(*rodb.termlist_begin(N - 1), "abc");
493 return false;
494 } catch (const Xapian::DatabaseModifiedError &) {
497 try {
498 Xapian::Enquire enq(rodb);
499 enq.set_query(Xapian::Query("abc"));
500 Xapian::MSet mset = enq.get_mset(0, 10);
501 return false;
502 } catch (const Xapian::DatabaseModifiedError &) {
505 return true;
508 /// Regression test for bug#462 fixed in 1.0.19 and 1.1.5.
509 DEFINE_TESTCASE(qpmemoryleak1, writable && !inmemory) {
510 // Inmemory never throws DatabaseModifiedError.
511 Xapian::WritableDatabase wdb(get_writable_database());
512 Xapian::Document doc;
514 doc.add_term("foo");
515 for (int i = 100; i < 120; ++i) {
516 doc.add_term(str(i));
519 for (int j = 0; j < 50; ++j) {
520 wdb.add_document(doc);
522 wdb.commit();
524 Xapian::Database database(get_writable_database_as_database());
525 Xapian::QueryParser queryparser;
526 queryparser.set_database(database);
527 TEST_EXCEPTION(Xapian::DatabaseModifiedError,
528 for (int k = 0; k < 1000; ++k) {
529 wdb.add_document(doc);
530 wdb.commit();
531 (void)queryparser.parse_query("1", queryparser.FLAG_PARTIAL);
533 SKIP_TEST("didn't manage to trigger DatabaseModifiedError");
536 return true;
539 static void
540 make_msize1_db(Xapian::WritableDatabase &db, const string &)
542 const char * value0 =
543 "ABBCDEFGHIJKLMMNOPQQRSTTUUVVWXYZZaabcdefghhijjkllmnopqrsttuvwxyz";
544 const char * value1 =
545 "EMLEMMMMMMMNMMLMELEDNLEDMLMLDMLMLMLMEDGFHPOPBAHJIQJNGRKCGF";
546 while (*value0) {
547 Xapian::Document doc;
548 doc.add_value(0, string(1, *value0++));
549 if (*value1) {
550 doc.add_value(1, string(1, *value1++));
551 doc.add_term("K1");
553 db.add_document(doc);
557 /// Regression test for ticket#464, fixed in 1.1.6 and 1.0.20.
558 DEFINE_TESTCASE(msize1, generated) {
559 Xapian::Database db = get_database("msize1", make_msize1_db);
560 Xapian::Enquire enq(db);
561 enq.set_sort_by_value(1, false);
562 enq.set_collapse_key(0);
563 enq.set_query(Xapian::Query("K1"));
565 Xapian::MSet mset = enq.get_mset(0, 10, 1000);
566 Xapian::doccount lb = mset.get_matches_lower_bound();
567 Xapian::doccount ub = mset.get_matches_upper_bound();
568 Xapian::doccount est = mset.get_matches_estimated();
569 TEST_EQUAL(lb, ub);
570 TEST_EQUAL(lb, est);
572 Xapian::MSet mset2 = enq.get_mset(50, 10, 1000);
573 Xapian::doccount lb2 = mset2.get_matches_lower_bound();
574 Xapian::doccount ub2 = mset2.get_matches_upper_bound();
575 Xapian::doccount est2 = mset2.get_matches_estimated();
576 TEST_EQUAL(lb2, ub2);
577 TEST_EQUAL(lb2, est2);
578 TEST_EQUAL(est, est2);
580 Xapian::MSet mset3 = enq.get_mset(0, 60);
581 Xapian::doccount lb3 = mset3.get_matches_lower_bound();
582 Xapian::doccount ub3 = mset3.get_matches_upper_bound();
583 Xapian::doccount est3 = mset3.get_matches_estimated();
584 TEST_EQUAL(lb3, ub3);
585 TEST_EQUAL(lb3, est3);
586 TEST_EQUAL(est, est3);
588 return true;
591 static void
592 make_msize2_db(Xapian::WritableDatabase &db, const string &)
594 const char * value0 = "AAABCDEEFGHIIJJKLLMNNOOPPQQRSTTUVWXYZ";
595 const char * value1 = "MLEMNMLMLMEDEDEMLEMLMLMLPOAHGF";
596 while (*value0) {
597 Xapian::Document doc;
598 doc.add_value(0, string(1, *value0++));
599 if (*value1) {
600 doc.add_value(1, string(1, *value1++));
601 doc.add_term("K1");
603 db.add_document(doc);
607 /// Regression test for bug related to ticket#464, fixed in 1.1.6 and 1.0.20.
608 DEFINE_TESTCASE(msize2, generated) {
609 Xapian::Database db = get_database("msize2", make_msize2_db);
610 Xapian::Enquire enq(db);
611 enq.set_sort_by_value(1, false);
612 enq.set_collapse_key(0);
613 enq.set_query(Xapian::Query("K1"));
615 Xapian::MSet mset = enq.get_mset(0, 10, 1000);
616 Xapian::doccount lb = mset.get_matches_lower_bound();
617 Xapian::doccount ub = mset.get_matches_upper_bound();
618 Xapian::doccount est = mset.get_matches_estimated();
619 TEST_EQUAL(lb, ub);
620 TEST_EQUAL(lb, est);
622 Xapian::MSet mset2 = enq.get_mset(50, 10, 1000);
623 Xapian::doccount lb2 = mset2.get_matches_lower_bound();
624 Xapian::doccount ub2 = mset2.get_matches_upper_bound();
625 Xapian::doccount est2 = mset2.get_matches_estimated();
626 TEST_EQUAL(lb2, ub2);
627 TEST_EQUAL(lb2, est2);
628 TEST_EQUAL(est, est2);
630 Xapian::MSet mset3 = enq.get_mset(0, 60);
631 Xapian::doccount lb3 = mset3.get_matches_lower_bound();
632 Xapian::doccount ub3 = mset3.get_matches_upper_bound();
633 Xapian::doccount est3 = mset3.get_matches_estimated();
634 TEST_EQUAL(lb3, ub3);
635 TEST_EQUAL(lb3, est3);
636 TEST_EQUAL(est, est3);
638 return true;
641 static void
642 make_xordecay1_db(Xapian::WritableDatabase &db, const string &)
644 for (int n = 1; n != 50; ++n) {
645 Xapian::Document doc;
646 for (int i = 1; i != 50; ++i) {
647 if (n % i == 0)
648 doc.add_term("N" + str(i));
650 db.add_document(doc);
654 /// Regression test for bug in decay of XOR, fixed in 1.2.1 and 1.0.21.
655 DEFINE_TESTCASE(xordecay1, generated) {
656 Xapian::Database db = get_database("xordecay1", make_xordecay1_db);
657 Xapian::Enquire enq(db);
658 enq.set_query(Xapian::Query(Xapian::Query::OP_XOR,
659 Xapian::Query("N10"),
660 Xapian::Query(Xapian::Query::OP_OR,
661 Xapian::Query("N2"),
662 Xapian::Query("N3"))));
663 Xapian::MSet mset1 = enq.get_mset(0, 1);
664 Xapian::MSet msetall = enq.get_mset(0, db.get_doccount());
666 TEST(mset_range_is_same(mset1, 0, msetall, 0, mset1.size()));
667 return true;
670 static void
671 make_ordecay_db(Xapian::WritableDatabase &db, const string &)
673 const char * p = "VJ=QC]LUNTaARLI;715RR^];A4O=P4ZG<2CS4EM^^VS[A6QENR";
674 for (int d = 0; p[d]; ++d) {
675 int l = int(p[d] - '0');
676 Xapian::Document doc;
677 for (int n = 1; n < l; ++n) {
678 doc.add_term("N" + str(n));
679 if (n % (d + 1) == 0) {
680 doc.add_term("M" + str(n));
683 db.add_document(doc);
687 /// Regression test for bug in decay of OR to AND, fixed in 1.2.1 and 1.0.21.
688 DEFINE_TESTCASE(ordecay1, generated) {
689 Xapian::Database db = get_database("ordecay", make_ordecay_db);
690 Xapian::Enquire enq(db);
691 enq.set_query(Xapian::Query(Xapian::Query::OP_OR,
692 Xapian::Query("N20"),
693 Xapian::Query("N21")));
695 Xapian::MSet msetall = enq.get_mset(0, db.get_doccount());
696 for (unsigned int i = 1; i < msetall.size(); ++i) {
697 Xapian::MSet submset = enq.get_mset(0, i);
698 TEST(mset_range_is_same(submset, 0, msetall, 0, submset.size()));
700 return true;
703 /** Regression test for bug in decay of OR to AND_MAYBE, fixed in 1.2.1 and
704 * 1.0.21.
706 DEFINE_TESTCASE(ordecay2, generated) {
707 Xapian::Database db = get_database("ordecay", make_ordecay_db);
708 Xapian::Enquire enq(db);
709 std::vector<Xapian::Query> q;
710 q.push_back(Xapian::Query("M20"));
711 q.push_back(Xapian::Query("N21"));
712 q.push_back(Xapian::Query("N22"));
713 enq.set_query(Xapian::Query(Xapian::Query::OP_OR,
714 Xapian::Query("N25"),
715 Xapian::Query(Xapian::Query::OP_AND,
716 q.begin(),
717 q.end())));
719 Xapian::MSet msetall = enq.get_mset(0, db.get_doccount());
720 for (unsigned int i = 1; i < msetall.size(); ++i) {
721 Xapian::MSet submset = enq.get_mset(0, i);
722 TEST(mset_range_is_same(submset, 0, msetall, 0, submset.size()));
724 return true;
727 static void
728 make_orcheck_db(Xapian::WritableDatabase &db, const string &)
730 static const int t1[6] = {2, 4, 6, 8, 10, 0};
731 static const int t2[11] = {6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 0};
732 static const int t3[11] = {3, 7, 8, 11, 12, 13, 14, 15, 16, 17, 0};
734 for (unsigned i = 1; i <= 17; ++i) {
735 Xapian::Document doc;
736 db.replace_document(i, doc);
738 for (const int * p = t1; *p != 0; ++p) {
739 Xapian::Document doc(db.get_document(*p));
740 doc.add_term("T1");
741 db.replace_document(*p, doc);
743 for (const int * p = t2; *p != 0; ++p) {
744 Xapian::Document doc(db.get_document(*p));
745 doc.add_term("T2");
746 if (*p < 17) {
747 doc.add_term("T2_lowfreq");
749 doc.add_value(2, "1");
750 db.replace_document(*p, doc);
752 for (const int * p = t3; *p != 0; ++p) {
753 Xapian::Document doc(db.get_document(*p));
754 doc.add_term("T3");
755 if (*p < 17) {
756 doc.add_term("T3_lowfreq");
758 doc.add_value(3, "1");
759 db.replace_document(*p, doc);
763 /** Regression test for bugs in the check() method of OrPostList. (ticket #485)
764 * Bugs introduced and fixed between 1.2.0 and 1.2.1 (never in a release).
766 DEFINE_TESTCASE(orcheck1, generated) {
767 Xapian::Database db = get_database("orcheck1", make_orcheck_db);
768 Xapian::Enquire enq(db);
769 Xapian::Query q1("T1");
770 Xapian::Query q2("T2");
771 Xapian::Query q2l("T2_lowfreq");
772 Xapian::Query q3("T3");
773 Xapian::Query q3l("T3_lowfreq");
774 Xapian::Query v2(Xapian::Query::OP_VALUE_RANGE, 2, "0", "2");
775 Xapian::Query v3(Xapian::Query::OP_VALUE_RANGE, 3, "0", "2");
777 tout << "Checking q2 OR q3\n";
778 enq.set_query(Xapian::Query(Xapian::Query::OP_AND, q1,
779 Xapian::Query(Xapian::Query::OP_OR, q2, q3)));
780 mset_expect_order(enq.get_mset(0, db.get_doccount()), 8, 6);
782 tout << "Checking q2l OR q3\n";
783 enq.set_query(Xapian::Query(Xapian::Query::OP_AND, q1,
784 Xapian::Query(Xapian::Query::OP_OR, q2l, q3)));
785 mset_expect_order(enq.get_mset(0, db.get_doccount()), 8, 6);
787 tout << "Checking q2 OR q3l\n";
788 enq.set_query(Xapian::Query(Xapian::Query::OP_AND, q1,
789 Xapian::Query(Xapian::Query::OP_OR, q2, q3l)));
790 mset_expect_order(enq.get_mset(0, db.get_doccount()), 8, 6);
792 tout << "Checking v2 OR q3\n";
793 enq.set_query(Xapian::Query(Xapian::Query::OP_AND, q1,
794 Xapian::Query(Xapian::Query::OP_OR, v2, q3)));
795 mset_expect_order(enq.get_mset(0, db.get_doccount()), 8, 6);
797 tout << "Checking q2 OR v3\n";
798 enq.set_query(Xapian::Query(Xapian::Query::OP_AND, q1,
799 Xapian::Query(Xapian::Query::OP_OR, q2, v3)));
800 // Order of results in this one is different, because v3 gives no weight,
801 // both documents are in q2, and document 8 has a higher length.
802 mset_expect_order(enq.get_mset(0, db.get_doccount()), 6, 8);
804 return true;
807 /** Regression test for bug fixed in 1.2.1 and 1.0.21.
809 * We failed to mark the Btree as unmodified after cancel().
811 DEFINE_TESTCASE(failedreplace1, glass) {
812 Xapian::WritableDatabase db(get_writable_database());
813 Xapian::Document doc;
814 doc.add_term("foo");
815 db.add_document(doc);
816 Xapian::docid did = db.add_document(doc);
817 doc.add_term("abc");
818 doc.add_term(string(1000, 'm'));
819 doc.add_term("xyz");
820 TEST_EXCEPTION(Xapian::InvalidArgumentError, db.replace_document(did, doc));
821 db.commit();
822 TEST_EQUAL(db.get_doccount(), 0);
823 TEST_EQUAL(db.get_termfreq("foo"), 0);
824 return true;
827 DEFINE_TESTCASE(failedreplace2, glass) {
828 Xapian::WritableDatabase db(get_writable_database("apitest_simpledata"));
829 db.commit();
830 Xapian::doccount db_size = db.get_doccount();
831 Xapian::Document doc;
832 doc.set_data("wibble");
833 doc.add_term("foo");
834 doc.add_value(0, "seven");
835 db.add_document(doc);
836 Xapian::docid did = db.add_document(doc);
837 doc.add_term("abc");
838 doc.add_term(string(1000, 'm'));
839 doc.add_term("xyz");
840 doc.add_value(0, "six");
841 TEST_EXCEPTION(Xapian::InvalidArgumentError, db.replace_document(did, doc));
842 db.commit();
843 TEST_EQUAL(db.get_doccount(), db_size);
844 TEST_EQUAL(db.get_termfreq("foo"), 0);
845 return true;
848 /// Coverage for SelectPostList::skip_to().
849 DEFINE_TESTCASE(phrase3, positional) {
850 Xapian::Database db = get_database("apitest_phrase");
852 const char * phrase_words[] = { "phrase", "near" };
853 Xapian::Query q(Xapian::Query::OP_NEAR, phrase_words, phrase_words + 2, 12);
854 q = Xapian::Query(Xapian::Query::OP_AND_MAYBE, Xapian::Query("pad"), q);
856 Xapian::Enquire enquire(db);
857 enquire.set_query(q);
858 Xapian::MSet mset = enquire.get_mset(0, 5);
860 return true;
863 /// Check that get_mset(<large number>, 10) doesn't exhaust memory needlessly.
864 // Regression test for fix in 1.2.4.
865 DEFINE_TESTCASE(msetfirst2, backend) {
866 Xapian::Database db(get_database("apitest_simpledata"));
867 Xapian::Enquire enquire(db);
868 enquire.set_query(Xapian::Query("paragraph"));
869 Xapian::MSet mset;
870 // Before the fix, this tried to allocated too much memory.
871 mset = enquire.get_mset(0xfffffff0, 1);
872 TEST_EQUAL(mset.get_firstitem(), 0xfffffff0);
873 // Check that the number of documents gets clamped too.
874 mset = enquire.get_mset(1, 0xfffffff0);
875 TEST_EQUAL(mset.get_firstitem(), 1);
876 // Another regression test - MatchNothing used to give an MSet with
877 // get_firstitem() returning 0.
878 enquire.set_query(Xapian::Query::MatchNothing);
879 mset = enquire.get_mset(1, 1);
880 TEST_EQUAL(mset.get_firstitem(), 1);
881 return true;
884 DEFINE_TESTCASE(bm25weight2, backend) {
885 Xapian::Database db(get_database("etext"));
886 Xapian::Enquire enquire(db);
887 enquire.set_query(Xapian::Query("the"));
888 enquire.set_weighting_scheme(Xapian::BM25Weight(0, 0, 0, 0, 1));
889 Xapian::MSet mset = enquire.get_mset(0, 100);
890 TEST_REL(mset.size(),>=,2);
891 double weight0 = mset[0].get_weight();
892 for (size_t i = 1; i != mset.size(); ++i) {
893 TEST_EQUAL(weight0, mset[i].get_weight());
895 return true;
898 DEFINE_TESTCASE(unigramlmweight2, backend) {
899 Xapian::Database db(get_database("etext"));
900 Xapian::Enquire enquire(db);
901 enquire.set_query(Xapian::Query("the"));
902 enquire.set_weighting_scheme(Xapian::LMWeight());
903 Xapian::MSet mset = enquire.get_mset(0, 100);
904 TEST_REL(mset.size(),>=,2);
905 return true;
908 DEFINE_TESTCASE(tradweight2, backend) {
909 Xapian::Database db(get_database("etext"));
910 Xapian::Enquire enquire(db);
911 enquire.set_query(Xapian::Query("the"));
912 enquire.set_weighting_scheme(Xapian::TradWeight(0));
913 Xapian::MSet mset = enquire.get_mset(0, 100);
914 TEST_REL(mset.size(),>=,2);
915 double weight0 = mset[0].get_weight();
916 for (size_t i = 1; i != mset.size(); ++i) {
917 TEST_EQUAL(weight0, mset[i].get_weight());
919 return true;
922 // Regression test for bug fix in 1.2.9.
923 DEFINE_TESTCASE(emptydb1, backend) {
924 Xapian::Database db(get_database(string()));
925 static const Xapian::Query::op ops[] = {
926 Xapian::Query::OP_AND,
927 Xapian::Query::OP_OR,
928 Xapian::Query::OP_AND_NOT,
929 Xapian::Query::OP_XOR,
930 Xapian::Query::OP_AND_MAYBE,
931 Xapian::Query::OP_FILTER,
932 Xapian::Query::OP_NEAR,
933 Xapian::Query::OP_PHRASE,
934 Xapian::Query::OP_ELITE_SET
936 const Xapian::Query::op * p;
937 for (p = ops; p - ops != sizeof(ops) / sizeof(*ops); ++p) {
938 tout << *p << endl;
939 Xapian::Enquire enquire(db);
940 Xapian::Query query(*p, Xapian::Query("a"), Xapian::Query("b"));
941 enquire.set_query(query);
942 Xapian::MSet mset = enquire.get_mset(0, 10);
943 TEST_EQUAL(mset.get_matches_estimated(), 0);
944 TEST_EQUAL(mset.get_matches_upper_bound(), 0);
945 TEST_EQUAL(mset.get_matches_lower_bound(), 0);
947 return true;
950 /// Test error opening non-existent stub databases.
951 // Regression test for bug fixed in 1.3.1 and 1.2.11.
952 DEFINE_TESTCASE(stubdb7, !backend) {
953 TEST_EXCEPTION(Xapian::DatabaseOpeningError,
954 Xapian::Database("nosuchdirectory", Xapian::DB_BACKEND_STUB));
955 TEST_EXCEPTION(Xapian::DatabaseOpeningError,
956 Xapian::WritableDatabase("nosuchdirectory",
957 Xapian::DB_OPEN|Xapian::DB_BACKEND_STUB));
958 return true;
961 /// Test which checks the weights are as expected.
962 // This runs for multi_* too, so serves to check that we get the same weights
963 // with multiple databases as without.
964 DEFINE_TESTCASE(msetweights1, backend) {
965 Xapian::Database db = get_database("apitest_simpledata");
966 Xapian::Enquire enq(db);
967 Xapian::Query q(Xapian::Query::OP_OR,
968 Xapian::Query("paragraph"),
969 Xapian::Query("word"));
970 enq.set_query(q);
971 // 5 documents match, and the 4th and 5th have the same weight, so ask for
972 // 4 as that's a good test that we get the right one in this case.
973 Xapian::MSet mset = enq.get_mset(0, 4);
975 static const struct { Xapian::docid did; double wt; } expected[] = {
976 { 2, 1.2058248004573934864 },
977 { 4, 0.81127876655507624726 },
978 { 1, 0.17309550762546158098 },
979 { 3, 0.14609528172558261527 }
982 TEST_EQUAL(mset.size(), sizeof(expected) / sizeof(expected[0]));
983 for (size_t i = 0; i < mset.size(); ++i) {
984 TEST_EQUAL(*mset[i], expected[i].did);
985 TEST_EQUAL_DOUBLE(mset[i].get_weight(), expected[i].wt);
988 // Now test a query which matches only even docids, so in the multi case
989 // one subdatabase doesn't match.
990 enq.set_query(Xapian::Query("one"));
991 mset = enq.get_mset(0, 3);
993 static const struct { Xapian::docid did; double wt; } expected2[] = {
994 { 6, 0.73354729848273669823 },
995 { 2, 0.45626501034348893038 }
998 TEST_EQUAL(mset.size(), sizeof(expected2) / sizeof(expected2[0]));
999 for (size_t i = 0; i < mset.size(); ++i) {
1000 TEST_EQUAL(*mset[i], expected2[i].did);
1001 TEST_EQUAL_DOUBLE(mset[i].get_weight(), expected2[i].wt);
1004 return true;
1007 DEFINE_TESTCASE(itorskiptofromend1, backend) {
1008 Xapian::Database db = get_database("apitest_simpledata");
1010 Xapian::TermIterator t = db.termlist_begin(1);
1011 t.skip_to("zzzzz");
1012 TEST(t == db.termlist_end(1));
1013 // This worked in 1.2.x but segfaulted in 1.3.1.
1014 t.skip_to("zzzzzz");
1016 Xapian::PostingIterator p = db.postlist_begin("one");
1017 p.skip_to(99999);
1018 TEST(p == db.postlist_end("one"));
1019 // This segfaulted prior to 1.3.2.
1020 p.skip_to(999999);
1022 Xapian::PositionIterator i = db.positionlist_begin(6, "one");
1023 i.skip_to(99999);
1024 TEST(i == db.positionlist_end(6, "one"));
1025 // This segfaulted prior to 1.3.2.
1026 i.skip_to(999999);
1028 Xapian::ValueIterator v = db.valuestream_begin(1);
1029 v.skip_to(99999);
1030 TEST(v == db.valuestream_end(1));
1031 // These segfaulted prior to 1.3.2.
1032 v.skip_to(999999);
1033 v.check(9999999);
1035 return true;
1038 /// Check handling of invalid block sizes.
1039 // Regression test for bug fixed in 1.2.17 and 1.3.2 - the size gets fixed
1040 // but the uncorrected size was passed to the base file. Also, abort() was
1041 // called on 0.
1042 DEFINE_TESTCASE(blocksize1, glass) {
1043 string db_dir = "." + get_dbtype();
1044 mkdir(db_dir.c_str(), 0755);
1045 db_dir += "/db__blocksize1";
1046 int flags;
1047 if (get_dbtype() == "glass") {
1048 flags = Xapian::DB_CREATE|Xapian::DB_BACKEND_GLASS;
1049 } else {
1050 FAIL_TEST("Unhandled backend type");
1052 static const unsigned bad_sizes[] = {
1053 65537, 8000, 2000, 1024, 16, 7, 3, 1, 0
1055 for (size_t i = 0; i < sizeof(bad_sizes) / sizeof(bad_sizes[0]); ++i) {
1056 size_t block_size = bad_sizes[i];
1057 rm_rf(db_dir);
1058 Xapian::WritableDatabase db(db_dir, flags, block_size);
1059 Xapian::Document doc;
1060 doc.add_term("XYZ");
1061 doc.set_data("foo");
1062 db.add_document(doc);
1063 db.commit();
1065 return true;
1068 /// Feature test for Xapian::DB_NO_TERMLIST.
1069 DEFINE_TESTCASE(notermlist1, glass) {
1070 string db_dir = "." + get_dbtype();
1071 mkdir(db_dir.c_str(), 0755);
1072 db_dir += "/db__notermlist1";
1073 int flags = Xapian::DB_CREATE|Xapian::DB_NO_TERMLIST;
1074 if (get_dbtype() == "glass") {
1075 flags |= Xapian::DB_BACKEND_GLASS;
1077 rm_rf(db_dir);
1078 Xapian::WritableDatabase db(db_dir, flags);
1079 Xapian::Document doc;
1080 doc.add_term("hello");
1081 doc.add_value(42, "answer");
1082 db.add_document(doc);
1083 db.commit();
1084 TEST(!file_exists(db_dir + "/termlist.glass"));
1085 TEST_EXCEPTION(Xapian::FeatureUnavailableError, db.termlist_begin(1));
1086 return true;
1089 /// Regression test for bug starting a new glass freelist block.
1090 DEFINE_TESTCASE(newfreelistblock1, writable) {
1091 Xapian::Document doc;
1092 doc.add_term("foo");
1093 for (int i = 100; i < 120; ++i) {
1094 doc.add_term(str(i));
1097 Xapian::WritableDatabase wdb(get_writable_database());
1098 for (int j = 0; j < 50; ++j) {
1099 wdb.add_document(doc);
1101 wdb.commit();
1103 for (int k = 0; k < 1000; ++k) {
1104 wdb.add_document(doc);
1105 wdb.commit();
1108 return true;
1111 /** Check that the parent directory for the database doesn't need to be
1112 * writable. Regression test for early versions on the glass new btree
1113 * branch which failed to append a "/" when generating a temporary filename
1114 * from the database directory.
1116 DEFINE_TESTCASE(readonlyparentdir1, glass) {
1117 #if !defined __WIN32__ && !defined __CYGWIN__ && !defined __OS2__
1118 string path = get_named_writable_database_path("readonlyparentdir1");
1119 // Fix permissions if the previous test was killed.
1120 (void)chmod(path.c_str(), 0700);
1121 mkdir(path.c_str(), 0777);
1122 mkdir((path + "/sub").c_str(), 0777);
1123 Xapian::WritableDatabase db = get_named_writable_database("readonlyparentdir1/sub");
1124 TEST(chmod(path.c_str(), 0500) == 0);
1125 try {
1126 Xapian::Document doc;
1127 doc.add_term("hello");
1128 doc.set_data("some text");
1129 db.add_document(doc);
1130 db.commit();
1131 } catch (...) {
1132 // Attempt to fix the permissions, otherwise things like "rm -rf" on
1133 // the source tree will fail.
1134 (void)chmod(path.c_str(), 0700);
1135 throw;
1137 TEST(chmod(path.c_str(), 0700) == 0);
1138 #endif
1139 return true;
1142 static void
1143 make_phrasebug1_db(Xapian::WritableDatabase &db, const string &)
1145 Xapian::Document doc;
1146 doc.add_posting("hurricane", 199881);
1147 doc.add_posting("hurricane", 203084);
1148 doc.add_posting("katrina", 199882);
1149 doc.add_posting("katrina", 202473);
1150 doc.add_posting("katrina", 203085);
1151 db.add_document(doc);
1154 /// Regression test for ticket#653, fixed in 1.3.2 and 1.2.19.
1155 DEFINE_TESTCASE(phrasebug1, generated && positional) {
1156 Xapian::Database db = get_database("phrasebug1", make_phrasebug1_db);
1157 const char * qterms[] = { "katrina", "hurricane" };
1158 Xapian::Enquire e(db);
1159 Xapian::Query q(Xapian::Query::OP_PHRASE, qterms, qterms + 2, 5);
1160 e.set_query(q);
1161 Xapian::MSet mset = e.get_mset(0, 100);
1162 TEST_EQUAL(mset.size(), 0);
1163 const char * qterms2[] = { "hurricane", "katrina" };
1164 Xapian::Query q2(Xapian::Query::OP_PHRASE, qterms2, qterms2 + 2, 5);
1165 e.set_query(q2);
1166 mset = e.get_mset(0, 100);
1167 TEST_EQUAL(mset.size(), 1);
1168 return true;
1171 /// Feature test for Xapian::DB_RETRY_LOCK
1172 DEFINE_TESTCASE(retrylock1, writable && !inmemory && !remote) {
1173 // FIXME: Can't see an easy way to test this for remote databases - the
1174 // harness doesn't seem to provide a suitable way to reopen a remote.
1175 #if defined HAVE_FORK && defined HAVE_SOCKETPAIR
1176 int fds[2];
1177 if (socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, PF_UNSPEC, fds) < 0) {
1178 FAIL_TEST("socketpair() failed");
1180 if (fcntl(fds[1], F_SETFL, O_NONBLOCK) < 0)
1181 FAIL_TEST("fcntl() failed to set O_NONBLOCK");
1182 pid_t child = fork();
1183 if (child == -1)
1184 FAIL_TEST("fork() failed");
1185 if (child == 0) {
1186 // Wait for signal that parent has opened the database.
1187 char ch;
1188 while (read(fds[0], &ch, 1) < 0) { }
1190 try {
1191 Xapian::WritableDatabase db2(get_named_writable_database_path("retrylock1"),
1192 Xapian::DB_OPEN|Xapian::DB_RETRY_LOCK);
1193 if (write(fds[0], "y", 1)) { }
1194 } catch (const Xapian::DatabaseLockError &) {
1195 if (write(fds[0], "l", 1)) { }
1196 } catch (const Xapian::Error &e) {
1197 const string & m = e.get_description();
1198 if (write(fds[0], m.data(), m.size())) { }
1199 } catch (...) {
1200 if (write(fds[0], "o", 1)) { }
1202 _exit(0);
1205 close(fds[0]);
1207 Xapian::WritableDatabase db = get_named_writable_database("retrylock1");
1208 if (write(fds[1], "", 1) != 1)
1209 FAIL_TEST("Failed to signal to child process");
1211 char result[256];
1212 int r = read(fds[1], result, sizeof(result));
1213 if (r == -1) {
1214 if (errno == EAGAIN) {
1215 // Good.
1216 result[0] = 'y';
1217 } else {
1218 // Error.
1219 tout << "errno=" << errno << ": " << strerror(errno) << endl;
1220 result[0] = 'e';
1222 r = 1;
1223 } else if (r >= 1) {
1224 if (result[0] == 'y') {
1225 // Child process managed to also get write lock!
1226 result[0] = '!';
1228 } else {
1229 // EOF.
1230 result[0] = 'z';
1231 r = 1;
1234 try {
1235 db.close();
1236 } catch (...) {
1237 kill(child, SIGKILL);
1238 int status;
1239 while (waitpid(child, &status, 0) < 0) {
1240 if (errno != EINTR) break;
1242 throw;
1245 if (result[0] == 'y') {
1246 struct timeval tv;
1247 tv.tv_sec = 3;
1248 tv.tv_usec = 0;
1249 fd_set f;
1250 FD_ZERO(&f);
1251 FD_SET(fds[1], &f);
1252 int sr = select(fds[1] + 1, &f, NULL, &f, &tv);
1253 if (sr == 0) {
1254 // Timed out.
1255 result[0] = 'T';
1256 r = 1;
1257 } else {
1258 r = read(fds[1], result, sizeof(result));
1259 if (r == -1) {
1260 // Error.
1261 tout << "errno=" << errno << ": " << strerror(errno) << endl;
1262 result[0] = 'E';
1263 r = 1;
1264 } else if (r == 0) {
1265 // EOF.
1266 result[0] = 'Z';
1267 r = 1;
1272 close(fds[1]);
1274 kill(child, SIGKILL);
1275 int status;
1276 while (waitpid(child, &status, 0) < 0) {
1277 if (errno != EINTR) break;
1280 tout << string(result, r) << endl;
1281 TEST_EQUAL(result[0], 'y');
1282 #endif
1284 return true;
1287 // Opening a WritableDatabase with low fds available - it should avoid them.
1288 DEFINE_TESTCASE(dbfilefd012, glass) {
1289 #if !defined __WIN32__ && !defined __CYGWIN__ && !defined __OS2__
1290 int oldfds[3];
1291 for (int i = 0; i < 3; ++i) {
1292 oldfds[i] = dup(i);
1294 try {
1295 for (int j = 0; j < 3; ++j) {
1296 close(j);
1297 TEST_EQUAL(lseek(j, 0, SEEK_CUR), -1);
1298 TEST_EQUAL(errno, EBADF);
1301 Xapian::WritableDatabase db = get_writable_database();
1303 // Check we didn't use any of those low fds for tables, as that risks
1304 // data corruption if some other code in the same process tries to
1305 // write to them (see #651).
1306 for (int fd = 0; fd < 3; ++fd) {
1307 // Check that the fd is still closed, or isn't open O_RDWR (the
1308 // lock file gets opened O_WRONLY), or it's a pipe (if we're using
1309 // a child process to hold a non-OFD fcntl lock).
1310 int flags = fcntl(fd, F_GETFL);
1311 if (flags == -1) {
1312 TEST_EQUAL(errno, EBADF);
1313 } else if ((flags & O_ACCMODE) != O_RDWR) {
1314 // OK.
1315 } else {
1316 struct stat sb;
1317 TEST_NOT_EQUAL(fstat(fd, &sb), -1);
1318 #ifdef S_ISSOCK
1319 TEST(S_ISSOCK(sb.st_mode));
1320 #else
1321 // If we can't check it is a socket, at least check it is not a
1322 // regular file.
1323 TEST(!S_ISREG(sb.st_mode));
1324 #endif
1327 } catch (...) {
1328 for (int j = 0; j < 3; ++j) {
1329 dup2(oldfds[j], j);
1330 close(oldfds[j]);
1332 throw;
1335 for (int j = 0; j < 3; ++j) {
1336 dup2(oldfds[j], j);
1337 close(oldfds[j]);
1339 #endif
1341 return true;
1344 /// Regression test for #675, fixed in 1.3.3 and 1.2.21.
1345 DEFINE_TESTCASE(cursorbug1, glass) {
1346 Xapian::WritableDatabase wdb = get_writable_database();
1347 Xapian::Database db = get_writable_database_as_database();
1348 Xapian::Enquire enq(db);
1349 enq.set_query(Xapian::Query::MatchAll);
1350 Xapian::MSet mset;
1351 // The original problem triggered for chert and glass on repeat==7.
1352 for (int repeat = 0; repeat < 10; ++repeat) {
1353 tout.str(string());
1354 tout << "iteration #" << repeat << endl;
1356 const int ITEMS = 10;
1357 int free_id = db.get_doccount();
1358 int offset = max(free_id, ITEMS * 2) - (ITEMS * 2);
1359 int limit = offset + (ITEMS * 2);
1361 mset = enq.get_mset(offset, limit);
1362 for (Xapian::MSetIterator m1 = mset.begin(); m1 != mset.end(); ++m1) {
1363 (void)m1.get_document().get_value(0);
1366 for (int i = free_id; i <= free_id + ITEMS; ++i) {
1367 Xapian::Document doc;
1368 const string & id = str(i);
1369 string qterm = "Q" + id;
1370 doc.add_value(0, id);
1371 doc.add_boolean_term(qterm);
1372 wdb.replace_document(qterm, doc);
1374 wdb.commit();
1376 db.reopen();
1377 mset = enq.get_mset(offset, limit);
1378 for (Xapian::MSetIterator m2 = mset.begin(); m2 != mset.end(); ++m2) {
1379 (void)m2.get_document().get_value(0);
1383 return true;
1386 // Regression test for #674, fixed in 1.2.21 and 1.3.3.
1387 DEFINE_TESTCASE(sortvalue2, backend) {
1388 Xapian::Database db = get_database("apitest_simpledata");
1389 db.add_database(get_database("apitest_simpledata2"));
1390 Xapian::Enquire enq(db);
1391 enq.set_query(Xapian::Query::MatchAll);
1392 enq.set_sort_by_value(0, false);
1393 Xapian::MSet mset = enq.get_mset(0, 50);
1395 // Check all results are in key order - the bug was that they were sorted
1396 // by docid instead with multiple remote databases.
1397 string old_key;
1398 for (Xapian::MSetIterator i = mset.begin(); i != mset.end(); ++i) {
1399 string key = db.get_document(*i).get_value(0);
1400 TEST(old_key <= key);
1401 swap(old_key, key);
1403 return true;
1406 /// Check behaviour of Enquire::get_query().
1407 DEFINE_TESTCASE(enquiregetquery1, backend) {
1408 Xapian::Database db = get_database("apitest_simpledata");
1409 Xapian::Enquire enq(db);
1410 TEST_EQUAL(enq.get_query().get_description(), "Query()");
1411 return true;
1414 DEFINE_TESTCASE(embedded1, singlefile) {
1415 // In reality you should align the embedded database to a multiple of
1416 // database block size, but any offset is meant to work.
1417 off_t offset = 1234;
1419 Xapian::Database db = get_database("apitest_simpledata");
1420 const string & db_path = get_database_path("apitest_simpledata");
1421 const string & tmp_path = db_path + "-embedded";
1422 ofstream out(tmp_path, fstream::trunc|fstream::binary);
1423 out.seekp(offset);
1424 out << ifstream(db_path, fstream::binary).rdbuf();
1425 out.close();
1428 int fd = open(tmp_path.c_str(), O_RDONLY|O_BINARY);
1429 lseek(fd, offset, SEEK_SET);
1430 Xapian::Database db_embedded(fd);
1431 TEST_EQUAL(db.get_doccount(), db_embedded.get_doccount());
1435 int fd = open(tmp_path.c_str(), O_RDONLY|O_BINARY);
1436 lseek(fd, offset, SEEK_SET);
1437 size_t check_errors =
1438 Xapian::Database::check(fd, Xapian::DBCHECK_SHOW_STATS, &tout);
1439 TEST_EQUAL(check_errors, 0);
1442 return true;
1445 /// Regression test for bug fixed in 1.3.7.
1446 DEFINE_TESTCASE(exactxor1, backend) {
1447 Xapian::Database db = get_database("apitest_simpledata");
1448 Xapian::Enquire enq(db);
1450 const char * words[4] = { "blank", "test", "paragraph", "banana" };
1451 Xapian::Query q(Xapian::Query::OP_XOR, words, words + 4);
1452 enq.set_query(q);
1453 enq.set_weighting_scheme(Xapian::BoolWeight());
1454 Xapian::MSet mset = enq.get_mset(0, 0);
1455 // A reversed conditional gave us 5 in this case.
1456 TEST_EQUAL(mset.get_matches_upper_bound(), 6);
1457 // Test improved lower bound in 1.3.7 (earlier versions gave 0).
1458 TEST_EQUAL(mset.get_matches_lower_bound(), 2);
1460 const char * words2[4] = { "queri", "test", "paragraph", "word" };
1461 Xapian::Query q2(Xapian::Query::OP_XOR, words2, words2 + 4);
1462 enq.set_query(q2);
1463 enq.set_weighting_scheme(Xapian::BoolWeight());
1464 mset = enq.get_mset(0, 0);
1465 // A reversed conditional gave us 6 in this case.
1466 TEST_EQUAL(mset.get_matches_upper_bound(), 5);
1467 // Test improved lower bound in 1.3.7 (earlier versions gave 0).
1468 TEST_EQUAL(mset.get_matches_lower_bound(), 1);
1470 return true;
1473 /// Feature test for Database::get_revision().
1474 DEFINE_TESTCASE(getrevision1, glass) {
1475 Xapian::WritableDatabase db = get_writable_database();
1476 TEST_EQUAL(db.get_revision(), 0);
1477 db.commit();
1478 TEST_EQUAL(db.get_revision(), 0);
1479 Xapian::Document doc;
1480 doc.add_term("hello");
1481 db.add_document(doc);
1482 TEST_EQUAL(db.get_revision(), 0);
1483 db.commit();
1484 TEST_EQUAL(db.get_revision(), 1);
1485 db.commit();
1486 TEST_EQUAL(db.get_revision(), 1);
1487 db.add_document(doc);
1488 db.commit();
1489 TEST_EQUAL(db.get_revision(), 2);
1490 return true;
1493 /// Feature test for DOC_ASSUME_VALID.
1494 DEFINE_TESTCASE(getdocumentlazy1, backend) {
1495 Xapian::Database db = get_database("apitest_simpledata");
1496 Xapian::Document doc_lazy = db.get_document(2, Xapian::DOC_ASSUME_VALID);
1497 Xapian::Document doc = db.get_document(2);
1498 TEST_EQUAL(doc.get_data(), doc_lazy.get_data());
1499 TEST_EQUAL(doc.get_value(0), doc_lazy.get_value(0));
1500 return true;
1503 /// Feature test for DOC_ASSUME_VALID for a docid that doesn't actually exist.
1504 DEFINE_TESTCASE(getdocumentlazy2, backend) {
1505 Xapian::Database db = get_database("apitest_simpledata");
1506 Xapian::Document doc;
1507 try {
1508 doc = db.get_document(db.get_lastdocid() + 1, Xapian::DOC_ASSUME_VALID);
1509 } catch (const Xapian::DocNotFoundError&) {
1510 // DOC_ASSUME_VALID is really just a hint, so ignoring is OK (the
1511 // remote backend currently does).
1513 TEST(doc.get_data().empty());
1514 TEST_EXCEPTION(Xapian::DocNotFoundError,
1515 doc = db.get_document(db.get_lastdocid() + 1);
1517 return true;
1520 static void
1521 gen_uniqterms_gt_doclen_db(Xapian::WritableDatabase& db, const string&)
1523 Xapian::Document doc;
1524 doc.add_term("foo");
1525 doc.add_boolean_term("bar");
1526 db.add_document(doc);
1529 DEFINE_TESTCASE(getuniqueterms1, generated) {
1530 Xapian::Database db =
1531 get_database("uniqterms_gt_doclen", gen_uniqterms_gt_doclen_db);
1533 TEST_REL(db.get_unique_terms(1), <=, db.get_doclength(1));
1534 TEST_REL(db.get_unique_terms(1), <, db.get_document(1).termlist_count());
1536 return true;
1539 /// Check estimate is rounded to suitable number of S.F. - new in 1.4.3.
1540 DEFINE_TESTCASE(estimaterounding1, backend) {
1541 Xapian::Database db = get_database("etext");
1542 Xapian::Enquire enquire(db);
1543 enquire.set_query(Xapian::Query("the") | Xapian::Query("road"));
1544 Xapian::MSet mset = enquire.get_mset(0, 10);
1545 // MSet::get_description() includes bounds and raw estimate.
1546 tout << mset.get_description() << endl;
1547 // Bounds are 411-439, raw estimate is 419.
1548 TEST_EQUAL(mset.get_matches_estimated() % 10, 0);
1549 enquire.set_query(Xapian::Query("king") | Xapian::Query("prussia"));
1550 mset = enquire.get_mset(0, 10);
1551 tout << mset.get_description() << endl;
1552 // Bounds are 111-138, raw estimate is 133.
1553 TEST_EQUAL(mset.get_matches_estimated() % 10, 0);
1554 return true;