silverstripe /
silverstripe-fulltextsearch
| 1 | <?php |
||
| 2 | |||
| 3 | namespace SilverStripe\FullTextSearch\Tests; |
||
| 4 | |||
| 5 | use Apache_Solr_Document; |
||
| 6 | use Page; |
||
| 7 | use SilverStripe\Assets\File; |
||
| 8 | use SilverStripe\CMS\Model\SiteTree; |
||
| 9 | use SilverStripe\Core\Config\Config; |
||
| 10 | use SilverStripe\Core\Injector\Injector; |
||
| 11 | use SilverStripe\Dev\SapphireTest; |
||
| 12 | use SilverStripe\FullTextSearch\Search\FullTextSearch; |
||
| 13 | use SilverStripe\FullTextSearch\Search\Updaters\SearchUpdater; |
||
| 14 | use SilverStripe\FullTextSearch\Search\Variants\SearchVariant; |
||
| 15 | use SilverStripe\FullTextSearch\Search\Variants\SearchVariantVersioned; |
||
| 16 | use SilverStripe\FullTextSearch\Solr\Reindex\Handlers\SolrReindexHandler; |
||
| 17 | use SilverStripe\FullTextSearch\Solr\Reindex\Handlers\SolrReindexImmediateHandler; |
||
| 18 | use SilverStripe\FullTextSearch\Solr\Services\Solr4Service; |
||
| 19 | use SilverStripe\FullTextSearch\Solr\Services\SolrService; |
||
| 20 | use SilverStripe\FullTextSearch\Solr\Tasks\Solr_Reindex; |
||
| 21 | use SilverStripe\FullTextSearch\Tests\SolrIndexTest\SolrIndexTest_MyDataObjectOne; |
||
| 22 | use SilverStripe\FullTextSearch\Tests\SolrIndexTest\SolrIndexTest_MyDataObjectTwo; |
||
| 23 | use SilverStripe\FullTextSearch\Tests\SolrIndexTest\SolrIndexTest_MyPage; |
||
| 24 | use SilverStripe\FullTextSearch\Tests\SolrIndexTest\SolrIndexTest_ShowInSearchIndex; |
||
| 25 | use SilverStripe\FullTextSearch\Tests\SolrReindexTest\SolrReindexTest_Index; |
||
| 26 | use SilverStripe\FullTextSearch\Tests\SolrReindexTest\SolrReindexTest_Item; |
||
| 27 | use SilverStripe\FullTextSearch\Tests\SolrReindexTest\SolrReindexTest_RecordingLogger; |
||
| 28 | use SilverStripe\FullTextSearch\Tests\SolrReindexTest\SolrReindexTest_TestHandler; |
||
| 29 | use SilverStripe\FullTextSearch\Tests\SolrReindexTest\SolrReindexTest_Variant; |
||
| 30 | use SilverStripe\Versioned\Versioned; |
||
| 31 | |||
| 32 | class SolrReindexTest extends SapphireTest |
||
| 33 | { |
||
| 34 | |||
| 35 | protected $usesDatabase = true; |
||
| 36 | |||
| 37 | protected static $extra_dataobjects = array( |
||
| 38 | SolrReindexTest_Item::class, |
||
| 39 | SolrIndexTest_MyPage::class, |
||
| 40 | SolrIndexTest_MyDataObjectOne::class, |
||
| 41 | SolrIndexTest_MyDataObjectTwo::class, |
||
| 42 | ); |
||
| 43 | |||
| 44 | /** |
||
| 45 | * Forced index for testing |
||
| 46 | * |
||
| 47 | * @var SolrReindexTest_Index |
||
| 48 | */ |
||
| 49 | protected $index = null; |
||
| 50 | |||
| 51 | /** |
||
| 52 | * Mock service |
||
| 53 | * |
||
| 54 | * @var SolrService |
||
| 55 | */ |
||
| 56 | protected $service = null; |
||
| 57 | |||
| 58 | protected function setUp() |
||
| 59 | { |
||
| 60 | parent::setUp(); |
||
| 61 | |||
| 62 | // Set test handler for reindex |
||
| 63 | Config::modify()->set(Injector::class, SolrReindexHandler::class, array( |
||
| 64 | 'class' => SolrReindexTest_TestHandler::class |
||
| 65 | )); |
||
| 66 | |||
| 67 | Injector::inst()->registerService(new SolrReindexTest_TestHandler(), SolrReindexHandler::class); |
||
| 68 | |||
| 69 | // Set test variant |
||
| 70 | SolrReindexTest_Variant::enable(); |
||
| 71 | |||
| 72 | // Set index list |
||
| 73 | $this->service = $this->getServiceMock(); |
||
| 74 | $this->index = singleton(SolrReindexTest_Index::class); |
||
| 75 | $this->index->setService($this->service); |
||
| 76 | |||
| 77 | FullTextSearch::force_index_list($this->index); |
||
| 78 | } |
||
| 79 | |||
| 80 | /** |
||
| 81 | * Populate database with dummy dataset |
||
| 82 | * |
||
| 83 | * @param int $number Number of records to create in each variant |
||
| 84 | */ |
||
| 85 | protected function createDummyData($number) |
||
| 86 | { |
||
| 87 | // Note that we don't create any records in variant = 2, to represent a variant |
||
| 88 | // that should be cleared without any re-indexes performed |
||
| 89 | foreach ([0, 1] as $variant) { |
||
| 90 | for ($i = 1; $i <= $number; $i++) { |
||
| 91 | $item = new SolrReindexTest_Item(); |
||
| 92 | $item->Variant = $variant; |
||
| 93 | $item->Title = "Item $variant / $i"; |
||
| 94 | $item->write(); |
||
| 95 | } |
||
| 96 | } |
||
| 97 | } |
||
| 98 | |||
| 99 | /** |
||
| 100 | * Mock service |
||
| 101 | * |
||
| 102 | * @return SolrService |
||
| 103 | */ |
||
| 104 | protected function getServiceMock() |
||
| 105 | { |
||
| 106 | $serviceMock = $this->getMockBuilder(Solr4Service::class) |
||
| 107 | ->setMethods(['deleteByQuery', 'addDocument']); |
||
| 108 | |||
| 109 | return $serviceMock->getMock(); |
||
| 110 | } |
||
| 111 | |||
| 112 | protected function tearDown() |
||
| 113 | { |
||
| 114 | FullTextSearch::force_index_list(); |
||
| 115 | SolrReindexTest_Variant::disable(); |
||
| 116 | parent::tearDown(); |
||
| 117 | } |
||
| 118 | |||
| 119 | /** |
||
| 120 | * Get the reindex handler |
||
| 121 | * |
||
| 122 | * @return SolrReindexHandler |
||
| 123 | */ |
||
| 124 | protected function getHandler() |
||
| 125 | { |
||
| 126 | return Injector::inst()->get(SolrReindexHandler::class); |
||
| 127 | } |
||
| 128 | |||
| 129 | /** |
||
| 130 | * Ensure the test variant is up and running properly |
||
| 131 | */ |
||
| 132 | public function testVariant() |
||
| 133 | { |
||
| 134 | // State defaults to 0 |
||
| 135 | $variant = SearchVariant::current_state(); |
||
| 136 | $this->assertEquals( |
||
| 137 | array( |
||
| 138 | SolrReindexTest_Variant::class => "0" |
||
| 139 | ), |
||
| 140 | $variant |
||
| 141 | ); |
||
| 142 | |||
| 143 | // All states enumerated |
||
| 144 | $allStates = iterator_to_array(SearchVariant::reindex_states()); |
||
| 145 | $this->assertEquals( |
||
| 146 | array( |
||
| 147 | array( |
||
| 148 | SolrReindexTest_Variant::class => "0" |
||
| 149 | ), |
||
| 150 | array( |
||
| 151 | SolrReindexTest_Variant::class => "1" |
||
| 152 | ), |
||
| 153 | array( |
||
| 154 | SolrReindexTest_Variant::class => "2" |
||
| 155 | ) |
||
| 156 | ), |
||
| 157 | $allStates |
||
| 158 | ); |
||
| 159 | |||
| 160 | // Check correct items created and that filtering on variant works |
||
| 161 | $this->createDummyData(120); |
||
| 162 | SolrReindexTest_Variant::set_current(2); |
||
| 163 | $this->assertEquals(0, SolrReindexTest_Item::get()->count()); |
||
| 164 | SolrReindexTest_Variant::set_current(1); |
||
| 165 | $this->assertEquals(120, SolrReindexTest_Item::get()->count()); |
||
| 166 | SolrReindexTest_Variant::set_current(0); |
||
| 167 | $this->assertEquals(120, SolrReindexTest_Item::get()->count()); |
||
| 168 | SolrReindexTest_Variant::disable(); |
||
| 169 | $this->assertEquals(240, SolrReindexTest_Item::get()->count()); |
||
| 170 | } |
||
| 171 | |||
| 172 | /** |
||
| 173 | * Given the invocation of a new re-index with a given set of data, ensure that the necessary |
||
| 174 | * list of groups are created and segmented for each state |
||
| 175 | * |
||
| 176 | * Test should work fine with any variants (versioned, subsites, etc) specified |
||
| 177 | */ |
||
| 178 | public function testReindexSegmentsGroups() |
||
| 179 | { |
||
| 180 | $this->service->method('deleteByQuery') |
||
| 181 | ->withConsecutive( |
||
| 182 | ['-(ClassHierarchy:' . SolrReindexTest_Item::class . ')'], |
||
| 183 | ['+(ClassHierarchy:' . SolrReindexTest_Item::class . ') +(_testvariant:"2")'] |
||
| 184 | ); |
||
| 185 | |||
| 186 | $this->createDummyData(120); |
||
| 187 | |||
| 188 | // Initiate re-index |
||
| 189 | $logger = new SolrReindexTest_RecordingLogger(); |
||
| 190 | $this->getHandler()->runReindex($logger, 21, Solr_Reindex::class); |
||
| 191 | |||
| 192 | // Test that invalid classes are removed |
||
| 193 | $this->assertContains( |
||
| 194 | 'Clearing obsolete classes from ' . str_replace('\\', '-', SolrReindexTest_Index::class), |
||
| 195 | $logger->getMessages() |
||
| 196 | ); |
||
| 197 | |||
| 198 | // Test that valid classes in invalid variants are removed |
||
| 199 | $this->assertContains( |
||
| 200 | 'Clearing all records of type ' . SolrReindexTest_Item::class . ' in the current state: {' |
||
| 201 | . json_encode(SolrReindexTest_Variant::class) . ':"2"}', |
||
| 202 | $logger->getMessages() |
||
| 203 | ); |
||
| 204 | |||
| 205 | // 120x2 grouped into groups of 21 results in 12 groups |
||
| 206 | $this->assertEquals(12, $logger->countMessages('Called processGroup with ')); |
||
| 207 | $this->assertEquals(6, $logger->countMessages('{' . json_encode(SolrReindexTest_Variant::class) . ':"0"}')); |
||
| 208 | $this->assertEquals(6, $logger->countMessages('{' . json_encode(SolrReindexTest_Variant::class) . ':"1"}')); |
||
| 209 | |||
| 210 | // Given that there are two variants, there should be two group ids of each number |
||
| 211 | $this->assertEquals(2, $logger->countMessages(' ' . SolrReindexTest_Item::class . ', group 0 of 6')); |
||
| 212 | $this->assertEquals(2, $logger->countMessages(' ' . SolrReindexTest_Item::class . ', group 1 of 6')); |
||
| 213 | $this->assertEquals(2, $logger->countMessages(' ' . SolrReindexTest_Item::class . ', group 2 of 6')); |
||
| 214 | $this->assertEquals(2, $logger->countMessages(' ' . SolrReindexTest_Item::class . ', group 3 of 6')); |
||
| 215 | $this->assertEquals(2, $logger->countMessages(' ' . SolrReindexTest_Item::class . ', group 4 of 6')); |
||
| 216 | $this->assertEquals(2, $logger->countMessages(' ' . SolrReindexTest_Item::class . ', group 5 of 6')); |
||
| 217 | |||
| 218 | // Check various group sizes |
||
| 219 | $logger->clear(); |
||
| 220 | $this->getHandler()->runReindex($logger, 120, 'Solr_Reindex'); |
||
| 221 | $this->assertEquals(2, $logger->countMessages('Called processGroup with ')); |
||
| 222 | $logger->clear(); |
||
| 223 | $this->getHandler()->runReindex($logger, 119, 'Solr_Reindex'); |
||
| 224 | $this->assertEquals(4, $logger->countMessages('Called processGroup with ')); |
||
| 225 | $logger->clear(); |
||
| 226 | $this->getHandler()->runReindex($logger, 121, 'Solr_Reindex'); |
||
| 227 | $this->assertEquals(2, $logger->countMessages('Called processGroup with ')); |
||
| 228 | $logger->clear(); |
||
| 229 | $this->getHandler()->runReindex($logger, 2, 'Solr_Reindex'); |
||
| 230 | $this->assertEquals(120, $logger->countMessages('Called processGroup with ')); |
||
| 231 | } |
||
| 232 | |||
| 233 | /** |
||
| 234 | * Test index processing on individual groups |
||
| 235 | */ |
||
| 236 | public function testRunGroup() |
||
| 237 | { |
||
| 238 | $this->service->method('deleteByQuery') |
||
| 239 | ->with('+(ClassHierarchy:' . SolrReindexTest_Item::class . ') +_query_:"{!frange l=2 u=2}mod(ID, 6)" +(_testvariant:"1")'); |
||
| 240 | |||
| 241 | $this->createDummyData(120); |
||
| 242 | $logger = new SolrReindexTest_RecordingLogger(); |
||
| 243 | |||
| 244 | // Initiate re-index of third group (index 2 of 6) |
||
| 245 | $state = array(SolrReindexTest_Variant::class => '1'); |
||
| 246 | $this->getHandler()->runGroup($logger, $this->index, $state, SolrReindexTest_Item::class, 6, 2); |
||
| 247 | $idMessage = $logger->filterMessages('Updated '); |
||
| 248 | $this->assertNotEmpty(preg_match('/^Updated (?<ids>[,\d]+)/i', $idMessage[0], $matches)); |
||
| 249 | $ids = array_unique(explode(',', $matches['ids'])); |
||
| 250 | |||
| 251 | // Test successful |
||
| 252 | $this->assertNotEmpty($logger->getMessages('Adding ' . SolrReindexTest_Item::class)); |
||
| 253 | $this->assertNotEmpty($logger->getMessages('Done')); |
||
| 254 | |||
| 255 | // Test that items in this variant / group are re-indexed |
||
| 256 | // 120 divided into 6 groups should be 20 at least (max 21) |
||
| 257 | $this->assertEquals(21, count($ids), 'Group size is about 20', 1); |
||
| 258 | foreach ($ids as $id) { |
||
| 259 | // Each id should be % 6 == 2 |
||
| 260 | $this->assertEquals(2, $id % 6, "ID $id Should match pattern ID % 6 = 2"); |
||
| 261 | } |
||
| 262 | } |
||
| 263 | |||
| 264 | /** |
||
| 265 | * Test that running all groups covers the entire range of dataobject IDs |
||
| 266 | */ |
||
| 267 | public function testRunAllGroups() |
||
| 268 | { |
||
| 269 | $this->service->method('deleteByQuery') |
||
| 270 | ->withConsecutive( |
||
| 271 | ['+(ClassHierarchy:' . SolrReindexTest_Item::class . ') +_query_:"{!frange l=0 u=0}mod(ID, 6)" +(_testvariant:"1")'], |
||
| 272 | ['+(ClassHierarchy:' . SolrReindexTest_Item::class . ') +_query_:"{!frange l=1 u=1}mod(ID, 6)" +(_testvariant:"1")'], |
||
| 273 | ['+(ClassHierarchy:' . SolrReindexTest_Item::class . ') +_query_:"{!frange l=2 u=2}mod(ID, 6)" +(_testvariant:"1")'], |
||
| 274 | ['+(ClassHierarchy:' . SolrReindexTest_Item::class . ') +_query_:"{!frange l=3 u=3}mod(ID, 6)" +(_testvariant:"1")'], |
||
| 275 | ['+(ClassHierarchy:' . SolrReindexTest_Item::class . ') +_query_:"{!frange l=4 u=4}mod(ID, 6)" +(_testvariant:"1")'], |
||
| 276 | ['+(ClassHierarchy:' . SolrReindexTest_Item::class . ') +_query_:"{!frange l=5 u=5}mod(ID, 6)" +(_testvariant:"1")'], |
||
| 277 | ['+(ClassHierarchy:' . SolrReindexTest_Item::class . ') +_query_:"{!frange l=6 u=6}mod(ID, 6)" +(_testvariant:"1")'] |
||
| 278 | ); |
||
| 279 | |||
| 280 | $this->createDummyData(120); |
||
| 281 | $logger = new SolrReindexTest_RecordingLogger(); |
||
| 282 | |||
| 283 | // Test that running all groups covers the complete set of ids |
||
| 284 | $state = array(SolrReindexTest_Variant::class => '1'); |
||
| 285 | for ($i = 0; $i < 6; $i++) { |
||
| 286 | // See testReindexSegmentsGroups for test that each of these states is invoked during a full reindex |
||
| 287 | $this |
||
| 288 | ->getHandler() |
||
| 289 | ->runGroup($logger, $this->index, $state, SolrReindexTest_Item::class, 6, $i); |
||
| 290 | } |
||
| 291 | |||
| 292 | // Count all ids updated |
||
| 293 | $ids = array(); |
||
| 294 | foreach ($logger->filterMessages('Updated ') as $message) { |
||
| 295 | $this->assertNotEmpty(preg_match('/^Updated (?<ids>[,\d]+)/', $message, $matches)); |
||
| 296 | $ids = array_unique(array_merge($ids, explode(',', $matches['ids']))); |
||
| 297 | } |
||
| 298 | |||
| 299 | // Check ids |
||
| 300 | $this->assertEquals(120, count($ids)); |
||
| 301 | } |
||
| 302 | |||
| 303 | /** |
||
| 304 | * Test that ShowInSearch filtering is working correctly |
||
| 305 | */ |
||
| 306 | public function testShowInSearch() |
||
| 307 | { |
||
| 308 | $defaultMode = Versioned::get_reading_mode(); |
||
| 309 | Versioned::set_reading_mode('Stage.' . Versioned::DRAFT); |
||
| 310 | |||
| 311 | // will get added |
||
| 312 | $pageA = new Page(); |
||
| 313 | $pageA->Title = 'Test Page true'; |
||
| 314 | $pageA->ShowInSearch = true; |
||
| 315 | $pageA->write(); |
||
| 316 | |||
| 317 | // will get filtered out |
||
| 318 | $page = new Page(); |
||
| 319 | $page->Title = 'Test Page false'; |
||
| 320 | $page->ShowInSearch = false; |
||
| 321 | $page->write(); |
||
| 322 | |||
| 323 | // will get added |
||
| 324 | $fileA = new File(); |
||
| 325 | $fileA->Title = 'Test File true'; |
||
| 326 | $fileA->ShowInSearch = true; |
||
|
0 ignored issues
–
show
|
|||
| 327 | $fileA->write(); |
||
| 328 | |||
| 329 | // will get filtered out |
||
| 330 | $file = new File(); |
||
| 331 | $file->Title = 'Test File false'; |
||
| 332 | $file->ShowInSearch = false; |
||
|
0 ignored issues
–
show
The property
$ShowInSearch was declared of type string, but false is of type false. Maybe add a type cast?
This check looks for assignments to scalar types that may be of the wrong type. To ensure the code behaves as expected, it may be a good idea to add an explicit type cast. $answer = 42;
$correct = false;
$correct = (bool) $answer;
Loading history...
|
|||
| 333 | $file->write(); |
||
| 334 | |||
| 335 | // will get added |
||
| 336 | $objOneA = new SolrIndexTest_MyDataObjectOne(); |
||
| 337 | $objOneA->Title = 'Test MyDataObjectOne true'; |
||
| 338 | $objOneA->ShowInSearch = true; |
||
| 339 | $objOneA->write(); |
||
| 340 | |||
| 341 | // will get filtered out |
||
| 342 | $objOne = new SolrIndexTest_MyDataObjectOne(); |
||
| 343 | $objOne->Title = 'Test MyDataObjectOne false'; |
||
| 344 | $objOne->ShowInSearch = false; |
||
| 345 | $objOne->write(); |
||
| 346 | |||
| 347 | // will get added |
||
| 348 | // this class has a getShowInSearch() == true, which will override $mypage->ShowInSearch = false |
||
| 349 | $objTwoA = new SolrIndexTest_MyDataObjectTwo(); |
||
| 350 | $objTwoA->Title = 'Test MyDataObjectTwo false'; |
||
| 351 | $objTwoA->ShowInSearch = false; |
||
| 352 | $objTwoA->write(); |
||
| 353 | |||
| 354 | // will get added |
||
| 355 | // this class has a getShowInSearch() == true, which will override $mypage->ShowInSearch = false |
||
| 356 | $myPageA = new SolrIndexTest_MyPage(); |
||
| 357 | $myPageA->Title = 'Test MyPage false'; |
||
| 358 | $myPageA->ShowInSearch = false; |
||
| 359 | $myPageA->write(); |
||
| 360 | |||
| 361 | $serviceMock = $this->getMockBuilder(Solr4Service::class) |
||
| 362 | ->setMethods(['addDocument', 'deleteByQuery']) |
||
| 363 | ->getMock(); |
||
| 364 | |||
| 365 | $index = new SolrIndexTest_ShowInSearchIndex(); |
||
| 366 | $index->setService($serviceMock); |
||
| 367 | FullTextSearch::force_index_list($index); |
||
| 368 | |||
| 369 | $callback = function (Apache_Solr_Document $doc) use ($pageA, $myPageA, $fileA, $objOneA, $objTwoA): bool { |
||
| 370 | $validKeys = [ |
||
| 371 | Page::class . $pageA->ID, |
||
| 372 | SolrIndexTest_MyPage::class . $myPageA->ID, |
||
| 373 | File::class . $fileA->ID, |
||
| 374 | SolrIndexTest_MyDataObjectOne::class . $objOneA->ID, |
||
| 375 | SolrIndexTest_MyDataObjectTwo::class . $objTwoA->ID |
||
| 376 | ]; |
||
| 377 | return in_array($this->createSolrDocKey($doc), $validKeys); |
||
| 378 | }; |
||
| 379 | |||
| 380 | $serviceMock |
||
| 381 | ->expects($this->exactly(5)) |
||
| 382 | ->method('addDocument') |
||
| 383 | ->withConsecutive( |
||
| 384 | [$this->callback($callback)], |
||
| 385 | [$this->callback($callback)], |
||
| 386 | [$this->callback($callback)], |
||
| 387 | [$this->callback($callback)], |
||
| 388 | [$this->callback($callback)] |
||
| 389 | ); |
||
| 390 | |||
| 391 | $logger = new SolrReindexTest_RecordingLogger(); |
||
| 392 | $state = [SearchVariantVersioned::class => Versioned::DRAFT]; |
||
| 393 | $handler = Injector::inst()->get(SolrReindexImmediateHandler::class); |
||
| 394 | $handler->runGroup($logger, $index, $state, SiteTree::class, 1, 0); |
||
| 395 | $handler->runGroup($logger, $index, $state, File::class, 1, 0); |
||
| 396 | $handler->runGroup($logger, $index, $state, SolrIndexTest_MyDataObjectOne::class, 1, 0); |
||
| 397 | |||
| 398 | Versioned::set_reading_mode($defaultMode); |
||
| 399 | } |
||
| 400 | |||
| 401 | protected function createSolrDocKey(Apache_Solr_Document $doc) |
||
| 402 | { |
||
| 403 | return $doc->getField('ClassName')['value'] . $doc->getField('ID')['value']; |
||
| 404 | } |
||
| 405 | } |
||
| 406 |
This check looks for assignments to scalar types that may be of the wrong type.
To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.