Issues (186)

tests/SolrReindexTest.php (5 issues)

1
<?php
2
3
namespace SilverStripe\FullTextSearch\Tests;
4
5
use Apache_Solr_Document;
6
use Page;
0 ignored issues
show
The type Page was not found. Maybe you did not declare it correctly or list all dependencies?

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:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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
Documentation Bug introduced by
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;
Loading history...
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
Documentation Bug introduced by
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;
0 ignored issues
show
Bug Best Practice introduced by
The property ShowInSearch does not exist on SilverStripe\FullTextSea...dexTest_MyDataObjectOne. Since you implemented __set, consider adding a @property annotation.
Loading history...
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
Bug Best Practice introduced by
The property ShowInSearch does not exist on SilverStripe\FullTextSea...dexTest_MyDataObjectTwo. Since you implemented __set, consider adding a @property annotation.
Loading history...
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