Proyectos de Subversion Moodle

Rev

| 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 core_question;
18
 
19
use core_question\local\bank\condition;
20
use core_question\local\bank\question_version_status;
21
use core_question\local\bank\random_question_loader;
22
use core_question_generator;
23
use mod_quiz\quiz_settings;
24
use qubaid_list;
25
use question_bank;
26
use question_engine;
27
use question_filter_test_helper;
28
 
29
defined('MOODLE_INTERNAL') || die();
30
 
31
global $CFG;
32
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
33
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
34
 
35
/**
36
 * Tests for the {@see \core_question\local\bank\random_question_loader} class.
37
 *
38
 * @package   core_question
39
 * @copyright 2015 The Open University
40
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41
 * @covers    \random_question_loader
42
 */
43
final class random_question_loader_test extends \advanced_testcase {
44
 
45
    use \quiz_question_helper_test_trait;
46
 
47
    public function test_empty_category_gives_null(): void {
48
        $this->resetAfterTest();
49
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
50
 
51
        $cat = $generator->create_question_category();
52
        $loader = new random_question_loader(new qubaid_list([]));
53
 
54
        $filters = question_filter_test_helper::create_filters([$cat->id]);
55
        $this->assertNull($loader->get_next_filtered_question_id($filters));
56
 
57
        $filters = question_filter_test_helper::create_filters([$cat->id], 1);
58
        $this->assertNull($loader->get_next_filtered_question_id($filters));
59
    }
60
 
61
    public function test_unknown_category_behaves_like_empty(): void {
62
        // It is up the caller to make sure the category id is valid.
63
        $loader = new random_question_loader(new qubaid_list([]));
64
        $filters = question_filter_test_helper::create_filters([-1], 1);
65
        $this->assertNull($loader->get_next_filtered_question_id($filters));
66
    }
67
 
68
    public function test_descriptions_not_returned(): void {
69
        $this->resetAfterTest();
70
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
71
 
72
        $cat = $generator->create_question_category();
73
        $info = $generator->create_question('description', null, ['category' => $cat->id]);
74
        $loader = new random_question_loader(new qubaid_list([]));
75
 
76
        $filters = question_filter_test_helper::create_filters([$cat->id]);
77
        $this->assertNull($loader->get_next_filtered_question_id($filters));
78
    }
79
 
80
    public function test_hidden_questions_not_returned(): void {
81
        global $DB;
82
        $this->resetAfterTest();
83
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
84
 
85
        $cat = $generator->create_question_category();
86
        $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
87
        $DB->set_field('question_versions', 'status',
88
                \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN, ['questionid' => $question1->id]);
89
        $loader = new random_question_loader(new qubaid_list([]));
90
 
91
        $filters = question_filter_test_helper::create_filters([$cat->id]);
92
        $this->assertNull($loader->get_next_filtered_question_id($filters));
93
    }
94
 
95
    public function test_cloze_subquestions_not_returned(): void {
96
        $this->resetAfterTest();
97
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
98
 
99
        $cat = $generator->create_question_category();
100
        $question1 = $generator->create_question('multianswer', null, ['category' => $cat->id]);
101
        $loader = new random_question_loader(new qubaid_list([]));
102
 
103
        $filters = question_filter_test_helper::create_filters([$cat->id]);
104
        $this->assertEquals($question1->id, $loader->get_next_filtered_question_id($filters));
105
        $this->assertNull($loader->get_next_filtered_question_id($filters));
106
    }
107
 
108
    public function test_random_questions_not_returned(): void {
109
        $this->resetAfterTest();
110
        $this->setAdminUser();
111
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
112
 
113
        $cat = $generator->create_question_category();
114
        $course = $this->getDataGenerator()->create_course();
115
        $quiz = $this->getDataGenerator()->create_module('quiz', ['course' => $course]);
116
        $this->add_random_questions($quiz->id, 1, $cat->id, 1);
117
        $loader = new random_question_loader(new qubaid_list([]));
118
 
119
        $filters = question_filter_test_helper::create_filters([$cat->id]);
120
        $this->assertNull($loader->get_next_filtered_question_id($filters));
121
    }
122
 
123
    public function test_draft_questions_not_returned(): void {
124
        $this->resetAfterTest();
125
        $this->setAdminUser();
126
        /** @var core_question_generator $questiongenerator */
127
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
128
 
129
        // Create a question in draft state.
130
        $category = $questiongenerator->create_question_category();
131
        $questiongenerator->create_question('shortanswer', null,
132
                ['category' => $category->id, 'status' => question_version_status::QUESTION_STATUS_DRAFT]);
133
 
134
        // Try to a random question from that category - should not be one.
135
        $filtercondition = [
136
            'category' => [
137
                'jointype' => condition::JOINTYPE_DEFAULT,
138
                'values' => [$category->id],
139
                'filteroptions' => ['includesubcategories' => false],
140
            ],
141
        ];
142
        $loader = new random_question_loader(new qubaid_list([]));
143
        $this->assertNull($loader->get_next_filtered_question_id($filtercondition));
144
    }
145
 
146
    public function test_questions_with_later_draft_version_is_returned(): void {
147
        $this->resetAfterTest();
148
        $this->setAdminUser();
149
        /** @var core_question_generator $questiongenerator */
150
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
151
 
152
        // Create a question in draft state.
153
        $category = $questiongenerator->create_question_category();
154
        $question = $questiongenerator->create_question('shortanswer', null,
155
                ['questiontext' => 'V1', 'category' => $category->id]);
156
        $questiongenerator->update_question($question, null,
157
                ['questiontext' => 'V2', 'status' => question_version_status::QUESTION_STATUS_DRAFT]);
158
 
159
        // Try to a random question from that category - should get V1.
160
        $filtercondition = [
161
            'category' => [
162
                'jointype' => condition::JOINTYPE_DEFAULT,
163
                'values' => [$category->id],
164
                'filteroptions' => ['includesubcategories' => false],
165
            ],
166
        ];
167
        $loader = new random_question_loader(new qubaid_list([]));
168
        $this->assertEquals($question->id, $loader->get_next_filtered_question_id($filtercondition));
169
    }
170
 
171
    public function test_one_question_category_returns_that_q_then_null(): void {
172
        $this->resetAfterTest();
173
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
174
 
175
        $cat = $generator->create_question_category();
176
        $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
177
        $loader = new random_question_loader(new qubaid_list([]));
178
 
179
        $filters = question_filter_test_helper::create_filters([$cat->id], 1);
180
        $this->assertEquals($question1->id, $loader->get_next_filtered_question_id($filters));
181
 
182
        $filters = question_filter_test_helper::create_filters([$cat->id]);
183
        $this->assertNull($loader->get_next_filtered_question_id($filters));
184
    }
185
 
186
    public function test_two_question_category_returns_both_then_null(): void {
187
        $this->resetAfterTest();
188
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
189
 
190
        $cat = $generator->create_question_category();
191
        $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
192
        $question2 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
193
        $loader = new random_question_loader(new qubaid_list([]));
194
 
195
        $questionids = [];
196
        $filters = question_filter_test_helper::create_filters([$cat->id]);
197
        $questionids[] = $loader->get_next_filtered_question_id($filters);
198
        $questionids[] = $loader->get_next_filtered_question_id($filters);
199
        sort($questionids);
200
        $this->assertEquals([$question1->id, $question2->id], $questionids);
201
 
202
        $filters = question_filter_test_helper::create_filters([$cat->id], 1);
203
        $this->assertNull($loader->get_next_filtered_question_id($filters));
204
    }
205
 
206
    public function test_nested_categories(): void {
207
        $this->resetAfterTest();
208
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
209
 
210
        $cat1 = $generator->create_question_category();
211
        $cat2 = $generator->create_question_category(['parent' => $cat1->id]);
212
        $question1 = $generator->create_question('shortanswer', null, ['category' => $cat1->id]);
213
        $question2 = $generator->create_question('shortanswer', null, ['category' => $cat2->id]);
214
        $loader = new random_question_loader(new qubaid_list([]));
215
 
216
        $filters = question_filter_test_helper::create_filters([$cat2->id], 1);
217
        $this->assertEquals($question2->id, $loader->get_next_filtered_question_id($filters));
218
        $filters = question_filter_test_helper::create_filters([$cat1->id], 1);
219
        $this->assertEquals($question1->id, $loader->get_next_filtered_question_id($filters));
220
 
221
        $filters = question_filter_test_helper::create_filters([$cat1->id]);
222
        $this->assertNull($loader->get_next_filtered_question_id($filters));
223
    }
224
 
225
    public function test_used_question_not_returned_until_later(): void {
226
        $this->resetAfterTest();
227
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
228
 
229
        $cat = $generator->create_question_category();
230
        $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
231
        $question2 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
232
        $loader = new random_question_loader(new qubaid_list([]),
233
                [$question2->id => 2]);
234
 
235
        $filters = question_filter_test_helper::create_filters([$cat->id]);
236
        $this->assertEquals($question1->id, $loader->get_next_filtered_question_id($filters));
237
        $this->assertNull($loader->get_next_filtered_question_id($filters));
238
    }
239
 
240
    public function test_previously_used_question_not_returned_until_later(): void {
241
        $this->resetAfterTest();
242
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
243
 
244
        $cat = $generator->create_question_category();
245
        $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
246
        $question2 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
247
        $quba = question_engine::make_questions_usage_by_activity('test', \context_system::instance());
248
        $quba->set_preferred_behaviour('deferredfeedback');
249
        $question = question_bank::load_question($question2->id);
250
        $quba->add_question($question);
251
        $quba->add_question($question);
252
        $quba->start_all_questions();
253
        question_engine::save_questions_usage_by_activity($quba);
254
 
255
        $loader = new random_question_loader(new qubaid_list([$quba->get_id()]));
256
 
257
        $filters = question_filter_test_helper::create_filters([$cat->id]);
258
        $this->assertEquals($question1->id, $loader->get_next_filtered_question_id($filters));
259
        $this->assertEquals($question2->id, $loader->get_next_filtered_question_id($filters));
260
        $this->assertNull($loader->get_next_filtered_question_id($filters));
261
    }
262
 
263
    public function test_empty_category_does_not_have_question_available(): void {
264
        $this->resetAfterTest();
265
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
266
 
267
        $cat = $generator->create_question_category();
268
        $loader = new random_question_loader(new qubaid_list([]));
269
 
270
        $filters = question_filter_test_helper::create_filters([$cat->id]);
271
        $this->assertFalse($loader->is_filtered_question_available($filters, 1));
272
        $filters = question_filter_test_helper::create_filters([$cat->id], 1);
273
        $this->assertFalse($loader->is_filtered_question_available($filters, 1));
274
    }
275
 
276
    public function test_descriptions_not_available(): void {
277
        $this->resetAfterTest();
278
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
279
 
280
        $cat = $generator->create_question_category();
281
        $info = $generator->create_question('description', null, ['category' => $cat->id]);
282
        $loader = new random_question_loader(new qubaid_list([]));
283
 
284
        $filters = question_filter_test_helper::create_filters([$cat->id]);
285
        $this->assertFalse($loader->is_filtered_question_available($filters, $info->id));
286
        $filters = question_filter_test_helper::create_filters([$cat->id], 1);
287
        $this->assertFalse($loader->is_filtered_question_available($filters, $info->id));
288
    }
289
 
290
    public function test_existing_question_is_available_but_then_marked_used(): void {
291
        $this->resetAfterTest();
292
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
293
 
294
        $cat = $generator->create_question_category();
295
        $question1 = $generator->create_question('shortanswer', null, ['category' => $cat->id]);
296
        $loader = new random_question_loader(new qubaid_list([]));
297
 
298
        $filters = question_filter_test_helper::create_filters([$cat->id]);
299
        $this->assertTrue($loader->is_filtered_question_available($filters, $question1->id));
300
        $this->assertFalse($loader->is_filtered_question_available($filters, $question1->id));
301
 
302
        $this->assertFalse($loader->is_filtered_question_available($filters, -1));
303
    }
304
 
305
    /**
306
     * Data provider for the get_questions test.
307
     *
308
     * @return array testcases.
309
     */
310
    public static function get_questions_test_cases(): array {
311
        return [
312
                'empty category' => [
313
                        'categoryindex' => 'emptycat',
314
                        'includesubcategories' => false,
315
                        'usetagnames' => [],
316
                        'expectedquestionindexes' => []
317
                ],
318
                'single category' => [
319
                        'categoryindex' => 'cat1',
320
                        'includesubcategories' => false,
321
                        'usetagnames' => [],
322
                        'expectedquestionindexes' => ['cat1q1', 'cat1q2']
323
                ],
324
                'include sub category' => [
325
                        'categoryindex' => 'cat1',
326
                        'includesubcategories' => true,
327
                        'usetagnames' => [],
328
                        'expectedquestionindexes' => ['cat1q1', 'cat1q2', 'subcatq1', 'subcatq2']
329
                ],
330
                'single category with tags' => [
331
                        'categoryindex' => 'cat1',
332
                        'includesubcategories' => false,
333
                        'usetagnames' => ['cat1'],
334
                        'expectedquestionindexes' => ['cat1q1']
335
                ],
336
                'include sub category with tag on parent' => [
337
                        'categoryindex' => 'cat1',
338
                        'includesubcategories' => true,
339
                        'usetagnames' => ['cat1'],
340
                        'expectedquestionindexes' => ['cat1q1']
341
                ],
342
                'include sub category with tag on sub' => [
343
                        'categoryindex' => 'cat1',
344
                        'includesubcategories' => true,
345
                        'usetagnames' => ['subcat'],
346
                        'expectedquestionindexes' => ['subcatq1']
347
                ],
348
                'include sub category with same tag on parent and sub' => [
349
                        'categoryindex' => 'cat1',
350
                        'includesubcategories' => true,
351
                        'usetagnames' => ['foo'],
352
                        'expectedquestionindexes' => ['cat1q1', 'subcatq1']
353
                ],
354
                'include sub category with tag not matching' => [
355
                        'categoryindex' => 'cat1',
356
                        'includesubcategories' => true,
357
                        'usetagnames' => ['cat1', 'cat2'],
358
                        'expectedquestionindexes' => []
359
                ]
360
        ];
361
    }
362
 
363
    /**
364
     * Test the get_questions function with various parameter combinations.
365
     *
366
     * This function creates a data set as follows:
367
     *      Category: cat1
368
     *          Question: cat1q1
369
     *              Tags: 'cat1', 'foo'
370
     *          Question: cat1q2
371
     *      Category: cat2
372
     *          Question: cat2q1
373
     *              Tags: 'cat2', 'foo'
374
     *          Question: cat2q2
375
     *      Category: subcat
376
     *          Question: subcatq1
377
     *              Tags: 'subcat', 'foo'
378
     *          Question: subcatq2
379
     *          Parent: cat1
380
     *      Category: emptycat
381
     *
382
     * @dataProvider get_questions_test_cases()
383
     * @param string $categoryindex The named index for the category to use
384
     * @param bool $includesubcategories If the search should include subcategories
385
     * @param string[] $usetagnames The tag names to include in the search
386
     * @param string[] $expectedquestionindexes The questions expected in the result
387
     */
388
    public function test_get_questions_variations(
389
            $categoryindex,
390
            $includesubcategories,
391
            $usetagnames,
392
            $expectedquestionindexes
393
    ): void {
394
        $this->resetAfterTest();
395
 
396
        $categories = [];
397
        $questions = [];
398
        $tagnames = [
399
                'cat1',
400
                'cat2',
401
                'subcat',
402
                'foo'
403
        ];
404
        $collid = \core_tag_collection::get_default();
405
        $tags = \core_tag_tag::create_if_missing($collid, $tagnames);
406
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
407
 
408
        // First category and questions.
409
        [$category, $categoryquestions] = $this->create_category_and_questions(2, ['cat1', 'foo']);
410
        $categories['cat1'] = $category;
411
        $questions['cat1q1'] = $categoryquestions[0];
412
        $questions['cat1q2'] = $categoryquestions[1];
413
        // Second category and questions.
414
        [$category, $categoryquestions] = $this->create_category_and_questions(2, ['cat2', 'foo']);
415
        $categories['cat2'] = $category;
416
        $questions['cat2q1'] = $categoryquestions[0];
417
        $questions['cat2q2'] = $categoryquestions[1];
418
        // Sub category and questions.
419
        [$category, $categoryquestions] = $this->create_category_and_questions(2, ['subcat', 'foo'], $categories['cat1']);
420
        $categories['subcat'] = $category;
421
        $questions['subcatq1'] = $categoryquestions[0];
422
        $questions['subcatq2'] = $categoryquestions[1];
423
        // Empty category.
424
        [$category, $categoryquestions] = $this->create_category_and_questions(0);
425
        $categories['emptycat'] = $category;
426
 
427
        // Generate the arguments for the get_questions function.
428
        $category = $categories[$categoryindex];
429
        $tagids = array_map(function($tagname) use ($tags) {
430
            return $tags[$tagname]->id;
431
        }, $usetagnames);
432
 
433
        $loader = new random_question_loader(new qubaid_list([]));
434
        $filters = question_filter_test_helper::create_filters([$category->id], $includesubcategories, $tagids);
435
        $result = $loader->get_filtered_questions($filters);
436
        // Generate the expected question set.
437
        $expectedquestions = array_map(function($index) use ($questions) {
438
            return $questions[$index];
439
        }, $expectedquestionindexes);
440
 
441
        // Ensure the result matches what was expected.
442
        $this->assertCount(count($expectedquestions), $result);
443
        foreach ($expectedquestions as $question) {
444
            $this->assertEquals($result[$question->id]->id, $question->id);
445
            $this->assertEquals($result[$question->id]->category, $question->category);
446
        }
447
    }
448
 
449
    /**
450
     * get_questions should allow limiting and offsetting of the result set.
451
     */
452
    public function test_get_questions_with_limit_and_offset(): void {
453
        $this->resetAfterTest();
454
        $numberofquestions = 5;
455
        $includesubcategories = false;
456
        $tagids = [];
457
        $limit = 1;
458
        $offset = 0;
459
        $loader = new random_question_loader(new qubaid_list([]));
460
        [$category, $questions] = $this->create_category_and_questions($numberofquestions);
461
 
462
        // Add questionid as key to find them easily later.
463
        $questionsbyid = [];
464
        array_walk($questions, function (&$value) use (&$questionsbyid) {
465
            $questionsbyid[$value->id] = $value;
466
        });
467
        $filters = question_filter_test_helper::create_filters([$category->id], $includesubcategories, $tagids);
468
        for ($i = 0; $i < $numberofquestions; $i++) {
469
            $result = $loader->get_filtered_questions(
470
                    $filters,
471
                    $limit,
472
                    $offset
473
            );
474
 
475
            $this->assertCount($limit, $result);
476
            $actual = array_shift($result);
477
            $expected = $questionsbyid[$actual->id];
478
            $this->assertEquals($expected->id, $actual->id);
479
            $offset++;
480
        }
481
    }
482
 
483
    /**
484
     * get_questions should allow retrieving questions with only a subset of
485
     * fields populated.
486
     */
487
    public function test_get_questions_with_restricted_fields(): void {
488
        $this->resetAfterTest();
489
        $includesubcategories = false;
490
        $tagids = [];
491
        $limit = 10;
492
        $offset = 0;
493
        $fields = ['id', 'name'];
494
        $loader = new random_question_loader(new qubaid_list([]));
495
        [$category, $questions] = $this->create_category_and_questions(1);
496
 
497
        $filters = question_filter_test_helper::create_filters([$category->id], $includesubcategories, $tagids);
498
        $result = $loader->get_filtered_questions(
499
                $filters,
500
                $limit,
501
                $offset,
502
                $fields
503
        );
504
 
505
        $expectedquestion = array_shift($questions);
506
        $actualquestion = array_shift($result);
507
        $actualfields = get_object_vars($actualquestion);
508
        $actualfields = array_keys($actualfields);
509
        sort($actualfields);
510
        sort($fields);
511
 
512
        $this->assertEquals($fields, $actualfields);
513
    }
514
 
515
    /**
516
     * Data provider for the count_questions test.
517
     *
518
     * @return array testcases.
519
     */
520
    public static function count_questions_test_cases(): array {
521
        return [
522
                'empty category' => [
523
                        'categoryindex' => 'emptycat',
524
                        'includesubcategories' => false,
525
                        'usetagnames' => [],
526
                        'expectedcount' => 0
527
                ],
528
                'single category' => [
529
                        'categoryindex' => 'cat1',
530
                        'includesubcategories' => false,
531
                        'usetagnames' => [],
532
                        'expectedcount' => 2
533
                ],
534
                'include sub category' => [
535
                        'categoryindex' => 'cat1',
536
                        'includesubcategories' => true,
537
                        'usetagnames' => [],
538
                        'expectedcount' => 4
539
                ],
540
                'single category with tags' => [
541
                        'categoryindex' => 'cat1',
542
                        'includesubcategories' => false,
543
                        'usetagnames' => ['cat1'],
544
                        'expectedcount' => 1
545
                ],
546
                'include sub category with tag on parent' => [
547
                        'categoryindex' => 'cat1',
548
                        'includesubcategories' => true,
549
                        'usetagnames' => ['cat1'],
550
                        'expectedcount' => 1
551
                ],
552
                'include sub category with tag on sub' => [
553
                        'categoryindex' => 'cat1',
554
                        'includesubcategories' => true,
555
                        'usetagnames' => ['subcat'],
556
                        'expectedcount' => 1
557
                ],
558
                'include sub category with same tag on parent and sub' => [
559
                        'categoryindex' => 'cat1',
560
                        'includesubcategories' => true,
561
                        'usetagnames' => ['foo'],
562
                        'expectedcount' => 2
563
                ],
564
                'include sub category with tag not matching' => [
565
                        'categoryindex' => 'cat1',
566
                        'includesubcategories' => true,
567
                        'usetagnames' => ['cat1', 'cat2'],
568
                        'expectedcount' => 0
569
                ]
570
        ];
571
    }
572
 
573
    /**
574
     * Test the count_questions function with various parameter combinations.
575
     *
576
     * This function creates a data set as follows:
577
     *      Category: cat1
578
     *          Question: cat1q1
579
     *              Tags: 'cat1', 'foo'
580
     *          Question: cat1q2
581
     *      Category: cat2
582
     *          Question: cat2q1
583
     *              Tags: 'cat2', 'foo'
584
     *          Question: cat2q2
585
     *      Category: subcat
586
     *          Question: subcatq1
587
     *              Tags: 'subcat', 'foo'
588
     *          Question: subcatq2
589
     *          Parent: cat1
590
     *      Category: emptycat
591
     *
592
     * @dataProvider count_questions_test_cases()
593
     * @param string $categoryindex The named index for the category to use
594
     * @param bool $includesubcategories If the search should include subcategories
595
     * @param string[] $usetagnames The tag names to include in the search
596
     * @param int $expectedcount The number of questions expected in the result
597
     */
598
    public function test_count_questions_variations(
599
            $categoryindex,
600
            $includesubcategories,
601
            $usetagnames,
602
            $expectedcount
603
    ): void {
604
        $this->resetAfterTest();
605
 
606
        $categories = [];
607
        $questions = [];
608
        $tagnames = [
609
                'cat1',
610
                'cat2',
611
                'subcat',
612
                'foo'
613
        ];
614
        $collid = \core_tag_collection::get_default();
615
        $tags = \core_tag_tag::create_if_missing($collid, $tagnames);
616
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
617
 
618
        // First category and questions.
619
        [$category, $categoryquestions] = $this->create_category_and_questions(2, ['cat1', 'foo']);
620
        $categories['cat1'] = $category;
621
        $questions['cat1q1'] = $categoryquestions[0];
622
        $questions['cat1q2'] = $categoryquestions[1];
623
        // Second category and questions.
624
        [$category, $categoryquestions] = $this->create_category_and_questions(2, ['cat2', 'foo']);
625
        $categories['cat2'] = $category;
626
        $questions['cat2q1'] = $categoryquestions[0];
627
        $questions['cat2q2'] = $categoryquestions[1];
628
        // Sub category and questions.
629
        [$category, $categoryquestions] = $this->create_category_and_questions(2, ['subcat', 'foo'], $categories['cat1']);
630
        $categories['subcat'] = $category;
631
        $questions['subcatq1'] = $categoryquestions[0];
632
        $questions['subcatq2'] = $categoryquestions[1];
633
        // Empty category.
634
        [$category, $categoryquestions] = $this->create_category_and_questions(0);
635
        $categories['emptycat'] = $category;
636
 
637
        // Generate the arguments for the get_questions function.
638
        $category = $categories[$categoryindex];
639
        $tagids = array_map(function($tagname) use ($tags) {
640
            return $tags[$tagname]->id;
641
        }, $usetagnames);
642
 
643
        $filters = question_filter_test_helper::create_filters([$category->id], $includesubcategories, $tagids);
644
        $loader = new random_question_loader(new qubaid_list([]));
645
        $result = $loader->count_filtered_questions($filters);
646
 
647
        // Ensure the result matches what was expected.
648
        $this->assertEquals($expectedcount, $result);
649
    }
650
 
651
    /**
652
     * Create a question category and create questions in that category. Tag
653
     * the first question in each category with the given tags.
654
     *
655
     * @param int $questioncount How many questions to create.
656
     * @param string[] $tagnames The list of tags to use.
657
     * @param stdClass|null $parentcategory The category to set as the parent of the created category.
658
     * @return array The category and questions.
659
     */
660
    protected function create_category_and_questions($questioncount, $tagnames = [], $parentcategory = null): array {
661
        $generator = $this->getDataGenerator()->get_plugin_generator('core_question');
662
 
663
        if ($parentcategory) {
664
            $catparams = ['parent' => $parentcategory->id];
665
        } else {
666
            $catparams = [];
667
        }
668
 
669
        $category = $generator->create_question_category($catparams);
670
        $questions = [];
671
 
672
        for ($i = 0; $i < $questioncount; $i++) {
673
            $questions[] = $generator->create_question('shortanswer', null, ['category' => $category->id]);
674
        }
675
 
676
        if (!empty($tagnames) && !empty($questions)) {
677
            $context = \context::instance_by_id($category->contextid);
678
            \core_tag_tag::set_item_tags('core_question', 'question', $questions[0]->id, $context, $tagnames);
679
        }
680
 
681
        return [$category, $questions];
682
    }
683
}