1<?php
2// Run this PHP script using 'make check' in the build tree.
3
4/* Simple test to ensure that we can load the xapian module and exercise basic
5 * functionality successfully.
6 *
7 * Copyright (C) 2004,2005,2006,2007,2009,2011,2012,2013,2014,2015,2016,2017 Olly Betts
8 * Copyright (C) 2010 Richard Boulton
9 *
10 * This program is free software; you can redistribute it and/or
11 * modify it under the terms of the GNU General Public License as
12 * published by the Free Software Foundation; either version 2 of the
13 * License, or (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
23 * USA
24 */
25
26# Die on any error, warning, notice, etc.
27function die_on_error($errno, $errstr, $file, $line) {
28    if ($file !== Null) {
29	print $file;
30	if ($line !== Null) print ":$line";
31	print ": ";
32    }
33    print "$errstr\n";
34    exit(1);
35}
36set_error_handler("die_on_error", -1);
37
38include "xapian.php";
39
40# Test the version number reporting functions give plausible results.
41$v = Xapian::major_version().'.'.Xapian::minor_version().'.'.Xapian::revision();
42$v2 = Xapian::version_string();
43if ($v != $v2) {
44    print "Unexpected version output ($v != $v2)\n";
45    exit(1);
46}
47
48$db = new XapianWritableDatabase('', Xapian::DB_BACKEND_INMEMORY);
49$db2 = new XapianWritableDatabase('', Xapian::DB_BACKEND_INMEMORY);
50
51# Check PHP5 handling of Xapian::DocNotFoundError
52try {
53    $doc2 = $db->get_document(2);
54    print "Retrieved non-existent document\n";
55    exit(1);
56} catch (Exception $e) {
57    if ($e->getMessage() !== "DocNotFoundError: Docid 2 not found") {
58	print "DocNotFoundError Exception string not as expected, got: '{$e->getMessage()}'\n";
59	exit(1);
60    }
61}
62
63# Check QueryParser parsing error.
64try {
65    $qp = new XapianQueryParser;
66    $qp->set_stemmer(new XapianStem("en"));
67    $qp->parse_query("test AND");
68    print "Successfully parsed bad query\n";
69    exit(1);
70} catch (Exception $e) {
71    if ($e->getMessage() !== "QueryParserError: Syntax: <expression> AND <expression>") {
72	print "QueryParserError Exception string not as expected, got: '$e->getMessage()'\n";
73	exit(1);
74    }
75}
76
77# Check that open_stub() is wrapped as expected.
78try {
79    $db = Xapian::auto_open_stub("nosuchdir/nosuchdb");
80    print "Opened non-existent stub database\n";
81    exit(1);
82} catch (Exception $e) {
83    if ($e->getMessage() !== "DatabaseNotFoundError: Couldn't open stub database file: nosuchdir/nosuchdb (No such file or directory)") {
84	print "DatabaseOpeningError Exception string not as expected, got: '{$e->getMessage()}'\n";
85	exit(1);
86    }
87}
88
89# Check that DB_BACKEND_STUB works as expected.
90try {
91    $db = new XapianDatabase("nosuchdir/nosuchdb", Xapian::DB_BACKEND_STUB);
92    print "Opened non-existent stub database\n";
93    exit(1);
94} catch (Exception $e) {
95    if ($e->getMessage() !== "DatabaseNotFoundError: Couldn't open stub database file: nosuchdir/nosuchdb (No such file or directory)") {
96	print "DatabaseNotFoundError Exception string not as expected, got: '{$e->getMessage()}'\n";
97	exit(1);
98    }
99}
100
101# Check that open_stub() writable form is wrapped as expected.
102try {
103    $db = Xapian::auto_open_stub("nosuchdir/nosuchdb", Xapian::DB_OPEN);
104    print "Opened non-existent stub database\n";
105    exit(1);
106} catch (Exception $e) {
107    if ($e->getMessage() !== "DatabaseNotFoundError: Couldn't open stub database file: nosuchdir/nosuchdb (No such file or directory)") {
108	print "DatabaseOpeningError Exception string not as expected, got: '{$e->getMessage()}'\n";
109	exit(1);
110    }
111}
112
113# Check that DB_BACKEND_STUB works as expected.
114try {
115    $db = new XapianWritableDatabase("nosuchdir/nosuchdb",
116				     Xapian::DB_OPEN|Xapian::DB_BACKEND_STUB);
117    print "Opened non-existent stub database\n";
118    exit(1);
119} catch (Exception $e) {
120    if ($e->getMessage() !== "DatabaseNotFoundError: Couldn't open stub database file: nosuchdir/nosuchdb (No such file or directory)") {
121	print "DatabaseNotFoundError Exception string not as expected, got: '{$e->getMessage()}'\n";
122	exit(1);
123    }
124}
125
126# Regression test for bug#193, fixed in 1.0.3.
127$vrp = new XapianNumberValueRangeProcessor(0, '$', true);
128$a = '$10';
129$b = '20';
130$vrp->apply($a, $b);
131if (Xapian::sortable_unserialise($a) != 10) {
132    print Xapian::sortable_unserialise($a)." != 10\n";
133    exit(1);
134}
135if (Xapian::sortable_unserialise($b) != 20) {
136    print Xapian::sortable_unserialise($b)." != 20\n";
137    exit(1);
138}
139
140$stem = new XapianStem("english");
141if ($stem->get_description() != "Xapian::Stem(english)") {
142    print "Unexpected \$stem->get_description()\n";
143    exit(1);
144}
145
146$doc = new XapianDocument();
147$doc->set_data("a\x00b");
148if ($doc->get_data() === "a") {
149    print "get_data+set_data truncates at a zero byte\n";
150    exit(1);
151}
152if ($doc->get_data() !== "a\x00b") {
153    print "get_data+set_data doesn't transparently handle a zero byte\n";
154    exit(1);
155}
156$doc->set_data("is there anybody out there?");
157$doc->add_term("XYzzy");
158$doc->add_posting($stem->apply("is"), 1);
159$doc->add_posting($stem->apply("there"), 2);
160$doc->add_posting($stem->apply("anybody"), 3);
161$doc->add_posting($stem->apply("out"), 4);
162$doc->add_posting($stem->apply("there"), 5);
163
164// Check virtual function dispatch.
165if (substr($db->get_description(), 0, 17) !== "WritableDatabase(") {
166    print "Unexpected \$db->get_description()\n";
167    exit(1);
168}
169$db->add_document($doc);
170if ($db->get_doccount() != 1) {
171    print "Unexpected \$db->get_doccount()\n";
172    exit(1);
173}
174
175$terms = array("smoke", "test", "terms");
176$query = new XapianQuery(XapianQuery::OP_OR, $terms);
177if ($query->get_description() != "Query((smoke OR test OR terms))") {
178    print "Unexpected \$query->get_description()\n";
179    exit(1);
180}
181$query1 = new XapianQuery(XapianQuery::OP_PHRASE, array("smoke", "test", "tuple"));
182if ($query1->get_description() != "Query((smoke PHRASE 3 test PHRASE 3 tuple))") {
183    print "Unexpected \$query1->get_description()\n";
184    exit(1);
185}
186$query1b = new XapianQuery(XapianQuery::OP_NEAR, array("smoke", "test", "tuple"), 4);
187if ($query1b->get_description() != "Query((smoke NEAR 4 test NEAR 4 tuple))") {
188    print "Unexpected \$query1b->get_description()\n";
189    exit(1);
190}
191$query2 = new XapianQuery(XapianQuery::OP_XOR, array(new XapianQuery("smoke"), $query1, "string"));
192if ($query2->get_description() != "Query((smoke XOR (smoke PHRASE 3 test PHRASE 3 tuple) XOR string))") {
193    print "Unexpected \$query2->get_description()\n";
194    exit(1);
195}
196$subqs = array("a", "b");
197$query3 = new XapianQuery(XapianQuery::OP_OR, $subqs);
198if ($query3->get_description() != "Query((a OR b))") {
199    print "Unexpected \$query3->get_description()\n";
200    exit(1);
201}
202$enq = new XapianEnquire($db);
203
204// This ought to be wrapped as a constant, but this tests how it has been
205// wrapped for some time in PHP bindings, which we need to maintain for
206// compatibility with existing user code.
207$enq->set_collapse_key(Xapian::BAD_VALUENO_get());
208
209$enq->set_query(new XapianQuery(XapianQuery::OP_OR, "there", "is"));
210$mset = $enq->get_mset(0, 10);
211if ($mset->size() != 1) {
212    print "Unexpected \$mset->size()\n";
213    exit(1);
214}
215$terms = join(" ", $enq->get_matching_terms($mset->get_hit(0)));
216if ($terms != "is there") {
217    print "Unexpected matching terms: $terms\n";
218    exit(1);
219}
220
221# Feature test for MatchDecider
222$doc = new XapianDocument();
223$doc->set_data("Two");
224$doc->add_posting($stem->apply("out"), 1);
225$doc->add_posting($stem->apply("outside"), 1);
226$doc->add_posting($stem->apply("source"), 2);
227$doc->add_value(0, "yes");
228$db->add_document($doc);
229
230class testmatchdecider extends XapianMatchDecider {
231    function apply($doc) {
232	return ($doc->get_value(0) == "yes");
233    }
234}
235
236$query = new XapianQuery($stem->apply("out"));
237$enquire = new XapianEnquire($db);
238$enquire->set_query($query);
239$mdecider = new testmatchdecider();
240$mset = $enquire->get_mset(0, 10, null, $mdecider);
241if ($mset->size() != 1) {
242    print "Unexpected number of documents returned by match decider (".$mset->size().")\n";
243    exit(1);
244}
245if ($mset->get_docid(0) != 2) {
246    print "MatchDecider mset has wrong docid in\n";
247    exit(1);
248}
249
250class testexpanddecider extends XapianExpandDecider {
251    function apply($term) {
252	return ($term[0] !== 'a');
253    }
254}
255
256$enquire = new XapianEnquire($db);
257$rset = new XapianRSet();
258$rset->add_document(1);
259$eset = $enquire->get_eset(10, $rset, XapianEnquire::USE_EXACT_TERMFREQ, 1.0, new testexpanddecider());
260foreach ($eset->begin() as $t) {
261    if ($t[0] === 'a') {
262	print "XapianExpandDecider was not used\n";
263	exit(1);
264    }
265}
266
267# Check min_wt argument to get_eset() works (new in 1.2.5).
268$eset = $enquire->get_eset(100, $rset, XapianEnquire::USE_EXACT_TERMFREQ);
269$min_wt = 0;
270foreach ($eset->begin() as $i => $dummy) {
271    $min_wt = $i->get_weight();
272}
273if ($min_wt >= 1.9) {
274    print "ESet min weight too high for testcase\n";
275    exit(1);
276}
277$eset = $enquire->get_eset(100, $rset, XapianEnquire::USE_EXACT_TERMFREQ, 1.0, NULL, 1.9);
278$min_wt = 0;
279foreach ($eset->begin() as $i => $dummy) {
280    $min_wt = $i->get_weight();
281}
282if ($min_wt < 1.9) {
283    print "ESet min_wt threshold not applied\n";
284    exit(1);
285}
286
287if (XapianQuery::OP_ELITE_SET != 10) {
288    print "OP_ELITE_SET is XapianQuery::OP_ELITE_SET not 10\n";
289    exit(1);
290}
291
292# Regression test - overload resolution involving boolean types failed.
293$enq->set_sort_by_value(1, TRUE);
294
295# Regression test - fixed in 0.9.10.1.
296$oqparser = new XapianQueryParser();
297$oquery = $oqparser->parse_query("I like tea");
298
299# Regression test for bug#192 - fixed in 1.0.3.
300$enq->set_cutoff(100);
301
302# Check DateRangeProcessor works.
303function add_rp_date(&$qp) {
304    $rpdate = new XapianDateRangeProcessor(1, Xapian::RP_DATE_PREFER_MDY, 1960);
305    $qp->add_rangeprocessor($rpdate);
306}
307$qp = new XapianQueryParser();
308add_rp_date($qp);
309$query = $qp->parse_query('12/03/99..12/04/01');
310if ($query->get_description() !== 'Query(VALUE_RANGE 1 19991203 20011204)') {
311    print "XapianDateRangeProcessor didn't work - result was ".$query->get_description()."\n";
312    exit(1);
313}
314
315# Check DateValueRangeProcessor works.
316function add_vrp_date(&$qp) {
317    $vrpdate = new XapianDateValueRangeProcessor(1, 1, 1960);
318    $qp->add_valuerangeprocessor($vrpdate);
319}
320$qp = new XapianQueryParser();
321add_vrp_date($qp);
322$query = $qp->parse_query('12/03/99..12/04/01');
323if ($query->get_description() !== 'Query(VALUE_RANGE 1 19991203 20011204)') {
324    print "XapianDateValueRangeProcessor didn't work - result was ".$query->get_description()."\n";
325    exit(1);
326}
327
328# Feature test for XapianFieldProcessor
329class testfieldprocessor extends XapianFieldProcessor {
330    function apply($str) {
331	if ($str === 'spam') throw new Exception('already spam');
332	return new XapianQuery("spam");
333    }
334}
335
336$tfp = new testfieldprocessor();
337$qp->add_prefix('spam', $tfp);
338$query = $qp->parse_query('spam:ignored');
339if ($query->get_description() !== 'Query(spam)') {
340    print "testfieldprocessor didn't work - result was ".$query->get_description()."\n";
341    exit(1);
342}
343
344try {
345    $query = $qp->parse_query('spam:spam');
346    print "testfieldprocessor exception not rethrown\n";
347    exit(1);
348} catch (Exception $e) {
349    if ($e->getMessage() !== 'already spam') {
350	print "Exception has wrong message\n";
351	exit(1);
352    }
353}
354
355# Test setting and getting metadata
356if ($db->get_metadata('Foo') !== '') {
357    print "Unexpected value for metadata associated with 'Foo' (expected ''): '".$db->get_metadata('Foo')."'\n";
358    exit(1);
359}
360$db->set_metadata('Foo', 'Foo');
361if ($db->get_metadata('Foo') !== 'Foo') {
362    print "Unexpected value for metadata associated with 'Foo' (expected 'Foo'): '".$db->get_metadata('Foo')."'\n";
363    exit(1);
364}
365
366# Test OP_SCALE_WEIGHT and corresponding constructor
367$query4 = new XapianQuery(XapianQuery::OP_SCALE_WEIGHT, new XapianQuery('foo'), 5.0);
368if ($query4->get_description() != "Query(5 * foo)") {
369    print "Unexpected \$query4->get_description()\n";
370    exit(1);
371}
372
373# Test MultiValueKeyMaker.
374
375$doc = new XapianDocument();
376$doc->add_term("foo");
377$doc->add_value(0, "ABB");
378$db2->add_document($doc);
379$doc->add_value(0, "ABC");
380$db2->add_document($doc);
381$doc->add_value(0, "ABC\0");
382$db2->add_document($doc);
383$doc->add_value(0, "ABCD");
384$db2->add_document($doc);
385$doc->add_value(0, "ABC\xff");
386$db2->add_document($doc);
387
388$enquire = new XapianEnquire($db2);
389$enquire->set_query(new XapianQuery("foo"));
390
391{
392    $sorter = new XapianMultiValueKeyMaker();
393    $sorter->add_value(0);
394    $enquire->set_sort_by_key($sorter, true);
395    $mset = $enquire->get_mset(0, 10);
396    mset_expect_order($mset, array(5, 4, 3, 2, 1));
397}
398
399{
400    $sorter = new XapianMultiValueKeyMaker();
401    $sorter->add_value(0, true);
402    $enquire->set_sort_by_key($sorter, true);
403    $mset = $enquire->get_mset(0, 10);
404    mset_expect_order($mset, array(1, 2, 3, 4, 5));
405}
406
407{
408    $sorter = new XapianMultiValueKeyMaker();
409    $sorter->add_value(0);
410    $sorter->add_value(1);
411    $enquire->set_sort_by_key($sorter, true);
412    $mset = $enquire->get_mset(0, 10);
413    mset_expect_order($mset, array(5, 4, 3, 2, 1));
414}
415
416{
417    $sorter = new XapianMultiValueKeyMaker();
418    $sorter->add_value(0, true);
419    $sorter->add_value(1);
420    $enquire->set_sort_by_key($sorter, true);
421    $mset = $enquire->get_mset(0, 10);
422    mset_expect_order($mset, array(1, 2, 3, 4, 5));
423}
424
425{
426    $sorter = new XapianMultiValueKeyMaker();
427    $sorter->add_value(0);
428    $sorter->add_value(1, true);
429    $enquire->set_sort_by_key($sorter, true);
430    $mset = $enquire->get_mset(0, 10);
431    mset_expect_order($mset, array(5, 4, 3, 2, 1));
432}
433
434{
435    $sorter = new XapianMultiValueKeyMaker();
436    $sorter->add_value(0, true);
437    $sorter->add_value(1, true);
438    $enquire->set_sort_by_key($sorter, true);
439    $mset = $enquire->get_mset(0, 10);
440    mset_expect_order($mset, array(1, 2, 3, 4, 5));
441}
442
443# Feature test for ValueSetMatchDecider:
444{
445    $md = new XapianValueSetMatchDecider(0, true);
446    $md->add_value("ABC");
447    $doc = new XapianDocument();
448    $doc->add_value(0, "ABCD");
449    if ($md->apply($doc)) {
450	print "Unexpected result from ValueSetMatchDecider->apply(); expected false\n";
451	exit(1);
452    }
453
454    $doc = new XapianDocument();
455    $doc->add_value(0, "ABC");
456    if (!$md->apply($doc)) {
457	print "Unexpected result from ValueSetMatchDecider->apply(); expected true\n";
458	exit(1);
459    }
460
461    $mset = $enquire->get_mset(0, 10, 0, null, $md, null);
462    mset_expect_order($mset, array(2));
463
464    $md = new XapianValueSetMatchDecider(0, false);
465    $md->add_value("ABC");
466    $mset = $enquire->get_mset(0, 10, 0, null, $md, null);
467    mset_expect_order($mset, array(1, 3, 4, 5));
468}
469
470function mset_expect_order($mset, $a) {
471    if ($mset->size() != sizeof($a)) {
472	print "MSet has ".$mset->size()." entries, expected ".sizeof($a)."\n";
473	exit(1);
474    }
475    for ($j = 0; $j < sizeof($a); ++$j) {
476	$docid = $mset->get_hit($j)->get_docid();
477	if ($docid != $a[$j]) {
478	    print "Expected MSet[$j] to be $a[$j], got ".$docid()."\n";
479	    exit(1);
480	}
481    }
482}
483
484# Feature tests for Query "term" constructor optional arguments:
485$query_wqf = new XapianQuery('wqf', 3);
486if ($query_wqf->get_description() != 'Query(wqf#3)') {
487    print "Unexpected \$query_wqf->get_description():\n";
488    print $query_wqf->get_description() . "\n";
489    exit(1);
490}
491
492$query = new XapianQuery(XapianQuery::OP_VALUE_GE, 0, "100");
493if ($query->get_description() != 'Query(VALUE_GE 0 100)') {
494    print "Unexpected \$query->get_description():\n";
495    print $query->get_description() . "\n";
496    exit(1);
497}
498
499$query = XapianQuery::MatchAll();
500if ($query->get_description() != 'Query(<alldocuments>)') {
501    print "Unexpected \$query->get_description():\n";
502    print $query->get_description() . "\n";
503    exit(1);
504}
505
506$query = XapianQuery::MatchNothing();
507if ($query->get_description() != 'Query()') {
508    print "Unexpected \$query->get_description():\n";
509    print $query->get_description() . "\n";
510    exit(1);
511}
512
513# Test access to matchspy values:
514{
515    $matchspy = new XapianValueCountMatchSpy(0);
516    $enquire->add_matchspy($matchspy);
517    $enquire->get_mset(0, 10);
518    $values = array();
519    foreach ($matchspy->values_begin() as $k => $term) {
520	$values[$term] = $k->get_termfreq();
521    }
522    $expected = array(
523        "ABB" => 1,
524	"ABC" => 1,
525	"ABC\0" => 1,
526	"ABCD" => 1,
527	"ABC\xff" => 1,
528    );
529    if ($values != $expected) {
530        print "Unexpected matchspy values():\n";
531	var_dump($values);
532	var_dump($expected);
533	print "\n";
534	exit(1);
535    }
536}
537
538{
539    class testspy extends XapianMatchSpy {
540	public $matchspy_count = 0;
541
542	function apply($doc, $wt) {
543	    if (substr($doc->get_value(0), 0, 3) == "ABC") ++$this->matchspy_count;
544	}
545    }
546
547    $matchspy = new testspy();
548    $enquire->clear_matchspies();
549    $enquire->add_matchspy($matchspy);
550    $enquire->get_mset(0, 10);
551    if ($matchspy->matchspy_count != 4) {
552	print "Unexpected matchspy count of {$matchspy->matchspy_count}\n";
553	exit(1);
554    }
555}
556
557# Regression test for SWIG bug - it was generating "return $r;" in wrapper
558# functions which didn't set $r.
559$indexer = new XapianTermGenerator();
560$doc = new XapianDocument();
561
562$indexer->set_document($doc);
563$indexer->index_text("I ask nothing in return");
564$indexer->index_text_without_positions("Tea time");
565$indexer->index_text("Return in time");
566
567$s = '';
568foreach ($doc->termlist_begin() as $term) {
569    $s .= $term . ' ';
570}
571if ($s !== 'ask i in nothing return tea time ') {
572    print "PHP Iterator wrapping of TermIterator doesn't work ($s)\n";
573    exit(1);
574}
575
576$s = '';
577foreach ($doc->termlist_begin() as $k => $term) {
578    $s .= $term . ':' . $k->get_wdf() . ' ';
579}
580if ($s !== 'ask:1 i:1 in:2 nothing:1 return:2 tea:1 time:2 ') {
581    print "PHP Iterator wrapping of TermIterator keys doesn't work ($s)\n";
582    exit(1);
583}
584
585# Test GeoSpatial API
586$coord = new XapianLatLongCoord();
587$coord = new XapianLatLongCoord(-41.288889, 174.777222);
588
589define('COORD_SLOT', 2);
590$metric = new XapianGreatCircleMetric();
591$range = 42.0;
592
593$centre = new XapianLatLongCoords($coord);
594$query = new XapianQuery(new XapianLatLongDistancePostingSource(COORD_SLOT, $centre, $metric, $range));
595
596$db = new XapianWritableDatabase('', Xapian::DB_BACKEND_INMEMORY);
597$coords = new XapianLatLongCoords();
598$coords->append(new XapianLatLongCoord(40.6048, -74.4427));
599$doc = new XapianDocument();
600$doc->add_term("coffee");
601$doc->add_value(COORD_SLOT, $coords->serialise());
602$db->add_document($doc);
603
604$centre = new XapianLatLongCoords();
605$centre->append(new XapianLatLongCoord(40.6048, -74.4427));
606
607$ps = new XapianLatLongDistancePostingSource(COORD_SLOT, $centre, $metric, $range);
608$q = new XapianQuery("coffee");
609$q = new XapianQuery(XapianQuery::OP_AND, $q, new XapianQuery($ps));
610
611$enq = new XapianEnquire($db);
612$enq->set_query($q);
613$mset = $enq->get_mset(0, 10);
614if ($mset->size() != 1) {
615    print "Expected one result with XapianLatLongDistancePostingSource, got ";
616    print $mset->size() . "\n";
617    exit(1);
618}
619
620$s = '';
621foreach ($db->allterms_begin() as $k => $term) {
622    $s .= "($term:{$k->get_termfreq()})";
623}
624if ($s !== '(coffee:1)') {
625    print "PHP Iterator iteration of allterms doesn't work ($s)\n";
626    exit(1);
627}
628
629# Test reference tracking and regression test for #659.
630$qp = new XapianQueryParser();
631{
632    $stop = new XapianSimpleStopper();
633    $stop->add('a');
634    $qp->set_stopper($stop);
635}
636$query = $qp->parse_query('a b');
637if ($query->get_description() !== 'Query(b@2)') {
638    print "XapianQueryParser::set_stopper() didn't work as expected - result was ".$query->get_description()."\n";
639    exit(1);
640}
641
642?>
643