1 | <?php |
||
2 | |||
3 | namespace SilverStripe\FullTextSearch\Tests; |
||
4 | |||
5 | use Apache_Solr_Document; |
||
6 | use Page; |
||
0 ignored issues
–
show
|
|||
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
The property
$ShowInSearch was declared of type string , but true is of type true . 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;
![]() |
|||
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;
![]() |
|||
333 | $file->write(); |
||
334 | |||
335 | // will get added |
||
336 | $objOneA = new SolrIndexTest_MyDataObjectOne(); |
||
337 | $objOneA->Title = 'Test MyDataObjectOne true'; |
||
338 | $objOneA->ShowInSearch = true; |
||
0 ignored issues
–
show
The property
ShowInSearch does not exist on SilverStripe\FullTextSea...dexTest_MyDataObjectOne . Since you implemented __set , consider adding a @property annotation.
![]() |
|||
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; |
||
0 ignored issues
–
show
The property
ShowInSearch does not exist on SilverStripe\FullTextSea...dexTest_MyDataObjectTwo . Since you implemented __set , consider adding a @property annotation.
![]() |
|||
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 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"]
, you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths