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\local\statistics;
18
 
19
defined('MOODLE_INTERNAL') || die();
20
 
21
use advanced_testcase;
22
use context;
23
use context_module;
24
use core_question\statistics\questions\all_calculated_for_qubaid_condition;
25
use quiz_statistics\tests\statistics_helper;
26
use core_question_generator;
27
use Generator;
28
use mod_quiz\quiz_attempt;
29
use mod_quiz\quiz_settings;
30
use question_engine;
31
use ReflectionMethod;
32
 
33
global $CFG;
34
require_once($CFG->dirroot . '/mod/quiz/tests/quiz_question_helper_test_trait.php');
35
 
36
/**
37
 * Tests for question statistics.
38
 *
39
 * @package   core_question
40
 * @copyright 2021 Catalyst IT Australia Pty Ltd
41
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42
 * @covers \core_question\local\statistics\statistics_bulk_loader
43
 */
44
class statistics_bulk_loader_test extends advanced_testcase {
45
 
46
    use \quiz_question_helper_test_trait;
47
 
48
    /** @var float Delta used when comparing statistics values out-of 1. */
49
    protected const DELTA = 0.00005;
50
 
51
    /** @var float Delta used when comparing statistics values out-of 100. */
52
    protected const PERCENT_DELTA = 0.005;
53
 
54
    /**
55
     * Test quizzes that contain a specified question.
56
     *
57
     * @covers ::get_all_places_where_questions_were_attempted
58
     */
59
    public function test_get_all_places_where_questions_were_attempted(): void {
60
        global $DB;
61
        $this->resetAfterTest();
62
        $this->setAdminUser();
63
 
64
        $rcm = new ReflectionMethod(statistics_bulk_loader::class, 'get_all_places_where_questions_were_attempted');
65
 
66
        // Create a course.
67
        $course = $this->getDataGenerator()->create_course();
68
 
69
        // Create three quizzes.
70
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
71
        $quiz1 = $quizgenerator->create_instance([
72
            'course' => $course->id,
73
            'grade' => 100.0, 'sumgrades' => 2,
74
            'layout' => '1,2,0'
75
        ]);
76
        $quiz1context = context_module::instance($quiz1->cmid);
77
 
78
        $quiz2 = $quizgenerator->create_instance([
79
            'course' => $course->id,
80
            'grade' => 100.0, 'sumgrades' => 2,
81
            'layout' => '1,2,0'
82
        ]);
83
        $quiz2context = context_module::instance($quiz2->cmid);
84
 
85
        $quiz3 = $quizgenerator->create_instance([
86
            'course' => $course->id,
87
            'grade' => 100.0, 'sumgrades' => 2,
88
            'layout' => '1,2,0'
89
        ]);
90
        $quiz3context = context_module::instance($quiz3->cmid);
91
 
92
        // Create questions.
93
        /** @var core_question_generator $questiongenerator */
94
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
95
        $cat = $questiongenerator->create_question_category();
96
        $question1 = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
97
        $question2 = $questiongenerator->create_question('numerical', null, ['category' => $cat->id]);
98
 
99
        // Add question 1 to quiz 1 and make an attempt.
100
        quiz_add_quiz_question($question1->id, $quiz1);
101
        // Quiz 1 attempt.
102
        $this->submit_quiz($quiz1, [1 => ['answer' => 'frog']]);
103
 
104
        // Add questions 1 and 2 to quiz 2.
105
        quiz_add_quiz_question($question1->id, $quiz2);
106
        quiz_add_quiz_question($question2->id, $quiz2);
107
        $this->submit_quiz($quiz2, [1 => ['answer' => 'frog'], 2 => ['answer' => 10]]);
108
 
109
        // Checking quizzes that use question 1.
110
        $q1places = $rcm->invoke(null, [$question1->id]);
111
        $this->assertCount(2, $q1places);
112
        $this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz1context->id], $q1places[0]);
113
        $this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz2context->id], $q1places[1]);
114
 
115
        // Checking quizzes that contain question 2.
116
        $q2places = $rcm->invoke(null, [$question2->id]);
117
        $this->assertCount(1, $q2places);
118
        $this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz2context->id], $q2places[0]);
119
 
120
        // Add a random question to quiz3.
