1 /** @file
2  * @brief tests of posting sources
3  */
4 /* Copyright 2008,2009,2011,2015,2016,2019 Olly Betts
5  * Copyright 2008,2009 Lemur Consulting Ltd
6  * Copyright 2010 Richard Boulton
7  *
8  * This program is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU General Public License as
10  * published by the Free Software Foundation; either version 2 of the
11  * License, or (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
21  * USA
22  */
23 
24 #include <config.h>
25 
26 #include "api_postingsource.h"
27 
28 #include <xapian.h>
29 
30 #include <string>
31 #include "safeunistd.h"
32 
33 #include "str.h"
34 #include "testutils.h"
35 #include "apitest.h"
36 
37 using namespace std;
38 
39 class MyOddPostingSource : public Xapian::PostingSource {
40     Xapian::doccount num_docs;
41 
42     Xapian::doccount last_docid;
43 
44     Xapian::docid did;
45 
MyOddPostingSource(Xapian::doccount num_docs_,Xapian::doccount last_docid_)46     MyOddPostingSource(Xapian::doccount num_docs_,
47 		       Xapian::doccount last_docid_)
48 	: num_docs(num_docs_), last_docid(last_docid_), did(0)
49     { }
50 
51   public:
MyOddPostingSource(const Xapian::Database & db)52     MyOddPostingSource(const Xapian::Database &db)
53 	: num_docs(db.get_doccount()), last_docid(db.get_lastdocid()), did(0)
54     { }
55 
clone() const56     PostingSource * clone() const { return new MyOddPostingSource(num_docs, last_docid); }
57 
init(const Xapian::Database &)58     void init(const Xapian::Database &) { did = 0; }
59 
60     // These bounds could be better, but that's not important here.
get_termfreq_min() const61     Xapian::doccount get_termfreq_min() const { return 0; }
62 
get_termfreq_est() const63     Xapian::doccount get_termfreq_est() const { return num_docs / 2; }
64 
get_termfreq_max() const65     Xapian::doccount get_termfreq_max() const { return num_docs; }
66 
next(double wt)67     void next(double wt) {
68 	(void)wt;
69 	++did;
70 	if (did % 2 == 0) ++did;
71     }
72 
skip_to(Xapian::docid to_did,double wt)73     void skip_to(Xapian::docid to_did, double wt) {
74 	(void)wt;
75 	did = to_did;
76 	if (did % 2 == 0) ++did;
77     }
78 
at_end() const79     bool at_end() const {
80 	// Doesn't work if last_docid is 2^32 - 1.
81 	return did > last_docid;
82     }
83 
get_docid() const84     Xapian::docid get_docid() const { return did; }
85 
get_description() const86     string get_description() const { return "MyOddPostingSource"; }
87 };
88 
89 DEFINE_TESTCASE(externalsource1, backend && !remote && !multi) {
90     // Doesn't work for remote without registering with the server.
91     // Doesn't work for multi because it checks the docid in the
92     // subdatabase.
93     Xapian::Database db(get_database("apitest_phrase"));
94     Xapian::Enquire enq(db);
95     MyOddPostingSource src(db);
96 
97     // Check that passing NULL is rejected as intended.
98     Xapian::PostingSource * nullsrc = NULL;
99     TEST_EXCEPTION(Xapian::InvalidArgumentError, Xapian::Query bad(nullsrc));
100 
101     enq.set_query(Xapian::Query(&src));
102 
103     Xapian::MSet mset = enq.get_mset(0, 10);
104     mset_expect_order(mset, 1, 3, 5, 7, 9, 11, 13, 15, 17);
105 
106     Xapian::Query q(Xapian::Query::OP_FILTER,
107 		    Xapian::Query("leav"),
108 		    Xapian::Query(&src));
109     enq.set_query(q);
110 
111     mset = enq.get_mset(0, 10);
112     mset_expect_order(mset, 5, 7, 11, 13, 9);
113 }
114 
115 // Test that trying to use PostingSource with the remote backend throws
116 // Xapian::UnimplementedError as expected (we need to register the class
117 // in xapian-tcpsrv/xapian-progsrv for this to work).
DEFINE_TESTCASE(externalsource2,remote)118 DEFINE_TESTCASE(externalsource2, remote) {
119     Xapian::Database db(get_database("apitest_phrase"));
120     Xapian::Enquire enq(db);
121     MyOddPostingSource src(db);
122 
123     enq.set_query(Xapian::Query(&src));
124 
125     TEST_EXCEPTION(Xapian::UnimplementedError,
126 		   Xapian::MSet mset = enq.get_mset(0, 10));
127 
128     Xapian::Query q(Xapian::Query::OP_FILTER,
129 		    Xapian::Query("leav"),
130 		    Xapian::Query(&src));
131     enq.set_query(q);
132 
133     TEST_EXCEPTION(Xapian::UnimplementedError,
134 		   Xapian::MSet mset = enq.get_mset(0, 10));
135 }
136 
137 class MyOddWeightingPostingSource : public Xapian::PostingSource {
138     Xapian::doccount num_docs;
139 
140     Xapian::doccount last_docid;
141 
142     Xapian::docid did;
143 
MyOddWeightingPostingSource(Xapian::doccount num_docs_,Xapian::doccount last_docid_)144     MyOddWeightingPostingSource(Xapian::doccount num_docs_,
145 				Xapian::doccount last_docid_)
146 	: num_docs(num_docs_), last_docid(last_docid_), did(0)
147     {
148 	set_maxweight(1000);
149     }
150 
151   public:
MyOddWeightingPostingSource(const Xapian::Database & db)152     MyOddWeightingPostingSource(const Xapian::Database &db)
153 	: num_docs(db.get_doccount()), last_docid(db.get_lastdocid()), did(0)
154     { }
155 
clone() const156     PostingSource * clone() const {
157 	return new MyOddWeightingPostingSource(num_docs, last_docid);
158     }
159 
init(const Xapian::Database &)160     void init(const Xapian::Database &) { did = 0; }
161 
get_weight() const162     double get_weight() const {
163 	return (did % 2) ? 1000 : 0.001;
164     }
165 
166     // These bounds could be better, but that's not important here.
get_termfreq_min() const167     Xapian::doccount get_termfreq_min() const { return 0; }
168 
get_termfreq_est() const169     Xapian::doccount get_termfreq_est() const { return num_docs / 2; }
170 
get_termfreq_max() const171     Xapian::doccount get_termfreq_max() const { return num_docs; }
172 
next(double wt)173     void next(double wt) {
174 	(void)wt;
175 	++did;
176     }
177 
skip_to(Xapian::docid to_did,double wt)178     void skip_to(Xapian::docid to_did, double wt) {
179 	(void)wt;
180 	did = to_did;
181     }
182 
at_end() const183     bool at_end() const {
184 	// Doesn't work if last_docid is 2^32 - 1.
185 	return did > last_docid;
186     }
187 
get_docid() const188     Xapian::docid get_docid() const { return did; }
189 
get_description() const190     string get_description() const {
191 	return "MyOddWeightingPostingSource";
192     }
193 };
194 
195 // Like externalsource1, except we use the weight to favour odd documents.
196 DEFINE_TESTCASE(externalsource3, backend && !remote && !multi) {
197     // Doesn't work for remote without registering with the server.
198     // Doesn't work for multi because it checks the docid in the
199     // subdatabase.
200     Xapian::Database db(get_database("apitest_phrase"));
201     Xapian::Enquire enq(db);
202     MyOddWeightingPostingSource src(db);
203 
204     enq.set_query(Xapian::Query(&src));
205 
206     Xapian::MSet mset = enq.get_mset(0, 10);
207     mset_expect_order(mset, 1, 3, 5, 7, 9, 11, 13, 15, 17, 2);
208 
209     Xapian::Query q(Xapian::Query::OP_OR,
210 		    Xapian::Query("leav"),
211 		    Xapian::Query(&src));
212     enq.set_query(q);
213 
214     mset = enq.get_mset(0, 5);
215     mset_expect_order(mset, 5, 7, 11, 13, 9);
216 
217     tout << "max possible weight = " << mset.get_max_possible() << endl;
218     TEST(mset.get_max_possible() > 1000);
219 
220     enq.set_cutoff(0, 1000.001);
221     mset = enq.get_mset(0, 10);
222     mset_expect_order(mset, 5, 7, 11, 13, 9);
223 
224     tout << "max possible weight = " << mset.get_max_possible() << endl;
225     TEST(mset.get_max_possible() > 1000);
226 
227     enq.set_query(Xapian::Query(q.OP_SCALE_WEIGHT, Xapian::Query(&src), 0.5));
228     mset = enq.get_mset(0, 10);
229     TEST(mset.empty());
230 
231     TEST_EQUAL(mset.get_max_possible(), 500);
232 
233     enq.set_query(Xapian::Query(q.OP_SCALE_WEIGHT, Xapian::Query(&src), 2));
234     mset = enq.get_mset(0, 10);
235     mset_expect_order(mset, 1, 3, 5, 7, 9, 11, 13, 15, 17);
236 
237     TEST_EQUAL(mset.get_max_possible(), 2000);
238 }
239 
240 class MyDontAskWeightPostingSource : public Xapian::PostingSource {
241     Xapian::doccount num_docs;
242 
243     Xapian::doccount last_docid;
244 
245     Xapian::docid did;
246 
MyDontAskWeightPostingSource(Xapian::doccount num_docs_,Xapian::doccount last_docid_)247     MyDontAskWeightPostingSource(Xapian::doccount num_docs_,
248 				 Xapian::doccount last_docid_)
249 	: num_docs(num_docs_), last_docid(last_docid_), did(0)
250     { }
251 
252   public:
MyDontAskWeightPostingSource()253     MyDontAskWeightPostingSource() : Xapian::PostingSource() {}
254 
clone() const255     PostingSource * clone() const { return new MyDontAskWeightPostingSource(num_docs, last_docid); }
256 
init(const Xapian::Database & db)257     void init(const Xapian::Database &db) {
258 	num_docs = db.get_doccount();
259 	last_docid = db.get_lastdocid();
260 	did = 0;
261     }
262 
get_weight() const263     double get_weight() const {
264 	FAIL_TEST("MyDontAskWeightPostingSource::get_weight() called");
265     }
266 
267     // These bounds could be better, but that's not important here.
get_termfreq_min() const268     Xapian::doccount get_termfreq_min() const { return num_docs; }
269 
get_termfreq_est() const270     Xapian::doccount get_termfreq_est() const { return num_docs; }
271 
get_termfreq_max() const272     Xapian::doccount get_termfreq_max() const { return num_docs; }
273 
next(double wt)274     void next(double wt) {
275 	(void)wt;
276 	++did;
277     }
278 
skip_to(Xapian::docid to_did,double wt)279     void skip_to(Xapian::docid to_did, double wt) {
280 	(void)wt;
281 	did = to_did;
282     }
283 
at_end() const284     bool at_end() const {
285 	// Doesn't work if last_docid is 2^32 - 1.
286 	return did > last_docid;
287     }
288 
get_docid() const289     Xapian::docid get_docid() const { return did; }
290 
get_description() const291     string get_description() const {
292 	return "MyDontAskWeightPostingSource";
293     }
294 };
295 
296 // Check that boolean use doesn't call get_weight().
297 DEFINE_TESTCASE(externalsource4, backend && !remote) {
298     Xapian::Database db(get_database("apitest_phrase"));
299     Xapian::Enquire enq(db);
300     MyDontAskWeightPostingSource src;
301 
302     tout << "OP_SCALE_WEIGHT 0" << endl;
303     enq.set_query(Xapian::Query(Xapian::Query::OP_SCALE_WEIGHT, Xapian::Query(&src), 0));
304 
305     Xapian::MSet mset = enq.get_mset(0, 5);
306     mset_expect_order(mset, 1, 2, 3, 4, 5);
307 
308     tout << "OP_FILTER" << endl;
309     Xapian::Query q(Xapian::Query::OP_FILTER,
310 		    Xapian::Query("leav"),
311 		    Xapian::Query(&src));
312     enq.set_query(q);
313 
314     mset = enq.get_mset(0, 5);
315     mset_expect_order(mset, 8, 6, 4, 5, 7);
316 
317     tout << "BoolWeight" << endl;
318     enq.set_query(Xapian::Query(&src));
319     enq.set_weighting_scheme(Xapian::BoolWeight());
320 
321     // mset = enq.get_mset(0, 5);
322     // mset_expect_order(mset, 1, 2, 3, 4, 5);
323 }
324 
325 // Check that valueweightsource works correctly.
DEFINE_TESTCASE(valueweightsource1,backend)326 DEFINE_TESTCASE(valueweightsource1, backend) {
327     Xapian::Database db(get_database("apitest_phrase"));
328     Xapian::Enquire enq(db);
329     Xapian::ValueWeightPostingSource src(11);
330 
331     // Should be in descending order of length
332     tout << "RAW" << endl;
333     enq.set_query(Xapian::Query(&src));
334     Xapian::MSet mset = enq.get_mset(0, 5);
335     mset_expect_order(mset, 3, 1, 2, 8, 14);
336 
337     // In relevance order
338     tout << "OP_FILTER" << endl;
339     Xapian::Query q(Xapian::Query::OP_FILTER,
340 		    Xapian::Query("leav"),
341 		    Xapian::Query(&src));
342     enq.set_query(q);
343     mset = enq.get_mset(0, 5);
344     mset_expect_order(mset, 8, 6, 4, 5, 7);
345 
346     // Should be in descending order of length
347     tout << "OP_FILTER other way" << endl;
348     q = Xapian::Query(Xapian::Query::OP_FILTER,
349 		      Xapian::Query(&src),
350 		      Xapian::Query("leav"));
351     enq.set_query(q);
352     mset = enq.get_mset(0, 5);
353     mset_expect_order(mset, 8, 14, 9, 13, 7);
354 }
355 
356 // Check that valueweightsource gives the correct bounds for those databases
357 // which support value statistics.
DEFINE_TESTCASE(valueweightsource2,valuestats)358 DEFINE_TESTCASE(valueweightsource2, valuestats) {
359     Xapian::Database db(get_database("apitest_phrase"));
360     Xapian::ValueWeightPostingSource src(11);
361     src.init(db);
362     TEST_EQUAL(src.get_termfreq_min(), 17);
363     TEST_EQUAL(src.get_termfreq_est(), 17);
364     TEST_EQUAL(src.get_termfreq_max(), 17);
365     TEST_EQUAL(src.get_maxweight(), 135);
366 }
367 
368 // Check that valueweightsource skip_to() can stay in the same position.
369 DEFINE_TESTCASE(valueweightsource3, valuestats && !multi) {
370     // FIXME: multi doesn't support iterating valuestreams yet.
371     Xapian::Database db(get_database("apitest_phrase"));
372     Xapian::ValueWeightPostingSource src(11);
373     src.init(db);
374     TEST(!src.at_end());
375     src.skip_to(8, 0.0);
376     TEST(!src.at_end());
377     TEST_EQUAL(src.get_docid(), 8);
378     src.skip_to(8, 0.0);
379     TEST(!src.at_end());
380     TEST_EQUAL(src.get_docid(), 8);
381 }
382 
383 // Check that fixedweightsource works correctly.
DEFINE_TESTCASE(fixedweightsource1,backend)384 DEFINE_TESTCASE(fixedweightsource1, backend) {
385     Xapian::Database db(get_database("apitest_phrase"));
386     Xapian::Enquire enq(db);
387     double wt = 5.6;
388 
389     {
390 	Xapian::FixedWeightPostingSource src(wt);
391 
392 	// Should be in increasing order of docid.
393 	enq.set_query(Xapian::Query(&src));
394 	Xapian::MSet mset = enq.get_mset(0, 5);
395 	mset_expect_order(mset, 1, 2, 3, 4, 5);
396 
397 	for (Xapian::MSetIterator i = mset.begin(); i != mset.end(); ++i) {
398 	    TEST_EQUAL(i.get_weight(), wt);
399 	}
400     }
401 
402     // Do some direct tests, to check the skip_to() and check() methods work.
403     {
404 	// Check next and skip_to().
405 	Xapian::FixedWeightPostingSource src(wt);
406 	src.init(db);
407 
408 	src.next(1.0);
409 	TEST(!src.at_end());
410 	TEST_EQUAL(src.get_docid(), 1);
411 	src.next(1.0);
412 	TEST(!src.at_end());
413 	TEST_EQUAL(src.get_docid(), 2);
414 	src.skip_to(5, 1.0);
415 	TEST(!src.at_end());
416 	TEST_EQUAL(src.get_docid(), 5);
417 	src.next(wt * 2);
418 	TEST(src.at_end());
419     }
420     {
421 	// Check check() as the first operation, followed by next.
422 	Xapian::FixedWeightPostingSource src(wt);
423 	src.init(db);
424 
425 	TEST_EQUAL(src.check(5, 1.0), true);
426 	TEST(!src.at_end());
427 	TEST_EQUAL(src.get_docid(), 5);
428 	src.next(1.0);
429 	TEST(!src.at_end());
430 	TEST_EQUAL(src.get_docid(), 6);
431     }
432     {
433 	// Check check() as the first operation, followed by skip_to().
434 	Xapian::FixedWeightPostingSource src(wt);
435 	src.init(db);
436 
437 	TEST_EQUAL(src.check(5, 1.0), true);
438 	TEST(!src.at_end());
439 	TEST_EQUAL(src.get_docid(), 5);
440 	src.skip_to(6, 1.0);
441 	TEST(!src.at_end());
442 	TEST_EQUAL(src.get_docid(), 6);
443 	src.skip_to(7, wt * 2);
444 	TEST(src.at_end());
445     }
446 }
447 
448 // A posting source which changes the maximum weight.
449 class ChangeMaxweightPostingSource : public Xapian::PostingSource {
450     Xapian::docid did;
451 
452     // Maximum docid that get_weight() should be called for.
453     Xapian::docid maxid_accessed;
454 
455   public:
ChangeMaxweightPostingSource(Xapian::docid maxid_accessed_)456     ChangeMaxweightPostingSource(Xapian::docid maxid_accessed_)
457 	    : did(0), maxid_accessed(maxid_accessed_) { }
458 
init(const Xapian::Database &)459     void init(const Xapian::Database &) { did = 0; }
460 
get_weight() const461     double get_weight() const {
462 	if (did > maxid_accessed) {
463 	    FAIL_TEST("ChangeMaxweightPostingSource::get_weight() called "
464 		      "for docid " + str(did) + ", max id accessed "
465 		      "should be " + str(maxid_accessed));
466 	}
467 	return 5 - did;
468     }
469 
get_termfreq_min() const470     Xapian::doccount get_termfreq_min() const { return 4; }
get_termfreq_est() const471     Xapian::doccount get_termfreq_est() const { return 4; }
get_termfreq_max() const472     Xapian::doccount get_termfreq_max() const { return 4; }
473 
next(double)474     void next(double) {
475 	++did;
476 	set_maxweight(5 - did);
477     }
478 
skip_to(Xapian::docid to_did,double)479     void skip_to(Xapian::docid to_did, double) {
480 	did = to_did;
481 	set_maxweight(5 - did);
482     }
483 
at_end() const484     bool at_end() const { return did >= 5; }
get_docid() const485     Xapian::docid get_docid() const { return did; }
get_description() const486     string get_description() const { return "ChangeMaxweightPostingSource"; }
487 };
488 
489 // Test a posting source with a variable maxweight.
490 DEFINE_TESTCASE(changemaxweightsource1, backend && !remote && !multi) {
491     // The ChangeMaxweightPostingSource doesn't work with multi or remote.
492     Xapian::Database db(get_database("apitest_phrase"));
493     Xapian::Enquire enq(db);
494 
495     {
496 	ChangeMaxweightPostingSource src1(5);
497 	Xapian::FixedWeightPostingSource src2(2.5);
498 
499 	Xapian::Query q(Xapian::Query::OP_AND,
500 			Xapian::Query(&src1), Xapian::Query(&src2));
501 	enq.set_query(q);
502 	// Set descending docid order so that the matcher isn't able to
503 	// terminate early after 4 documents just because weight == maxweight.
504 	enq.set_docid_order(enq.DESCENDING);
505 
506 	Xapian::MSet mset = enq.get_mset(0, 4);
507 	TEST(src1.at_end());
508 	mset_expect_order(mset, 1, 2, 3, 4);
509 	for (Xapian::MSetIterator i = mset.begin(); i != mset.end(); ++i) {
510 	    TEST_EQUAL_DOUBLE(i.get_weight(), 7.5 - *i);
511 	}
512     }
513 
514     {
515 	ChangeMaxweightPostingSource src1(3);
516 	Xapian::FixedWeightPostingSource src2(2.5);
517 
518 	Xapian::Query q(Xapian::Query::OP_AND,
519 			Xapian::Query(&src1), Xapian::Query(&src2));
520 	enq.set_query(q);
521 
522 	Xapian::MSet mset = enq.get_mset(0, 2);
523 	TEST(!src1.at_end());
524 	TEST_EQUAL(src1.get_docid(), 3);
525 	TEST_EQUAL_DOUBLE(src1.get_maxweight(), 2.0);
526 	mset_expect_order(mset, 1, 2);
527 	for (Xapian::MSetIterator i = mset.begin(); i != mset.end(); ++i) {
528 	    TEST_EQUAL_DOUBLE(i.get_weight(), 7.5 - *i);
529 	}
530     }
531 }
532 
533 // Test using a valueweightpostingsource which has no entries.
534 DEFINE_TESTCASE(emptyvalwtsource1, backend && !remote && !multi) {
535     Xapian::Database db(get_database("apitest_phrase"));
536     Xapian::Enquire enq(db);
537 
538     Xapian::ValueWeightPostingSource src2(11); // A non-empty slot.
539     Xapian::ValueWeightPostingSource src3(100); // An empty slot.
540     Xapian::Query q1("leav");
541     Xapian::Query q2(&src2);
542     Xapian::Query q3(&src3);
543     Xapian::Query q(Xapian::Query::OP_OR, Xapian::Query(Xapian::Query::OP_AND_MAYBE, q1, q2), q3);
544 
545     // Perform search without ORring with the posting source.
546     Xapian::doccount size1;
547     {
548 	enq.set_query(q1);
549 	Xapian::MSet mset = enq.get_mset(0, 10);
550 	TEST_REL(mset.get_max_possible(), >, 0.0);
551 	size1 = mset.size();
552 	TEST_REL(size1, >, 0);
553     }
554 
555     // Perform a search with just the non-empty posting source, checking it
556     // returns something.
557     {
558 	enq.set_query(q2);
559 	Xapian::MSet mset = enq.get_mset(0, 10);
560 	TEST_REL(mset.get_max_possible(), >, 0.0);
561 	TEST_REL(mset.size(), >, 0);
562     }
563 
564     // Perform a search with just the empty posting source, checking it returns
565     // nothing.
566     {
567 	enq.set_query(q3);
568 	Xapian::MSet mset = enq.get_mset(0, 10);
569 
570 	// get_max_possible() returns 0 here for backends which track the upper
571 	// bound on value slot entries, MAX_DBL for backends which don't.
572 	// Either is valid.
573 	TEST_REL(mset.get_max_possible(), >=, 0.0);
574 
575 	TEST_EQUAL(mset.size(), 0);
576     }
577 
578     // Perform a search with the posting source ORred with the normal query.
579     // This is a regression test - it used to return nothing.
580     {
581 	enq.set_query(q);
582 	Xapian::MSet mset = enq.get_mset(0, 10);
583 	TEST_REL(mset.get_max_possible(), >, 0.0);
584 	TEST_REL(mset.size(), >, 0.0);
585 	TEST_EQUAL(mset.size(), size1);
586     }
587 }
588 
589 class SlowDecreasingValueWeightPostingSource
590     : public Xapian::DecreasingValueWeightPostingSource {
591   public:
592     int & count;
593 
SlowDecreasingValueWeightPostingSource(int & count_)594     SlowDecreasingValueWeightPostingSource(int & count_)
595 	: Xapian::DecreasingValueWeightPostingSource(0), count(count_) { }
596 
clone() const597     SlowDecreasingValueWeightPostingSource * clone() const
598     {
599 	return new SlowDecreasingValueWeightPostingSource(count);
600     }
601 
next(double min_wt)602     void next(double min_wt) {
603 	sleep(1);
604 	++count;
605 	return Xapian::DecreasingValueWeightPostingSource::next(min_wt);
606     }
607 };
608 
609 static void
make_matchtimelimit1_db(Xapian::WritableDatabase & db,const string &)610 make_matchtimelimit1_db(Xapian::WritableDatabase &db, const string &)
611 {
612     for (int wt = 20; wt > 0; --wt) {
613 	Xapian::Document doc;
614 	doc.add_value(0, Xapian::sortable_serialise(double(wt)));
615 	db.add_document(doc);
616     }
617 }
618 
619 // FIXME: This doesn't run for remote databases (we'd need to register
620 // SlowDecreasingValueWeightPostingSource on the remote).
621 DEFINE_TESTCASE(matchtimelimit1, generated && !remote)
622 {
623 #ifndef HAVE_TIMER_CREATE
624     SKIP_TEST("Enquire::set_time_limit() not implemented for this platform");
625 #endif
626     Xapian::Database db = get_database("matchtimelimit1",
627 				       make_matchtimelimit1_db);
628 
629     int count = 0;
630     SlowDecreasingValueWeightPostingSource src(count);
631     src.init(db);
632     Xapian::Enquire enquire(db);
633     enquire.set_query(Xapian::Query(&src));
634 
635     enquire.set_time_limit(1.5);
636 
637     Xapian::MSet mset = enquire.get_mset(0, 1, 1000);
638     TEST_EQUAL(mset.size(), 1);
639     TEST_EQUAL(count, 2);
640 }
641 
642 class CheckBoundsPostingSource
643     : public Xapian::DecreasingValueWeightPostingSource {
644   public:
645     Xapian::doccount& doclen_lb;
646 
647     Xapian::doccount& doclen_ub;
648 
CheckBoundsPostingSource(Xapian::doccount & doclen_lb_,Xapian::doccount & doclen_ub_)649     CheckBoundsPostingSource(Xapian::doccount& doclen_lb_,
650 			     Xapian::doccount& doclen_ub_)
651 	: Xapian::DecreasingValueWeightPostingSource(0),
652 	  doclen_lb(doclen_lb_),
653 	  doclen_ub(doclen_ub_) { }
654 
clone() const655     CheckBoundsPostingSource * clone() const
656     {
657 	return new CheckBoundsPostingSource(doclen_lb, doclen_ub);
658     }
659 
init(const Xapian::Database & database)660     void init(const Xapian::Database& database) {
661 	doclen_lb = database.get_doclength_lower_bound();
662 	doclen_ub = database.get_doclength_upper_bound();
663 	Xapian::DecreasingValueWeightPostingSource::init(database);
664     }
665 };
666 
667 // Test that doclength bounds are correct.
668 // Regression test for bug fixed in 1.2.25 and 1.4.1.
669 DEFINE_TESTCASE(postingsourcebounds1, backend && !remote)
670 {
671     Xapian::Database db = get_database("apitest_simpledata");
672 
673     Xapian::doccount doclen_lb = 0, doclen_ub = 0;
674     CheckBoundsPostingSource ps(doclen_lb, doclen_ub);
675 
676     Xapian::Enquire enquire(db);
677     enquire.set_query(Xapian::Query(&ps));
678 
679     Xapian::MSet mset = enquire.get_mset(0, 1);
680 
681     TEST_EQUAL(doclen_lb, db.get_doclength_lower_bound());
682     TEST_EQUAL(doclen_ub, db.get_doclength_upper_bound());
683 }
684 
685 // PostingSource which really just counts the clone() calls.
686 // Never actually matches anything, but pretends it might.
687 class CloneTestPostingSource : public Xapian::PostingSource {
688     int& clone_count;
689 
690   public:
CloneTestPostingSource(int & clone_count_)691     CloneTestPostingSource(int& clone_count_)
692 	: clone_count(clone_count_)
693     { }
694 
clone() const695     PostingSource * clone() const {
696 	++clone_count;
697 	return new CloneTestPostingSource(clone_count);
698     }
699 
init(const Xapian::Database &)700     void init(const Xapian::Database&) { }
701 
get_termfreq_min() const702     Xapian::doccount get_termfreq_min() const { return 0; }
703 
get_termfreq_est() const704     Xapian::doccount get_termfreq_est() const { return 1; }
705 
get_termfreq_max() const706     Xapian::doccount get_termfreq_max() const { return 2; }
707 
next(double)708     void next(double) { }
709 
skip_to(Xapian::docid,double)710     void skip_to(Xapian::docid, double) { }
711 
at_end() const712     bool at_end() const {
713 	return true;
714     }
715 
get_docid() const716     Xapian::docid get_docid() const { return 0; }
717 
get_description() const718     string get_description() const { return "CloneTestPostingSource"; }
719 };
720 
721 /// Test cloning of initial object, which regressed in 1.3.5.
722 DEFINE_TESTCASE(postingsourceclone1, !backend)
723 {
724     // This fails with 1.3.5-1.4.0 inclusive.
725     {
726 	int clones = 0;
727 	CloneTestPostingSource ps(clones);
728 	TEST_EQUAL(clones, 0);
729 	Xapian::Query q(&ps);
730 	TEST_EQUAL(clones, 1);
731     }
732 
733     // Check that clone() isn't needlessly called if reference counting has
734     // been turned on for the PostingSource.
735     {
736 	int clones = 0;
737 	CloneTestPostingSource* ps = new CloneTestPostingSource(clones);
738 	TEST_EQUAL(clones, 0);
739 	Xapian::Query q(ps->release());
740 	TEST_EQUAL(clones, 0);
741     }
742 }
743 
744 class OnlyTheFirstPostingSource : public Xapian::PostingSource {
745     Xapian::doccount last_docid;
746 
747     Xapian::docid did;
748 
749     bool allow_clone;
750 
751   public:
752     static Xapian::doccount shard_index;
753 
754     explicit
OnlyTheFirstPostingSource(bool allow_clone_)755     OnlyTheFirstPostingSource(bool allow_clone_) : allow_clone(allow_clone_) {}
756 
clone() const757     PostingSource* clone() const {
758 	return allow_clone ? new OnlyTheFirstPostingSource(true) : nullptr;
759     }
760 
init(const Xapian::Database & db)761     void init(const Xapian::Database& db) {
762 	did = 0;
763 	if (shard_index == 0) {
764 	    last_docid = db.get_lastdocid();
765 	} else {
766 	    last_docid = 0;
767 	}
768 	++shard_index;
769     }
770 
get_termfreq_min() const771     Xapian::doccount get_termfreq_min() const { return 0; }
772 
get_termfreq_est() const773     Xapian::doccount get_termfreq_est() const { return last_docid / 2; }
774 
get_termfreq_max() const775     Xapian::doccount get_termfreq_max() const { return last_docid; }
776 
next(double wt)777     void next(double wt) {
778 	(void)wt;
779 	++did;
780 	if (did > last_docid) did = 0;
781     }
782 
skip_to(Xapian::docid to_did,double wt)783     void skip_to(Xapian::docid to_did, double wt) {
784 	(void)wt;
785 	did = to_did;
786 	if (did > last_docid) did = 0;
787     }
788 
at_end() const789     bool at_end() const {
790 	return did == 0;
791     }
792 
get_docid() const793     Xapian::docid get_docid() const { return did; }
794 
get_description() const795     string get_description() const { return "OnlyTheFirstPostingSource"; }
796 };
797 
798 Xapian::doccount OnlyTheFirstPostingSource::shard_index;
799 
800 DEFINE_TESTCASE(postingsourceshardindex1, multi && !remote) {
801     Xapian::Database db = get_database("apitest_simpledata");
802 
803     OnlyTheFirstPostingSource::shard_index = 0;
804 
805     Xapian::Enquire enquire(db);
806     {
807 	auto ps = new OnlyTheFirstPostingSource(true);
808 	enquire.set_query(Xapian::Query(ps->release()));
809 
810 	Xapian::MSet mset = enquire.get_mset(0, 10);
811 	mset_expect_order(mset, 1, 3, 5);
812     }
813 
814     {
815 	/* Regression test for bug fixed in 1.4.12 - we should get an exception
816 	 * if we use a PostingSource that doesn't support clone() with a multi
817 	 * DB.
818 	 */
819 	auto ps = new OnlyTheFirstPostingSource(false);
820 	enquire.set_query(Xapian::Query(ps->release()));
821 
822 	TEST_EXCEPTION(Xapian::InvalidOperationError,
823 		       auto m = enquire.get_mset(0, 10));
824     }
825 }
826 
827 /// PostingSource subclass for injecting tf bounds and estimate.
828 class EstimatePS : public Xapian::PostingSource {
829     Xapian::doccount lb, est, ub;
830 
831   public:
EstimatePS(Xapian::doccount lb_,Xapian::doccount est_,Xapian::doccount ub_)832     EstimatePS(Xapian::doccount lb_,
833 	       Xapian::doccount est_,
834 	       Xapian::doccount ub_)
835 	: lb(lb_), est(est_), ub(ub_)
836     { }
837 
clone() const838     PostingSource * clone() const { return new EstimatePS(lb, est, ub); }
839 
init(const Xapian::Database &)840     void init(const Xapian::Database &) { }
841 
get_termfreq_min() const842     Xapian::doccount get_termfreq_min() const { return lb; }
843 
get_termfreq_est() const844     Xapian::doccount get_termfreq_est() const { return est; }
845 
get_termfreq_max() const846     Xapian::doccount get_termfreq_max() const { return ub; }
847 
next(double)848     void next(double) {
849 	FAIL_TEST("EstimatePS::next() shouldn't be called");
850     }
851 
skip_to(Xapian::docid,double)852     void skip_to(Xapian::docid, double) {
853 	FAIL_TEST("EstimatePS::skip_to() shouldn't be called");
854     }
855 
at_end() const856     bool at_end() const {
857 	return false;
858     }
859 
get_docid() const860     Xapian::docid get_docid() const {
861 	FAIL_TEST("EstimatePS::get_docid() shouldn't be called");
862     }
863 
get_description() const864     string get_description() const { return "EstimatePS"; }
865 };
866 
867 /// Check estimate is rounded to suitable number of S.F. - new in 1.4.3.
868 DEFINE_TESTCASE(estimaterounding1, backend && !multi && !remote) {
869     Xapian::Database db = get_database("etext");
870     Xapian::Enquire enquire(db);
871     static const struct { Xapian::doccount lb, est, ub, exp; } testcases[] = {
872 	// Test rounding down.
873 	{411, 424, 439, 420},
874 	{1, 312, 439, 300},
875 	// Test rounding up.
876 	{411, 426, 439, 430},
877 	{123, 351, 439, 400},
878 	// Rounding based on estimate size if smaller than range size.
879 	{1, 12, 439, 10},
880 	// Round "5" away from the nearer bound.
881 	{1, 15, 439, 20},
882 	{1, 350, 439, 300},
883 	// Check we round up if rounding down would be out of range.
884 	{411, 416, 439, 420},
885 	{411, 412, 439, 420},
886 	// Check we round down if rounding up would be out of range.
887 	{111, 133, 138, 130},
888 	{111, 137, 138, 130},
889 	// Check we don't round if either way would be out of range.
890 	{411, 415, 419, 415},
891 	// Leave small estimates alone.
892 	{1, 6, 439, 6},
893     };
894     for (auto& t : testcases) {
895 	EstimatePS ps(t.lb, t.est, t.ub);
896 	enquire.set_query(Xapian::Query(&ps));
897 	Xapian::MSet mset = enquire.get_mset(0, 0);
898 	// MSet::get_description() includes bounds and raw estimate.
899 	tout << mset.get_description() << endl;
900 	TEST_EQUAL(mset.get_matches_estimated(), t.exp);
901     }
902 }
903