[ci] Fix clang-santisers job for GHA change
[xapian.git] / xapian-bindings / php / smoketest.php
blob4429c732925cd6b15304f0479a36e0110a402802
1 <?php
2 // Run this PHP script using 'make check' in the build tree.
4 /* Simple test to ensure that we can load the xapian module and exercise basic
5 * functionality successfully.
7 * Copyright (C) 2004-2022 Olly Betts
8 * Copyright (C) 2010 Richard Boulton
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.
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.
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
26 # Die on any error, warning, notice, etc.
27 function die_on_error($errno, $errstr, $file, $line) {
28 if ($file !== Null) {
29 print $file;
30 if ($line !== Null) print ":$line";
31 print ": ";
33 print "$errstr\n";
34 exit(1);
36 set_error_handler("die_on_error", -1);
38 # Test the version number reporting functions give plausible results.
39 $v = Xapian::major_version().'.'.Xapian::minor_version().'.'.Xapian::revision();
40 $v2 = Xapian::version_string();
41 if ($v != $v2) {
42 print "Unexpected version output ($v != $v2)\n";
43 exit(1);
46 $db = new XapianWritableDatabase('', Xapian::DB_BACKEND_INMEMORY);
47 $db2 = new XapianWritableDatabase('', Xapian::DB_BACKEND_INMEMORY);
49 # Check handling of Xapian::DocNotFoundError
50 try {
51 $doc2 = $db->get_document(2);
52 print "Retrieved non-existent document\n";
53 exit(1);
54 } catch (Exception $e) {
55 if ($e->getMessage() !== "DocNotFoundError: Docid 2 not found") {
56 print "DocNotFoundError Exception string not as expected, got: '{$e->getMessage()}'\n";
57 exit(1);
61 # Check QueryParser parsing error.
62 try {
63 $qp = new XapianQueryParser;
64 $qp->set_stemmer(new XapianStem("en"));
65 $qp->parse_query("test AND");
66 print "Successfully parsed bad query\n";
67 exit(1);
68 } catch (Exception $e) {
69 if ($e->getMessage() !== "QueryParserError: Syntax: <expression> AND <expression>") {
70 print "QueryParserError Exception string not as expected, got: '$e->getMessage()'\n";
71 exit(1);
75 # Check that DB_BACKEND_STUB works as expected.
76 try {
77 $db = new XapianDatabase("nosuchdir/nosuchdb", Xapian::DB_BACKEND_STUB);
78 print "Opened non-existent stub database\n";
79 exit(1);
80 } catch (Exception $e) {
81 if ($e->getMessage() !== "DatabaseNotFoundError: Couldn't open stub database file: nosuchdir/nosuchdb (No such file or directory)") {
82 print "DatabaseNotFoundError Exception string not as expected, got: '{$e->getMessage()}'\n";
83 exit(1);
87 # Check that DB_BACKEND_STUB works as expected.
88 try {
89 $db = new XapianWritableDatabase("nosuchdir/nosuchdb",
90 Xapian::DB_OPEN|Xapian::DB_BACKEND_STUB);
91 print "Opened non-existent stub database\n";
92 exit(1);
93 } catch (Exception $e) {
94 if ($e->getMessage() !== "DatabaseNotFoundError: Couldn't open stub database file: nosuchdir/nosuchdb (No such file or directory)") {
95 print "DatabaseNotFoundError Exception string not as expected, got: '{$e->getMessage()}'\n";
96 exit(1);
100 $stem = new XapianStem("english");
101 if ($stem->get_description() != "Xapian::Stem(english)") {
102 print "Unexpected \$stem->get_description()\n";
103 exit(1);
106 $doc = new XapianDocument();
107 $doc->set_data("a\x00b");
108 if ($doc->get_data() === "a") {
109 print "get_data+set_data truncates at a zero byte\n";
110 exit(1);
112 if ($doc->get_data() !== "a\x00b") {
113 print "get_data+set_data doesn't transparently handle a zero byte\n";
114 exit(1);
116 $doc->set_data("is there anybody out there?");
117 $doc->add_term("XYzzy");
118 $doc->add_posting($stem->apply("is"), 1);
119 $doc->add_posting($stem->apply("there"), 2);
120 $doc->add_posting($stem->apply("anybody"), 3);
121 $doc->add_posting($stem->apply("out"), 4);
122 $doc->add_posting($stem->apply("there"), 5);
124 // Check virtual function dispatch.
125 if (substr($db->get_description(), 0, 17) !== "WritableDatabase(") {
126 print "Unexpected \$db->get_description()\n";
127 exit(1);
129 $db->add_document($doc);
130 if ($db->get_doccount() != 1) {
131 print "Unexpected \$db->get_doccount()\n";
132 exit(1);
135 $terms = array("smoke", "test", "terms");
136 $query = new XapianQuery(XapianQuery::OP_OR, $terms);
137 if ($query->get_description() != "Query((smoke OR test OR terms))") {
138 print "Unexpected \$query->get_description()\n";
139 exit(1);
141 $query1 = new XapianQuery(XapianQuery::OP_PHRASE, array("smoke", "test", "tuple"));
142 if ($query1->get_description() != "Query((smoke PHRASE 3 test PHRASE 3 tuple))") {
143 print "Unexpected \$query1->get_description()\n";
144 exit(1);
146 $query1b = new XapianQuery(XapianQuery::OP_NEAR, array("smoke", "test", "tuple"), 4);
147 if ($query1b->get_description() != "Query((smoke NEAR 4 test NEAR 4 tuple))") {
148 print "Unexpected \$query1b->get_description()\n";
149 exit(1);
151 $query2 = new XapianQuery(XapianQuery::OP_XOR, array(new XapianQuery("smoke"), $query1, "string"));
152 if ($query2->get_description() != "Query((smoke XOR (smoke PHRASE 3 test PHRASE 3 tuple) XOR string))") {
153 print "Unexpected \$query2->get_description()\n";
154 exit(1);
156 $subqs = array("a", "b");
157 $query3 = new XapianQuery(XapianQuery::OP_OR, $subqs);
158 if ($query3->get_description() != "Query((a OR b))") {
159 print "Unexpected \$query3->get_description()\n";
160 exit(1);
162 $enq = new XapianEnquire($db);
164 // Check Xapian::BAD_VALUENO is wrapped suitably.
165 $enq->set_collapse_key(Xapian::BAD_VALUENO);
167 // Test that the non-constant wrapping prior to 1.4.10 still works.
168 $enq->set_collapse_key(Xapian::BAD_VALUENO_get());
170 $enq->set_query(new XapianQuery(XapianQuery::OP_OR, "there", "is"));
171 $mset = $enq->get_mset(0, 10);
172 if ($mset->size() != 1) {
173 print "Unexpected \$mset->size()\n";
174 exit(1);
176 $terms = join(" ", $enq->get_matching_terms($mset->get_hit(0)));
177 if ($terms != "is there") {
178 print "Unexpected matching terms: $terms\n";
179 exit(1);
182 # Feature test for MatchDecider
183 $doc = new XapianDocument();
184 $doc->set_data("Two");
185 $doc->add_posting($stem->apply("out"), 1);
186 $doc->add_posting($stem->apply("outside"), 1);
187 $doc->add_posting($stem->apply("source"), 2);
188 $doc->add_value(0, "yes");
189 $db->add_document($doc);
191 class testmatchdecider extends XapianMatchDecider {
192 function apply($doc) {
193 return ($doc->get_value(0) == "yes");
197 $query = new XapianQuery($stem->apply("out"));
198 $enquire = new XapianEnquire($db);
199 $enquire->set_query($query);
200 $mdecider = new testmatchdecider();
201 $mset = $enquire->get_mset(0, 10, null, $mdecider);
202 if ($mset->size() != 1) {
203 print "Unexpected number of documents returned by match decider (".$mset->size().")\n";
204 exit(1);
206 if ($mset->get_docid(0) != 2) {
207 print "MatchDecider mset has wrong docid in\n";
208 exit(1);
211 class testexpanddecider extends XapianExpandDecider {
212 function apply($term) {
213 return ($term[0] !== 'a');
217 $enquire = new XapianEnquire($db);
218 $rset = new XapianRSet();
219 $rset->add_document(1);
220 $eset = $enquire->get_eset(10, $rset, XapianEnquire::USE_EXACT_TERMFREQ, new testexpanddecider());
221 foreach ($eset->begin() as $t) {
222 if ($t[0] === 'a') {
223 print "XapianExpandDecider was not used\n";
224 exit(1);
228 # Check min_wt argument to get_eset() works (new in 1.2.5).
229 $eset = $enquire->get_eset(100, $rset, XapianEnquire::USE_EXACT_TERMFREQ);
230 $min_wt = 0;
231 foreach ($eset->begin() as $i => $dummy) {
232 $min_wt = $i->get_weight();
234 if ($min_wt >= 1.9) {
235 print "ESet min weight too high for testcase\n";
236 exit(1);
238 $eset = $enquire->get_eset(100, $rset, XapianEnquire::USE_EXACT_TERMFREQ, NULL, 1.9);
239 $min_wt = 0;
240 foreach ($eset->begin() as $i => $dummy) {
241 $min_wt = $i->get_weight();
243 if ($min_wt < 1.9) {
244 print "ESet min_wt threshold not applied\n";
245 exit(1);
248 if (XapianQuery::OP_ELITE_SET != 10) {
249 print "OP_ELITE_SET is XapianQuery::OP_ELITE_SET not 10\n";
250 exit(1);
253 # Regression test - overload resolution involving boolean types failed.
254 $enq->set_sort_by_value(1, TRUE);
256 # Regression test - fixed in 0.9.10.1.
257 $oqparser = new XapianQueryParser();
258 $oquery = $oqparser->parse_query("I like tea");
260 # Regression test for bug#192 - fixed in 1.0.3.
261 $enq->set_cutoff(100);
263 # Check DateRangeProcessor works.
264 function add_rp_date(&$qp) {
265 $rpdate = new XapianDateRangeProcessor(1, Xapian::RP_DATE_PREFER_MDY, 1960);
266 $qp->add_rangeprocessor($rpdate);
268 $qp = new XapianQueryParser();
269 add_rp_date($qp);
270 $query = $qp->parse_query('12/03/99..12/04/01');
271 if ($query->get_description() !== 'Query(VALUE_RANGE 1 19991203 20011204)') {
272 print "XapianDateRangeProcessor didn't work - result was ".$query->get_description()."\n";
273 exit(1);
276 # Feature test for XapianFieldProcessor
277 class testfieldprocessor extends XapianFieldProcessor {
278 static $count = 0;
280 function __construct() {
281 ++self::$count;
282 parent::__construct();
285 function __destruct() {
286 --self::$count;
289 function apply($str) {
290 if ($str === 'spam') throw new Exception('already spam');
291 return new XapianQuery("spam");
295 $fp = new testfieldprocessor;
296 if (testfieldprocessor::$count !== 1) {
297 print "testfieldprocessor counting not working\n";
298 exit(1);
301 # Check object is still usable after being assigned to an object that gets
302 # deleted. The initial development version of PHP8 bindings failed to
303 # handle this case.
304 $qptmp = new XapianQueryParser;
305 $qptmp->add_prefix('spam', $fp);
306 unset($qptmp);
308 if (testfieldprocessor::$count === 0) {
309 print "testfieldprocessor object deleted early\n";
310 exit(1);
312 $qp->add_prefix('spam', $fp);
313 unset($fp);
314 $qp->add_boolean_prefix('filter', new testfieldprocessor);
315 $query = $qp->parse_query('spam:ignored');
316 if ($query->get_description() !== 'Query(spam)') {
317 print "testfieldprocessor didn't work - result was ".$query->get_description()."\n";
318 exit(1);
320 $query = $qp->parse_query('filter:ignored');
321 if ($query->get_description() !== 'Query(0 * spam)') {
322 print "Boolean testfieldprocessor didn't work - result was ".$query->get_description()."\n";
323 exit(1);
326 try {
327 $query = $qp->parse_query('spam:spam');
328 print "testfieldprocessor exception not rethrown\n";
329 exit(1);
330 } catch (Exception $e) {
331 if ($e->getMessage() !== 'already spam') {
332 print "Exception has wrong message\n";
333 exit(1);
337 if (testfieldprocessor::$count === 0) {
338 print "testfieldprocessor deleted early\n";
339 exit(1);
341 unset($qp);
342 if (testfieldprocessor::$count !== 0) {
343 print "testfieldprocessor object not deleted\n";
346 # Test setting and getting metadata
347 if ($db->get_metadata('Foo') !== '') {
348 print "Unexpected value for metadata associated with 'Foo' (expected ''): '".$db->get_metadata('Foo')."'\n";
349 exit(1);
351 $db->set_metadata('Foo', 'Foo');
352 if ($db->get_metadata('Foo') !== 'Foo') {
353 print "Unexpected value for metadata associated with 'Foo' (expected 'Foo'): '".$db->get_metadata('Foo')."'\n";
354 exit(1);
357 # Test OP_SCALE_WEIGHT and corresponding constructor
358 $query4 = new XapianQuery(XapianQuery::OP_SCALE_WEIGHT, new XapianQuery('foo'), 5.0);
359 if ($query4->get_description() != "Query(5 * foo)") {
360 print "Unexpected \$query4->get_description()\n";
361 exit(1);
364 # Test MultiValueKeyMaker.
366 $doc = new XapianDocument();
367 $doc->add_term("foo");
368 $doc->add_value(0, "ABB");
369 $db2->add_document($doc);
370 $doc->add_value(0, "ABC");
371 $db2->add_document($doc);
372 $doc->add_value(0, "ABC\0");
373 $db2->add_document($doc);
374 $doc->add_value(0, "ABCD");
375 $db2->add_document($doc);
376 $doc->add_value(0, "ABC\xff");
377 $db2->add_document($doc);
379 $enquire = new XapianEnquire($db2);
380 $enquire->set_query(new XapianQuery("foo"));
383 $sorter = new XapianMultiValueKeyMaker();
384 $sorter->add_value(0);
385 $enquire->set_sort_by_key($sorter, true);
386 $mset = $enquire->get_mset(0, 10);
387 mset_expect_order($mset, array(5, 4, 3, 2, 1));
391 $sorter = new XapianMultiValueKeyMaker();
392 $sorter->add_value(0, true);
393 $enquire->set_sort_by_key($sorter, true);
394 $mset = $enquire->get_mset(0, 10);
395 mset_expect_order($mset, array(1, 2, 3, 4, 5));
399 $sorter = new XapianMultiValueKeyMaker();
400 $sorter->add_value(0);
401 $sorter->add_value(1);
402 $enquire->set_sort_by_key($sorter, true);
403 $mset = $enquire->get_mset(0, 10);
404 mset_expect_order($mset, array(5, 4, 3, 2, 1));
408 $sorter = new XapianMultiValueKeyMaker();
409 $sorter->add_value(0, true);
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(1, 2, 3, 4, 5));
417 $sorter = new XapianMultiValueKeyMaker();
418 $sorter->add_value(0);
419 $sorter->add_value(1, true);
420 $enquire->set_sort_by_key($sorter, true);
421 $mset = $enquire->get_mset(0, 10);
422 mset_expect_order($mset, array(5, 4, 3, 2, 1));
426 $sorter = new XapianMultiValueKeyMaker();
427 $sorter->add_value(0, true);
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(1, 2, 3, 4, 5));
434 # Feature test for ValueSetMatchDecider:
436 $md = new XapianValueSetMatchDecider(0, true);
437 $md->add_value("ABC");
438 $doc = new XapianDocument();
439 $doc->add_value(0, "ABCD");
440 if ($md->apply($doc)) {
441 print "Unexpected result from ValueSetMatchDecider->apply(); expected false\n";
442 exit(1);
445 $doc = new XapianDocument();
446 $doc->add_value(0, "ABC");
447 if (!$md->apply($doc)) {
448 print "Unexpected result from ValueSetMatchDecider->apply(); expected true\n";
449 exit(1);
452 $mset = $enquire->get_mset(0, 10, 0, null, $md);
453 mset_expect_order($mset, array(2));
455 $md = new XapianValueSetMatchDecider(0, false);
456 $md->add_value("ABC");
457 $mset = $enquire->get_mset(0, 10, 0, null, $md);
458 mset_expect_order($mset, array(1, 3, 4, 5));
461 function mset_expect_order($mset, $a) {
462 if ($mset->size() != sizeof($a)) {
463 print "MSet has ".$mset->size()." entries, expected ".sizeof($a)."\n";
464 exit(1);
466 for ($j = 0; $j < sizeof($a); ++$j) {
467 $docid = $mset->get_hit($j)->get_docid();
468 if ($docid != $a[$j]) {
469 print "Expected MSet[$j] to be $a[$j], got ".$docid()."\n";
470 exit(1);
475 # Feature tests for Query "term" constructor optional arguments:
476 $query_wqf = new XapianQuery('wqf', 3);
477 if ($query_wqf->get_description() != 'Query(wqf#3)') {
478 print "Unexpected \$query_wqf->get_description():\n";
479 print $query_wqf->get_description() . "\n";
480 exit(1);
483 $query = new XapianQuery(XapianQuery::OP_VALUE_GE, 0, "100");
484 if ($query->get_description() != 'Query(VALUE_GE 0 100)') {
485 print "Unexpected \$query->get_description():\n";
486 print $query->get_description() . "\n";
487 exit(1);
490 $query = XapianQuery::MatchAll();
491 if ($query->get_description() != 'Query(<alldocuments>)') {
492 print "Unexpected \$query->get_description():\n";
493 print $query->get_description() . "\n";
494 exit(1);
497 $query = XapianQuery::MatchNothing();
498 if ($query->get_description() != 'Query()') {
499 print "Unexpected \$query->get_description():\n";
500 print $query->get_description() . "\n";
501 exit(1);
504 # Test access to matchspy values:
506 $matchspy = new XapianValueCountMatchSpy(0);
507 $enquire->add_matchspy($matchspy);
508 $enquire->get_mset(0, 10);
509 $values = array();
510 foreach ($matchspy->values_begin() as $k => $term) {
511 $values[$term] = $k->get_termfreq();
513 $expected = array(
514 "ABB" => 1,
515 "ABC" => 1,
516 "ABC\0" => 1,
517 "ABCD" => 1,
518 "ABC\xff" => 1,
520 if ($values != $expected) {
521 print "Unexpected matchspy values():\n";
522 var_dump($values);
523 var_dump($expected);
524 print "\n";
525 exit(1);
530 class testspy extends XapianMatchSpy {
531 static $count = 0;
533 function __construct() {
534 ++self::$count;
535 parent::__construct();
538 function __destruct() {
539 --self::$count;
542 public $matchspy_count = 0;
544 function apply($doc, $wt) {
545 if (substr($doc->get_value(0), 0, 3) == "ABC") ++$this->matchspy_count;
549 $matchspy = new testspy();
550 if (testspy::$count !== 1) {
551 print "testspy counting not working\n";
552 exit(1);
554 $enquire->clear_matchspies();
555 $enquire->add_matchspy($matchspy);
556 $enquire->get_mset(0, 10);
557 if ($matchspy->matchspy_count != 4) {
558 print "Unexpected matchspy count of {$matchspy->matchspy_count}\n";
559 exit(1);
561 unset($matchspy);
562 if (testspy::$count === 0) {
563 print "testspy object deleted early\n";
564 exit(1);
566 unset($enquire);
567 if (testspy::$count !== 0) {
568 print "testspy object not deleted\n";
569 exit(1);
573 # Regression test for SWIG bug - it was generating "return $r;" in wrapper
574 # functions which didn't set $r.
575 $indexer = new XapianTermGenerator();
576 $doc = new XapianDocument();
578 $indexer->set_document($doc);
579 $indexer->index_text("I ask nothing in return");
580 $indexer->index_text_without_positions("Tea time");
581 $indexer->index_text("Return in time");
583 $s = '';
584 foreach ($doc->termlist_begin() as $term) {
585 $s .= $term . ' ';
587 if ($s !== 'ask i in nothing return tea time ') {
588 print "PHP Iterator wrapping of TermIterator doesn't work ($s)\n";
589 exit(1);
592 $s = '';
593 foreach ($doc->termlist_begin() as $k => $term) {
594 $s .= $term . ':' . $k->get_wdf() . ' ';
596 if ($s !== 'ask:1 i:1 in:2 nothing:1 return:2 tea:1 time:2 ') {
597 print "PHP Iterator wrapping of TermIterator keys doesn't work ($s)\n";
598 exit(1);
601 // Test that XapianTermGenerator keeps a reference to XapianStopper.
602 $indexer->set_stopper_strategy(XapianTermGenerator::STOP_ALL);
604 $stop = new XapianSimpleStopper();
605 $stop->add('a');
606 $indexer->set_stopper($stop);
607 $stop = null;
609 $indexer->index_text("a b");
611 # Test GeoSpatial API
612 $coord = new XapianLatLongCoord();
613 $coord = new XapianLatLongCoord(-41.288889, 174.777222);
615 define('COORD_SLOT', 2);
616 $metric = new XapianGreatCircleMetric();
617 $range = 42.0;
619 $centre = new XapianLatLongCoords($coord);
620 $query = new XapianQuery(new XapianLatLongDistancePostingSource(COORD_SLOT, $centre, $metric, $range));
622 $db = new XapianWritableDatabase('', Xapian::DB_BACKEND_INMEMORY);
623 $coords = new XapianLatLongCoords();
624 $coords->append(new XapianLatLongCoord(40.6048, -74.4427));
625 $doc = new XapianDocument();
626 $doc->add_term("coffee");
627 $doc->add_value(COORD_SLOT, $coords->serialise());
628 $db->add_document($doc);
630 $centre = new XapianLatLongCoords();
631 $centre->append(new XapianLatLongCoord(40.6048, -74.4427));
633 $ps = new XapianLatLongDistancePostingSource(COORD_SLOT, $centre, $metric, $range);
634 $q = new XapianQuery("coffee");
635 $q = new XapianQuery(XapianQuery::OP_AND, $q, new XapianQuery($ps));
636 $q = new XapianQuery(XapianQuery::OP_OR, [$q, XapianQuery::MatchNothing()]);
637 // Check that we keep a reference via XapianQuery.
638 $ps = null;
640 $enq = new XapianEnquire($db);
641 $enq->set_query($q);
642 $mset = $enq->get_mset(0, 10);
643 if ($mset->size() != 1) {
644 print "Expected one result with XapianLatLongDistancePostingSource, got ";
645 print $mset->size() . "\n";
646 exit(1);
649 $s = '';
650 foreach ($db->allterms_begin() as $k => $term) {
651 $s .= "($term:{$k->get_termfreq()})";
653 if ($s !== '(coffee:1)') {
654 print "PHP Iterator iteration of allterms doesn't work ($s)\n";
655 exit(1);
658 # Test reference tracking and regression test for #659.
659 $qp = new XapianQueryParser();
661 $stop = new XapianSimpleStopper();
662 $stop->add('a');
663 $qp->set_stopper($stop);
664 // Test that XapianQueryParser keeps a reference to XapianStopper.
665 $stop = null;
667 $query = $qp->parse_query('a b');
668 if ($query->get_description() !== 'Query(b@2)') {
669 print "XapianQueryParser::set_stopper() didn't work as expected - result was ".$query->get_description()."\n";
670 exit(1);