121
        $this->add_random_questions($quiz3->id, 0, $cat->id, 1, false);
122
        $this->submit_quiz($quiz3, [1 => ['answer' => 'willbewrong']]);
123
 
124
        // Quiz 3 will now be in one of these arrays.
125
        $q1places = $rcm->invoke(null, [$question1->id]);
126
        $q2places = $rcm->invoke(null, [$question2->id]);
127
        if (count($q1places) == 3) {
128
            $newplace = end($q1places);
129
        } else {
130
            $newplace = end($q2places);
131
        }
132
        $this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz3context->id], $newplace);
133
 
134
        // Simulate the situation where the context for quiz3 is gone from the database, without
135
        // the corresponding attempt data being properly cleaned up. Ensure this does not cause errors.
136
        $DB->delete_records('context', ['id' => context_module::instance($quiz3->cmid)->id]);
137
        accesslib_clear_all_caches_for_unit_testing();
138
        // Same asserts as above, before we added quiz3.
139
        $q1places = $rcm->invoke(null, [$question1->id]);
140
        $this->assertCount(2, $q1places);
141
        $this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz1context->id], $q1places[0]);
142
        $this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz2context->id], $q1places[1]);
143
        $q2places = $rcm->invoke(null, [$question2->id]);
144
        $this->assertCount(1, $q2places);
145
        $this->assertEquals((object) ['component' => 'mod_quiz', 'contextid' => $quiz2context->id], $q2places[0]);
146
    }
147
 
148
    /**
149
     * Create 2 quizzes.
150
     *
151
     * @return array return 2 quizzes
152
     */
153
    private function prepare_quizzes(): array {
154
        // Create a course.
155
        $course = $this->getDataGenerator()->create_course();
156
 
157
        // Make 2 quizzes.
158
        $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
159
        $layout = '1,2,0,3,4,0';
160
        $quiz1 = $quizgenerator->create_instance([
161
            'course' => $course->id,
162
            'grade' => 100.0, 'sumgrades' => 2,
163
            'layout' => $layout
164
        ]);
165
 
166
        $quiz2 = $quizgenerator->create_instance([
167
            'course' => $course->id,
168
            'grade' => 100.0, 'sumgrades' => 2,
169
            'layout' => $layout
170
        ]);
171
 
172
        /** @var core_question_generator $questiongenerator */
173
        $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
174
        $cat = $questiongenerator->create_question_category();
175
 
176
        $page = 1;
177
        $questions = [];
178
        foreach (explode(',', $layout) as $slot) {
179
            if ($slot == 0) {
180
                $page += 1;
181
                continue;
182
            }
183
 
184
            $question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
185
            $questions[$slot] = $question;
186
            quiz_add_quiz_question($question->id, $quiz1, $page);
187
            quiz_add_quiz_question($question->id, $quiz2, $page);
188
        }
189
 
190
        return [$quiz1, $quiz2, $questions];
191
    }
192
 
193
    /**
194
     * Submit quiz answers
195
     *
196
     * @param object $quiz
197
     * @param array $answers
198
     */
199
    private function submit_quiz(object $quiz, array $answers): void {
200
        // Create user.
201
        $user = $this->getDataGenerator()->create_user();
202
        // Create attempt.
203
        $quizobj = quiz_settings::create($quiz->id, $user->id);
204
        $quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
205
        $quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
206
        $timenow = time();
207
        $attempt = quiz_create_attempt($quizobj, 1, null, $timenow, false, $user->id);
208
        quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
209
        quiz_attempt_save_started($quizobj, $quba, $attempt);
210
        // Submit attempt.
211
        $attemptobj = quiz_attempt::create($attempt->id);
212
        $attemptobj->process_submitted_actions($timenow, false, $answers);
213
        $attemptobj->process_finish($timenow, false);
214
    }
215
 
216
    /**
217
     * Generate attempt answers.
218
     *
219
     * @param array $correctanswerflags array of 1 or 0
220
     * 1 : generate correct answer
221
     * 0 : generate wrong answer
222
     *
223
     * @return array
224
     */
