Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace search_simpledb;
18
 
19
defined('MOODLE_INTERNAL') || die();
20
 
21
global $CFG;
22
require_once($CFG->dirroot . '/search/tests/fixtures/testable_core_search.php');
23
require_once($CFG->dirroot . '/search/tests/fixtures/mock_search_area.php');
24
 
25
/**
26
 * Simple search engine base unit tests.
27
 *
28
 * @package     search_simpledb
29
 * @category    test
30
 * @copyright   2016 David Monllao {@link http://www.davidmonllao.com}
31
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32
 */
33
class engine_test extends \advanced_testcase {
34
 
35
    /**
36
     * @var \core_search::manager
37
     */
38
    protected $search = null;
39
 
40
    /**
41
     * @var \
42
     */
43
    protected $engine = null;
44
 
45
    /**
46
     * @var \core_search_generator
47
     */
48
    protected $generator = null;
49
 
50
    /**
51
     * Initial stuff.
52
     *
53
     * @return void
54
     */
55
    public function setUp(): void {
56
        $this->resetAfterTest();
57
 
58
        if ($this->requires_manual_index_update()) {
59
            // We need to update fulltext index manually, which requires an alter table statement.
60
            $this->preventResetByRollback();
61
        }
62
 
63
        set_config('enableglobalsearch', true);
64
 
65
        // Inject search_simpledb engine into the testable core search as we need to add the mock
66
        // search component to it.
67
 
68
        $this->engine = new \search_simpledb\engine();
69
        $this->search = \testable_core_search::instance($this->engine);
70
 
71
        $this->generator = self::getDataGenerator()->get_plugin_generator('core_search');
72
        $this->generator->setup();
73
 
74
        $this->setAdminUser();
75
    }
76
 
77
    /**
78
     * tearDown
79
     *
80
     * @return void
81
     */
82
    public function tearDown(): void {
83
        // For unit tests before PHP 7, teardown is called even on skip. So only do our teardown if we did setup.
84
        if ($this->generator) {
85
            // Moodle DML freaks out if we don't teardown the temp table after each run.
86
            $this->generator->teardown();
87
            $this->generator = null;
88
        }
89
    }
90
 
91
    /**
92
     * Test indexing process.
93
     *
94
     * @return void
95
     */
96
    public function test_index() {
97
        global $DB;
98
 
99
        $this->add_mock_search_area();
100
 
101
        $record = new \stdClass();
102
        $record->timemodified = time() - 1;
103
        $this->generator->create_record($record);
104
 
105
        // Data gets into the search engine.
106
        $this->assertTrue($this->search->index());
107
 
108
        // Not anymore as everything was already added.
109
        sleep(1);
110
        $this->assertFalse($this->search->index());
111
 
112
        $this->generator->create_record();
113
 
114
        // Indexing again once there is new data.
115
        $this->assertTrue($this->search->index());
116
    }
117
 
118
    /**
119
     * Test search filters.
120
     *
121
     * @return void
122
     */
123
    public function test_search() {
124
        global $USER, $DB;
125
 
126
        $this->add_mock_search_area();
127
 
128
        $this->generator->create_record();
129
        $record = new \stdClass();
130
        $record->title = "Special title";
131
        $this->generator->create_record($record);
132
 
133
        $this->search->index();
134
        $this->update_index();
135
 
136
        $querydata = new \stdClass();
137
        $querydata->q = 'message';
138
        $results = $this->search->search($querydata);
139
        $this->assertCount(2, $results);
140
 
141
        // Based on core_mocksearch\search\indexer.
142
        $this->assertEquals($USER->id, $results[0]->get('userid'));
143
        $this->assertEquals(\context_course::instance(SITEID)->id, $results[0]->get('contextid'));
144
 
145
        // Do a test to make sure we aren't searching non-query fields, like areaid.
146
        $querydata->q = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
147
        $this->assertCount(0, $this->search->search($querydata));
148
        $querydata->q = 'message';
149
 
150
        sleep(1);
151
        $beforeadding = time();
152
        sleep(1);
153
        $this->generator->create_record();
154
        $this->search->index();
155
        $this->update_index();
156
 
157
        // Timestart.
158
        $querydata->timestart = $beforeadding;
159
        $this->assertCount(1, $this->search->search($querydata));
160
 
161
        // Timeend.
162
        unset($querydata->timestart);
163
        $querydata->timeend = $beforeadding;
164
        $this->assertCount(2, $this->search->search($querydata));
165
 
166
        // Title.
167
        unset($querydata->timeend);
168
        $querydata->title = 'Special title';
169
        $this->assertCount(1, $this->search->search($querydata));
170
 
171
        // Course IDs.
172
        unset($querydata->title);
173
        $querydata->courseids = array(SITEID + 1);
174
        $this->assertCount(0, $this->search->search($querydata));
175
 
176
        $querydata->courseids = array(SITEID);
177
        $this->assertCount(3, $this->search->search($querydata));
178
 
179
        // Now try some area-id combinations.
180
        unset($querydata->courseids);
181
        $forumpostareaid = \core_search\manager::generate_areaid('mod_forum', 'post');
182
        $mockareaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
183
 
184
        $querydata->areaids = array($forumpostareaid);
185
        $this->assertCount(0, $this->search->search($querydata));
186
 
187
        $querydata->areaids = array($forumpostareaid, $mockareaid);
188
        $this->assertCount(3, $this->search->search($querydata));
189
 
190
        $querydata->areaids = array($mockareaid);
191
        $this->assertCount(3, $this->search->search($querydata));
192
 
193
        $querydata->areaids = array();
194
        $this->assertCount(3, $this->search->search($querydata));
195
 
196
        // Check that index contents get updated.
197
        $this->generator->delete_all();
198
        $this->search->index(true);
199
        $this->update_index();
200
        unset($querydata->title);
201
        $querydata->q = '';
202
        $this->assertCount(0, $this->search->search($querydata));
203
    }
204
 
205
    /**
206
     * Test delete function
207
     *
208
     * @return void
209
     */
210
    public function test_delete() {
211
 
212
        $this->add_mock_search_area();
213
 
214
        $this->generator->create_record();
215
        $this->generator->create_record();
216
        $this->search->index();
217
        $this->update_index();
218
 
219
        $querydata = new \stdClass();
220
        $querydata->q = 'message';
221
 
222
        $this->assertCount(2, $this->search->search($querydata));
223
 
224
        $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
225
        $this->search->delete_index($areaid);
226
        $this->update_index();
227
        $this->assertCount(0, $this->search->search($querydata));
228
    }
229
 
230
    /**
231
     * Test user is allowed.
232
     *
233
     * @return void
234
     */
235
    public function test_alloweduserid() {
236
 
237
        $this->add_mock_search_area();
238
 
239
        $area = new \core_mocksearch\search\mock_search_area();
240
 
241
        $record = $this->generator->create_record();
242
 
243
        // Get the doc and insert the default doc.
244
        $doc = $area->get_document($record);
245
        $this->engine->add_document($doc);
246
 
247
        $users = array();
248
        $users[] = $this->getDataGenerator()->create_user();
249
        $users[] = $this->getDataGenerator()->create_user();
250
        $users[] = $this->getDataGenerator()->create_user();
251
 
252
        // Add a record that only user 100 can see.
253
        $originalid = $doc->get('id');
254
 
255
        // Now add a custom doc for each user.
256
        foreach ($users as $user) {
257
            $doc = $area->get_document($record);
258
            $doc->set('id', $originalid.'-'.$user->id);
259
            $doc->set('owneruserid', $user->id);
260
            $this->engine->add_document($doc);
261
        }
262
        $this->update_index();
263
 
264
        $this->engine->area_index_complete($area->get_area_id());
265
 
266
        $querydata = new \stdClass();
267
        $querydata->q = 'message';
268
        $querydata->title = $doc->get('title');
269
 
270
        // We are going to go through each user and see if they get the original and the owned doc.
271
        foreach ($users as $user) {
272
            $this->setUser($user);
273
 
274
            $results = $this->search->search($querydata);
275
            $this->assertCount(2, $results);
276
 
277
            $owned = 0;
278
            $notowned = 0;
279
 
280
            // We don't know what order we will get the results in, so we are doing this.
281
            foreach ($results as $result) {
282
                $owneruserid = $result->get('owneruserid');
283
                if (empty($owneruserid)) {
284
                    $notowned++;
285
                    $this->assertEquals(0, $owneruserid);
286
                    $this->assertEquals($originalid, $result->get('id'));
287
                } else {
288
                    $owned++;
289
                    $this->assertEquals($user->id, $owneruserid);
290
                    $this->assertEquals($originalid.'-'.$user->id, $result->get('id'));
291
                }
292
            }
293
 
294
            $this->assertEquals(1, $owned);
295
            $this->assertEquals(1, $notowned);
296
        }
297
 
298
        // Now test a user with no owned results.
299
        $otheruser = $this->getDataGenerator()->create_user();
300
        $this->setUser($otheruser);
301
 
302
        $results = $this->search->search($querydata);
303
        $this->assertCount(1, $results);
304
 
305
        $this->assertEquals(0, $results[0]->get('owneruserid'));
306
        $this->assertEquals($originalid, $results[0]->get('id'));
307
    }
308
 
309
    public function test_delete_by_id() {
310
 
311
        $this->add_mock_search_area();
312
 
313
        $this->generator->create_record();
314
        $this->generator->create_record();
315
        $this->search->index();
316
        $this->update_index();
317
 
318
        $querydata = new \stdClass();
319
 
320
        // Then search to make sure they are there.
321
        $querydata->q = 'message';
322
        $results = $this->search->search($querydata);
323
        $this->assertCount(2, $results);
324
 
325
        $first = reset($results);
326
        $deleteid = $first->get('id');
327
 
328
        $this->engine->delete_by_id($deleteid);
329
        $this->update_index();
330
 
331
        // Check that we don't get a result for it anymore.
332
        $results = $this->search->search($querydata);
333
        $this->assertCount(1, $results);
334
        $result = reset($results);
335
        $this->assertNotEquals($deleteid, $result->get('id'));
336
    }
337
 
338
    /**
339
     * Tries out deleting data for a context or a course.
340
     */
341
    public function test_deleted_contexts_and_courses() {
342
        // Create some courses and activities.
343
        $generator = $this->getDataGenerator();
344
        $course1 = $generator->create_course(['fullname' => 'C1', 'summary' => 'xyzzy']);
345
        $course1page1 = $generator->create_module('page', ['course' => $course1, 'name' => 'C1P1', 'content' => 'xyzzy']);
346
        $generator->create_module('page', ['course' => $course1, 'name' => 'C1P2', 'content' => 'xyzzy']);
347
        $course2 = $generator->create_course(['fullname' => 'C2', 'summary' => 'xyzzy']);
348
        $course2page = $generator->create_module('page', ['course' => $course2, 'name' => 'C2P', 'content' => 'xyzzy']);
349
        $course2pagecontext = \context_module::instance($course2page->cmid);
350
 
351
        $this->search->index();
352
 
353
        // By default we have all data in the index.
354
        $this->assert_raw_index_contents('xyzzy', ['C1', 'C1P1', 'C1P2', 'C2', 'C2P']);
355
 
356
        // Say we delete the course2pagecontext...
357
        $this->engine->delete_index_for_context($course2pagecontext->id);
358
        $this->assert_raw_index_contents('xyzzy', ['C1', 'C1P1', 'C1P2', 'C2']);
359
 
360
        // Now delete the second course...
361
        $this->engine->delete_index_for_course($course2->id);
362
        $this->assert_raw_index_contents('xyzzy', ['C1', 'C1P1', 'C1P2']);
363
 
364
        // Finally let's delete using Moodle functions to check that works. Single context first.
365
        course_delete_module($course1page1->cmid);
366
        $this->assert_raw_index_contents('xyzzy', ['C1', 'C1P2']);
367
        delete_course($course1, false);
368
        $this->assert_raw_index_contents('xyzzy', []);
369
    }
370
 
371
    /**
372
     * Check the contents of the index.
373
     *
374
     * @param string $searchword Word to match within the content field
375
     * @param string[] $expected Array of expected result titles, in alphabetical order
376
     */
377
    protected function assert_raw_index_contents(string $searchword, array $expected) {
378
        global $DB;
379
        $results = $DB->get_records_select('search_simpledb_index',
380
                $DB->sql_like('content', '?'), ['%' . $searchword . '%'], "id, {$DB->sql_order_by_text('title')}");
381
        $titles = array_map(function($x) {
382
            return $x->title;
383
        }, $results);
384
        sort($titles);
385
        $this->assertEquals($expected, $titles);
386
    }
387
 
388
    /**
389
     * Adds a mock search area to the search system.
390
     */
391
    protected function add_mock_search_area() {
392
        $areaid = \core_search\manager::generate_areaid('core_mocksearch', 'mock_search_area');
393
        $this->search->add_search_area($areaid, new \core_mocksearch\search\mock_search_area());
394
    }
395
 
396
    /**
397
     * Updates mssql fulltext index if necessary.
398
     *
399
     * @return bool
400
     */
401
    private function update_index() {
402
        global $DB;
403
 
404
        if (!$this->requires_manual_index_update()) {
405
            return;
406
        }
407
 
408
        $DB->execute("ALTER FULLTEXT INDEX ON {search_simpledb_index} START UPDATE POPULATION");
409
 
410
        $catalogname = $DB->get_prefix() . 'search_simpledb_catalog';
411
        $retries = 0;
412
        do {
413
            // 0.2 seconds.
414
            usleep(200000);
415
 
416
            $record = $DB->get_record_sql("SELECT FULLTEXTCATALOGPROPERTY(cat.name, 'PopulateStatus') AS [PopulateStatus]
417
                                             FROM sys.fulltext_catalogs AS cat
418
                                            WHERE cat.name = ?", array($catalogname));
419
            $retries++;
420
 
421
        } while ($retries < 100 && $record->populatestatus != '0');
422
 
423
        if ($retries === 100) {
424
            // No update after 20 seconds...
425
            $this->fail('Sorry, your SQL server fulltext search index is too slow.');
426
        }
427
    }
428
 
429
    /**
430
     * Mssql with fulltext support requires manual updates.
431
     *
432
     * @return bool
433
     */
434
    private function requires_manual_index_update() {
435
        global $DB;
436
        return ($DB->get_dbfamily() === 'mssql' && $DB->is_fulltext_search_supported());
437
    }
438
}