"Fossies" - the Fresh Open Source Software Archive

Member "xapian-core-1.4.14/tests/api_backend.cc" (23 Nov 2019, 54287 Bytes) of package /linux/www/xapian-core-1.4.14.tar.xz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "api_backend.cc": 1.4.13_vs_1.4.14.

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