225
    private function generate_attempt_answers(array $correctanswerflags): array {
226
        $attempt = [];
227
        for ($i = 1; $i <= 4; $i++) {
228
            if (isset($correctanswerflags) && $correctanswerflags[$i - 1] == 1) {
229
                // Correct answer.
230
                $attempt[$i] = ['answer' => 'frog'];
231
            } else {
232
                $attempt[$i] = ['answer' => 'false'];
233
            }
234
        }
235
        return $attempt;
236
    }
237
 
238
    /**
239
     * Generate quizzes and submit answers.
240
     *
241
     * @param array $quiz1attempts quiz 1 attempts
242
     * @param array $quiz2attempts quiz 2 attempts
243
     *
244
     * @return array
245
     */
246
    private function prepare_and_submit_quizzes(array $quiz1attempts, array $quiz2attempts): array {
247
        list($quiz1, $quiz2, $questions) = $this->prepare_quizzes();
248
        // Submit attempts of quiz1.
249
        foreach ($quiz1attempts as $attempt) {
250
            $this->submit_quiz($quiz1, $attempt);
251
        }
252
        // Submit attempts of quiz2.
253
        foreach ($quiz2attempts as $attempt) {
254
            $this->submit_quiz($quiz2, $attempt);
255
        }
256
 
257
        // Calculate the statistics.
258
        $this->expectOutputRegex('~.*Calculations completed.*~');
259
        statistics_helper::run_pending_recalculation_tasks();
260
 
261
        return [$quiz1, $quiz2, $questions];
262
    }
263
 
264
    /**
265
     * To use private helper::extract_item_value function.
266
     *
267
     * @param all_calculated_for_qubaid_condition $statistics the batch of statistics.
268
     * @param int $questionid a question id.
269
     * @param string $item one of the field names in all_calculated_for_qubaid_condition, e.g. 'facility'.
270
     * @return float|null the required value.
271
     */
272
    private function extract_item_value(all_calculated_for_qubaid_condition $statistics,
273
                                        int $questionid, string $item): ?float {
274
        $rcm = new ReflectionMethod(statistics_bulk_loader::class, 'extract_item_value');
275
        return $rcm->invoke(null, $statistics, $questionid, $item);
276
    }
277
 
278
    /**
279
     * To use private helper::load_statistics_for_place function (with mod_quiz component).
280
     *
281
     * @param context $context the context to load the statistics for.
282
     * @return all_calculated_for_qubaid_condition|null question statistics.
283
     */
284
    private function load_quiz_statistics_for_place(context $context): ?all_calculated_for_qubaid_condition {
285
        $rcm = new ReflectionMethod(statistics_bulk_loader::class, 'load_statistics_for_place');
286
        return $rcm->invoke(null, 'mod_quiz', $context);
287
    }
288
 
289
    /**
290
     * Data provider for {@see test_load_question_facility()}.
291
     *
292
     * @return Generator
293
     */
294
    public function load_question_facility_provider(): Generator {
295
        yield 'Facility case 1' => [
296
            'Quiz 1 attempts' => [
297
                $this->generate_attempt_answers([1, 0, 0, 0]),
298
            ],
299
            'Expected quiz 1 facilities' => [1.0, 0.0, 0.0, 0.0],
300
            'Quiz 2 attempts' => [
301
                $this->generate_attempt_answers([1, 0, 0, 0]),
302
                $this->generate_attempt_answers([1, 1, 0, 0]),
303
            ],
304
            'Expected quiz 2 facilities' => [1.0, 0.5, 0.0, 0.0],
305
            'Expected average facilities' => [1.0, 0.25, 0.0, 0.0],
306
        ];
307
        yield 'Facility case 2' => [
308
            'Quiz 1 attempts' => [
309
                $this->generate_attempt_answers([1, 0, 0, 0]),
310
                $this->generate_attempt_answers([1, 1, 0, 0]),
311
                $this->generate_attempt_answers([1, 1, 1, 0]),
312
            ],
313
            'Expected quiz 1 facilities' => [1.0, 0.6667, 0.3333, 0.0],
314
            'Quiz 2 attempts' => [
315
                $this->generate_attempt_answers([1, 0, 0, 0]),
316
                $this->generate_attempt_answers([1, 1, 0, 0]),
317
                $this->generate_attempt_answers([1, 1, 1, 0]),
318
                $this->generate_attempt_answers([1, 1, 1, 1]),
319
            ],
320
            'Expected quiz 2 facilities' => [1.0, 0.75, 0.5, 0.25],
321
            'Expected average facilities' => [1.0, 0.7083, 0.4167, 0.1250],
322
        ];
323
    }
324
 
325
    /**
326
     * Test question facility
327
     *
328
     * @dataProvider load_question_facility_provider
329
     *
330
     * @param array $quiz1attempts quiz 1 attempts
331
     * @param array $expectedquiz1facilities expected quiz 1 facilities
332
     * @param array $quiz2attempts quiz 2 attempts
333
     * @param array $expectedquiz2facilities  expected quiz 2 facilities
334
     * @param array $expectedaveragefacilities expected average facilities
335
     */
336
    public function test_load_question_facility(
337
        array $quiz1attempts,
338
        array $expectedquiz1facilities,
339
        array $quiz2attempts,
340
        array $expectedquiz2facilities,
341
        array $expectedaveragefacilities
342
    ): void {
343
        $this->resetAfterTest();
344
 
345
        list($quiz1, $quiz2, $questions) = $this->prepare_and_submit_quizzes($quiz1attempts, $quiz2attempts);
346
 
347
        // Quiz 1 facilities.
348
        $stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz1->cmid));
349
        $quiz1facility1 = $this->extract_item_value($stats, $questions[1]->id, 'facility');
350
        $quiz1facility2 = $this->extract_item_value($stats, $questions[2]->id, 'facility');
351
        $quiz1facility3 = $this->extract_item_value($stats, $questions[3]->id, 'facility');
352
        $quiz1facility4 = $this->extract_item_value($stats, $questions[4]->id, 'facility');
353
 
354
        $this->assertEqualsWithDelta($expectedquiz1facilities[0], $quiz1facility1, self::DELTA);
355
        $this->assertEqualsWithDelta($expectedquiz1facilities[1], $quiz1facility2, self::DELTA);
356
        $this->assertEqualsWithDelta($expectedquiz1facilities[2], $quiz1facility3, self::DELTA);
357
        $this->assertEqualsWithDelta($expectedquiz1facilities[3], $quiz1facility4, self::DELTA);
358
 
359
        // Quiz 2 facilities.
360
        $stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz2->cmid));
361
        $quiz2facility1 = $this->extract_item_value($stats, $questions[1]->id, 'facility');
362
        $quiz2facility2 = $this->extract_item_value($stats, $questions[2]->id, 'facility');
363
        $quiz2facility3 = $this->extract_item_value($stats, $questions[3]->id, 'facility');
364
        $quiz2facility4 = $this->extract_item_value($stats, $questions[4]->id, 'facility');
365
 
366
        $this->assertEqualsWithDelta($expectedquiz2facilities[0], $quiz2facility1, self::DELTA);
367
        $this->assertEqualsWithDelta($expectedquiz2facilities[1], $quiz2facility2, self::DELTA);
368
        $this->assertEqualsWithDelta($expectedquiz2facilities[2], $quiz2facility3, self::DELTA);
369
        $this->assertEqualsWithDelta($expectedquiz2facilities[3], $quiz2facility4, self::DELTA);
370
 
371
        // Average question facilities.
372
        $stats = statistics_bulk_loader::load_aggregate_statistics(
373
            [$questions[1]->id, $questions[2]->id, $questions[3]->id, $questions[4]->id],
374
            ['facility']
375
        );
376
 
377
        $this->assertEqualsWithDelta($expectedaveragefacilities[0],
378
            $stats[$questions[1]->id]['facility'], self::DELTA);
379
        $this->assertEqualsWithDelta($expectedaveragefacilities[1],
380
            $stats[$questions[2]->id]['facility'], self::DELTA);
381
        $this->assertEqualsWithDelta($expectedaveragefacilities[2],
382
            $stats[$questions[3]->id]['facility'], self::DELTA);
383
        $this->assertEqualsWithDelta($expectedaveragefacilities[3],
384
            $stats[$questions[4]->id]['facility'], self::DELTA);
385
    }
386
 
387
    /**
388
     * Data provider for {@see test_load_question_discriminative_efficiency()}.
389
     * @return Generator
390
     */
391
    public function load_question_discriminative_efficiency_provider(): Generator {
392
        yield 'Discriminative efficiency' => [
393
            'Quiz 1 attempts' => [
394
                $this->generate_attempt_answers([1, 0, 0, 0]),
395
                $this->generate_attempt_answers([1, 1, 0, 0]),
396
                $this->generate_attempt_answers([1, 0, 1, 0]),
397
                $this->generate_attempt_answers([1, 1, 1, 1]),
398
            ],
399
            'Expected quiz 1 discriminative efficiency' => [null, 33.33, 33.33, 100.00],
400
            'Quiz 2 attempts' => [
401
                $this->generate_attempt_answers([1, 1, 1, 1]),
402
                $this->generate_attempt_answers([0, 0, 0, 0]),
403
                $this->generate_attempt_answers([1, 0, 0, 1]),
404
                $this->generate_attempt_answers([0, 1, 1, 0]),
405
            ],
406
            'Expected quiz 2 discriminative efficiency' => [50.00, 50.00, 50.00, 50.00],
407
            'Expected average discriminative efficiency' => [50.00, 41.67, 41.67, 75.00],
408
        ];
409
    }
410
 
411
    /**
412
     * Test discriminative efficiency
413
     *
414
     * @dataProvider load_question_discriminative_efficiency_provider
415
     *
416
     * @param array $quiz1attempts quiz 1 attempts
417
     * @param array $expectedquiz1discriminativeefficiency expected quiz 1 discriminative efficiency
418
     * @param array $quiz2attempts quiz 2 attempts
419
     * @param array $expectedquiz2discriminativeefficiency expected quiz 2 discriminative efficiency
420
     * @param array $expectedaveragediscriminativeefficiency expected average discriminative efficiency
421
     */
422
    public function test_load_question_discriminative_efficiency(
423
        array $quiz1attempts,
424
        array $expectedquiz1discriminativeefficiency,
425
        array $quiz2attempts,
426
        array $expectedquiz2discriminativeefficiency,
427
        array $expectedaveragediscriminativeefficiency
428
    ): void {
429
        $this->resetAfterTest();
430
 
431
        list($quiz1, $quiz2, $questions) = $this->prepare_and_submit_quizzes($quiz1attempts, $quiz2attempts);
432
 
433
        // Quiz 1 discriminative efficiency.
434
        $stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz1->cmid));
435
        $discriminativeefficiency1 = $this->extract_item_value($stats, $questions[1]->id, 'discriminativeefficiency');
436
        $discriminativeefficiency2 = $this->extract_item_value($stats, $questions[2]->id, 'discriminativeefficiency');
437
        $discriminativeefficiency3 = $this->extract_item_value($stats, $questions[3]->id, 'discriminativeefficiency');
438
        $discriminativeefficiency4 = $this->extract_item_value($stats, $questions[4]->id, 'discriminativeefficiency');
439
 
440
        $this->assertEqualsWithDelta($expectedquiz1discriminativeefficiency[0],
441
                $discriminativeefficiency1, self::PERCENT_DELTA);
442
        $this->assertEqualsWithDelta($expectedquiz1discriminativeefficiency[1],
443
                $discriminativeefficiency2, self::PERCENT_DELTA);
444
        $this->assertEqualsWithDelta($expectedquiz1discriminativeefficiency[2],
445
                $discriminativeefficiency3, self::PERCENT_DELTA);
446
        $this->assertEqualsWithDelta($expectedquiz1discriminativeefficiency[3],
447
                $discriminativeefficiency4, self::PERCENT_DELTA);
448
 
449
        // Quiz 2 discriminative efficiency.
450
        $stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz2->cmid));
451
        $discriminativeefficiency1 = $this->extract_item_value($stats, $questions[1]->id, 'discriminativeefficiency');
452
        $discriminativeefficiency2 = $this->extract_item_value($stats, $questions[2]->id, 'discriminativeefficiency');
453
        $discriminativeefficiency3 = $this->extract_item_value($stats, $questions[3]->id, 'discriminativeefficiency');
454
        $discriminativeefficiency4 = $this->extract_item_value($stats, $questions[4]->id, 'discriminativeefficiency');
455
 
456
        $this->assertEqualsWithDelta($expectedquiz2discriminativeefficiency[0],
457
                $discriminativeefficiency1, self::PERCENT_DELTA);
458
        $this->assertEqualsWithDelta($expectedquiz2discriminativeefficiency[1],
459
                $discriminativeefficiency2, self::PERCENT_DELTA);
460
        $this->assertEqualsWithDelta($expectedquiz2discriminativeefficiency[2],
461
                $discriminativeefficiency3, self::PERCENT_DELTA);
462
        $this->assertEqualsWithDelta($expectedquiz2discriminativeefficiency[3],
463
                $discriminativeefficiency4, self::PERCENT_DELTA);
464
 
465
        // Average question discriminative efficiency.
466
        $stats = statistics_bulk_loader::load_aggregate_statistics(
467
            [$questions[1]->id, $questions[2]->id, $questions[3]->id, $questions[4]->id],
468
            ['discriminativeefficiency']
469
        );
470
 
471
        $this->assertEqualsWithDelta($expectedaveragediscriminativeefficiency[0],
472
            $stats[$questions[1]->id]['discriminativeefficiency'], self::PERCENT_DELTA);
473
        $this->assertEqualsWithDelta($expectedaveragediscriminativeefficiency[1],
474
            $stats[$questions[2]->id]['discriminativeefficiency'], self::PERCENT_DELTA);
475
        $this->assertEqualsWithDelta($expectedaveragediscriminativeefficiency[2],
476
            $stats[$questions[3]->id]['discriminativeefficiency'], self::PERCENT_DELTA);
477
        $this->assertEqualsWithDelta($expectedaveragediscriminativeefficiency[3],
478
            $stats[$questions[4]->id]['discriminativeefficiency'], self::PERCENT_DELTA);
479
    }
480
 
481
    /**
482
     * Data provider for {@see test_load_question_discrimination_index()}.
483
     * @return Generator
484
     */
485
    public function load_question_discrimination_index_provider(): Generator {
486
        yield 'Discrimination Index' => [
487
            'Quiz 1 attempts' => [
488
                $this->generate_attempt_answers([1, 0, 0, 0]),
489
                $this->generate_attempt_answers([1, 1, 0, 0]),
490
                $this->generate_attempt_answers([1, 0, 1, 0]),
491
                $this->generate_attempt_answers([1, 1, 1, 1]),
492
            ],
493
            'Expected quiz 1 Discrimination Index' => [null, 30.15, 30.15, 81.65],
494
            'Quiz 2 attempts' => [
495
                $this->generate_attempt_answers([1, 1, 1, 1]),
496
                $this->generate_attempt_answers([0, 0, 0, 0]),
497
                $this->generate_attempt_answers([1, 0, 0, 1]),
498
                $this->generate_attempt_answers([0, 1, 1, 0]),
499
            ],
500
            'Expected quiz 2 discrimination Index' => [44.72, 44.72, 44.72, 44.72],
501
            'Expected average discrimination Index' => [44.72, 37.44, 37.44, 63.19],
502
        ];
503
    }
504
 
505
    /**
506
     * Test discrimination index
507
     *
508
     * @dataProvider load_question_discrimination_index_provider
509
     *
510
     * @param array $quiz1attempts quiz 1 attempts
511
     * @param array $expectedquiz1discriminationindex expected quiz 1 discrimination index
512
     * @param array $quiz2attempts quiz 2 attempts
513
     * @param array $expectedquiz2discriminationindex expected quiz 2 discrimination index
514
     * @param array $expectedaveragediscriminationindex expected average discrimination index
515
     */
516
    public function test_load_question_discrimination_index(
517
        array $quiz1attempts,
518
        array $expectedquiz1discriminationindex,
519
        array $quiz2attempts,
520
        array $expectedquiz2discriminationindex,
521
        array $expectedaveragediscriminationindex
522
    ): void {
523
        $this->resetAfterTest();
524
 
525
        list($quiz1, $quiz2, $questions) = $this->prepare_and_submit_quizzes($quiz1attempts, $quiz2attempts);
526
 
527
        // Quiz 1 discrimination index.
528
        $stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz1->cmid));
529
        $discriminationindex1 = $this->extract_item_value($stats, $questions[1]->id, 'discriminationindex');
530
        $discriminationindex2 = $this->extract_item_value($stats, $questions[2]->id, 'discriminationindex');
531
        $discriminationindex3 = $this->extract_item_value($stats, $questions[3]->id, 'discriminationindex');
532
        $discriminationindex4 = $this->extract_item_value($stats, $questions[4]->id, 'discriminationindex');
533
 
534
        $this->assertEqualsWithDelta($expectedquiz1discriminationindex[0],
535
            $discriminationindex1, self::PERCENT_DELTA);
536
        $this->assertEqualsWithDelta($expectedquiz1discriminationindex[1],
537
            $discriminationindex2, self::PERCENT_DELTA);
538
        $this->assertEqualsWithDelta($expectedquiz1discriminationindex[2],
539
            $discriminationindex3, self::PERCENT_DELTA);
540
        $this->assertEqualsWithDelta($expectedquiz1discriminationindex[3],
541
            $discriminationindex4, self::PERCENT_DELTA);
542
 
543
        // Quiz 2 discrimination index.
544
        $stats = $this->load_quiz_statistics_for_place(context_module::instance($quiz2->cmid));
545
        $discriminationindex1 = $this->extract_item_value($stats, $questions[1]->id, 'discriminationindex');
546
        $discriminationindex2 = $this->extract_item_value($stats, $questions[2]->id, 'discriminationindex');
547
        $discriminationindex3 = $this->extract_item_value($stats, $questions[3]->id, 'discriminationindex');
548
        $discriminationindex4 = $this->extract_item_value($stats, $questions[4]->id, 'discriminationindex');
549
 
550
        $this->assertEqualsWithDelta($expectedquiz2discriminationindex[0],
551
            $discriminationindex1, self::PERCENT_DELTA);
552
        $this->assertEqualsWithDelta($expectedquiz2discriminationindex[1],
553
            $discriminationindex2, self::PERCENT_DELTA);
554
        $this->assertEqualsWithDelta($expectedquiz2discriminationindex[2],
555
            $discriminationindex3, self::PERCENT_DELTA);
556
        $this->assertEqualsWithDelta($expectedquiz2discriminationindex[3],
557
            $discriminationindex4, self::PERCENT_DELTA);
558
 
559
        // Average question discrimination index.
560
        $stats = statistics_bulk_loader::load_aggregate_statistics(
561
            [$questions[1]->id, $questions[2]->id, $questions[3]->id, $questions[4]->id],
562
            ['discriminationindex']
563
        );
564
 
565
        $this->assertEqualsWithDelta($expectedaveragediscriminationindex[0],
566
            $stats[$questions[1]->id]['discriminationindex'], self::PERCENT_DELTA);
567
        $this->assertEqualsWithDelta($expectedaveragediscriminationindex[1],
568
            $stats[$questions[2]->id]['discriminationindex'], self::PERCENT_DELTA);
569
        $this->assertEqualsWithDelta($expectedaveragediscriminationindex[2],
570
            $stats[$questions[3]->id]['discriminationindex'], self::PERCENT_DELTA);
571
        $this->assertEqualsWithDelta($expectedaveragediscriminationindex[3],
572
            $stats[$questions[4]->id]['discriminationindex'], self::PERCENT_DELTA);
573
    }
574
 
575
    /**
576
     * Test with question statistics disabled
577
     */
578
    public function test_statistics_disabled(): void {
579
        $this->resetAfterTest();
580
 
581
        // Prepare some quizzes and attempts. Exactly what is not important to this test.
582
        $quiz1attempts = [$this->generate_attempt_answers([1, 0, 0, 0])];
583
        $quiz2attempts = [$this->generate_attempt_answers([1, 1, 1, 1])];
584
        [, , $questions] = $this->prepare_and_submit_quizzes($quiz1attempts, $quiz2attempts);
585
 
586
        // Prepare some useful arrays.
587
        $expectedstats = [
588
            $questions[1]->id => [],
589
            $questions[2]->id => [],
590
            $questions[3]->id => [],
591
            $questions[4]->id => [],
592
        ];
593
        $questionids = array_keys($expectedstats);
594
 
595
        // Ask to load no statistics at all.
596
        $stats = statistics_bulk_loader::load_aggregate_statistics($questionids, []);
597
 
598
        // Verify we got the right thing.
599
        $this->assertEquals($expectedstats, $stats);
600
    }
601
}