Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | 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
defined('MOODLE_INTERNAL') || die();
18
 
19
global $CFG;
20
require_once($CFG->libdir.'/completionlib.php');
21
 
22
/**
23
 * Completion tests.
24
 *
25
 * @package    core_completion
26
 * @category   test
27
 * @copyright  2008 Sam Marshall
28
 * @copyright  2013 Frédéric Massart
29
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30
 * @coversDefaultClass \completion_info
31
 */
1441 ariadna 32
final class completionlib_test extends advanced_testcase {
1 efrain 33
    protected $course;
34
    protected $user;
35
    protected $module1;
36
    protected $module2;
37
 
38
    protected function mock_setup() {
39
        global $DB, $CFG, $USER;
40
 
41
        $this->resetAfterTest();
42
 
43
        $DB = $this->createMock(get_class($DB));
44
        $CFG->enablecompletion = COMPLETION_ENABLED;
45
        $USER = (object)array('id' => 314159);
46
    }
47
 
48
    /**
49
     * Create course with user and activities.
50
     */
51
    protected function setup_data() {
52
        global $DB, $CFG;
53
 
54
        $this->resetAfterTest();
55
 
56
        // Enable completion before creating modules, otherwise the completion data is not written in DB.
57
        $CFG->enablecompletion = true;
58
 
59
        // Create a course with activities.
60
        $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
61
        $this->user = $this->getDataGenerator()->create_user();
62
        $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id);
63
 
64
        $this->module1 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id));
65
        $this->module2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id));
66
    }
67
 
68
    /**
69
     * Asserts that two variables are equal.
70
     *
71
     * @param  mixed   $expected
72
     * @param  mixed   $actual
73
     * @param  string  $message
74
     * @param  float   $delta
75
     * @param  integer $maxDepth
76
     * @param  boolean $canonicalize
77
     * @param  boolean $ignoreCase
78
     */
1441 ariadna 79
    public static function assertCompletionEquals($expected, $actual, string $message = '', float $delta = 0, int $maxDepth = 10,
1 efrain 80
                                        bool $canonicalize = false, bool $ignoreCase = false): void {
81
        // Nasty cheating hack: prevent random failures on timemodified field.
82
        if (is_array($actual) && (is_object($expected) || is_array($expected))) {
83
            $actual = (object) $actual;
84
            $expected = (object) $expected;
85
        }
86
        if (is_object($expected) and is_object($actual)) {
87
            if (property_exists($expected, 'timemodified') and property_exists($actual, 'timemodified')) {
88
                if ($expected->timemodified + 1 == $actual->timemodified) {
89
                    $expected = clone($expected);
90
                    $expected->timemodified = $actual->timemodified;
91
                }
92
            }
93
        }
94
        parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
95
    }
96
 
97
    /**
98
     * @covers ::is_enabled_for_site
99
     * @covers ::is_enabled
100
     */
11 efrain 101
    public function test_is_enabled(): void {
1 efrain 102
        global $CFG;
103
        $this->mock_setup();
104
 
105
        // Config alone.
106
        $CFG->enablecompletion = COMPLETION_DISABLED;
1441 ariadna 107
        $this->assertCompletionEquals(COMPLETION_DISABLED, completion_info::is_enabled_for_site());
1 efrain 108
        $CFG->enablecompletion = COMPLETION_ENABLED;
1441 ariadna 109
        $this->assertCompletionEquals(COMPLETION_ENABLED, completion_info::is_enabled_for_site());
1 efrain 110
 
111
        // Course.
112
        $course = (object)array('id' => 13);
113
        $c = new completion_info($course);
114
        $course->enablecompletion = COMPLETION_DISABLED;
1441 ariadna 115
        $this->assertCompletionEquals(COMPLETION_DISABLED, $c->is_enabled());
1 efrain 116
        $course->enablecompletion = COMPLETION_ENABLED;
1441 ariadna 117
        $this->assertCompletionEquals(COMPLETION_ENABLED, $c->is_enabled());
1 efrain 118
        $CFG->enablecompletion = COMPLETION_DISABLED;
1441 ariadna 119
        $this->assertCompletionEquals(COMPLETION_DISABLED, $c->is_enabled());
1 efrain 120
 
121
        // Course and CM.
122
        $cm = new stdClass();
123
        $cm->completion = COMPLETION_TRACKING_MANUAL;
1441 ariadna 124
        $this->assertCompletionEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
1 efrain 125
        $CFG->enablecompletion = COMPLETION_ENABLED;
126
        $course->enablecompletion = COMPLETION_DISABLED;
1441 ariadna 127
        $this->assertCompletionEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
1 efrain 128
        $course->enablecompletion = COMPLETION_ENABLED;
1441 ariadna 129
        $this->assertCompletionEquals(COMPLETION_TRACKING_MANUAL, $c->is_enabled($cm));
1 efrain 130
        $cm->completion = COMPLETION_TRACKING_NONE;
1441 ariadna 131
        $this->assertCompletionEquals(COMPLETION_TRACKING_NONE, $c->is_enabled($cm));
1 efrain 132
        $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
1441 ariadna 133
        $this->assertCompletionEquals(COMPLETION_TRACKING_AUTOMATIC, $c->is_enabled($cm));
1 efrain 134
    }
135
 
136
    /**
137
     * @covers ::update_state
138
     */
11 efrain 139
    public function test_update_state(): void {
1 efrain 140
        $this->mock_setup();
141
 
142
        $mockbuilder = $this->getMockBuilder('completion_info');
143
        $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data',
144
                                       'user_can_override_completion'));
145
        $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
146
        $cm = (object)array('id' => 13, 'course' => 42);
147
 
148
        // Not enabled, should do nothing.
149
        $c = $mockbuilder->getMock();
150
        $c->expects($this->once())
151
            ->method('is_enabled')
152
            ->with($cm)
153
            ->will($this->returnValue(false));
154
        $c->update_state($cm);
155
 
156
        // Enabled, but current state is same as possible result, do nothing.
157
        $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
158
        $c = $mockbuilder->getMock();
159
        $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
160
        $c->expects($this->once())
161
            ->method('is_enabled')
162
            ->with($cm)
163
            ->will($this->returnValue(true));
164
        $c->expects($this->once())
165
            ->method('get_data')
166
            ->will($this->returnValue($current));
167
        $c->update_state($cm, COMPLETION_COMPLETE);
168
 
169
        // Enabled, but current state is a specific one and new state is just
170
        // complete, so do nothing.
171
        $c = $mockbuilder->getMock();
172
        $current->completionstate = COMPLETION_COMPLETE_PASS;
173
        $c->expects($this->once())
174
            ->method('is_enabled')
175
            ->with($cm)
176
            ->will($this->returnValue(true));
177
        $c->expects($this->once())
178
            ->method('get_data')
179
            ->will($this->returnValue($current));
180
        $c->update_state($cm, COMPLETION_COMPLETE);
181
 
182
        // Manual, change state (no change).
183
        $c = $mockbuilder->getMock();
184
        $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
185
        $current->completionstate = COMPLETION_COMPLETE;
186
        $c->expects($this->once())
187
            ->method('is_enabled')
188
            ->with($cm)
189
            ->will($this->returnValue(true));
190
        $c->expects($this->once())
191
            ->method('get_data')
192
            ->will($this->returnValue($current));
193
        $c->update_state($cm, COMPLETION_COMPLETE);
194
 
195
        // Manual, change state (change).
196
        $c = $mockbuilder->getMock();
197
        $c->expects($this->once())
198
            ->method('is_enabled')
199
            ->with($cm)
200
            ->will($this->returnValue(true));
201
        $c->expects($this->once())
202
            ->method('get_data')
203
            ->will($this->returnValue($current));
204
        $changed = clone($current);
205
        $changed->timemodified = time();
206
        $changed->completionstate = COMPLETION_INCOMPLETE;
207
        $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
208
        $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
209
        $c->expects($this->once())
210
            ->method('internal_set_data')
211
            ->with($cm, $comparewith);
212
        $c->update_state($cm, COMPLETION_INCOMPLETE);
213
 
214
        // Auto, change state.
215
        $c = $mockbuilder->getMock();
216
        $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
217
        $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
218
        $c->expects($this->once())
219
            ->method('is_enabled')
220
            ->with($cm)
221
            ->will($this->returnValue(true));
222
        $c->expects($this->once())
223
            ->method('get_data')
224
            ->will($this->returnValue($current));
225
        $c->expects($this->once())
226
            ->method('internal_get_state')
227
            ->will($this->returnValue(COMPLETION_COMPLETE_PASS));
228
        $changed = clone($current);
229
        $changed->timemodified = time();
230
        $changed->completionstate = COMPLETION_COMPLETE_PASS;
231
        $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
232
        $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
233
        $c->expects($this->once())
234
            ->method('internal_set_data')
235
            ->with($cm, $comparewith);
236
        $c->update_state($cm, COMPLETION_COMPLETE_PASS);
237
 
238
        // Manual tracking, change state by overriding it manually.
239
        $c = $mockbuilder->getMock();
240
        $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
241
        $current1 = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
242
        $current2 = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
243
        $c->expects($this->exactly(2))
244
            ->method('is_enabled')
245
            ->with($cm)
246
            ->will($this->returnValue(true));
247
        $c->expects($this->exactly(1)) // Pretend the user has the required capability for overriding completion statuses.
248
            ->method('user_can_override_completion')
249
            ->will($this->returnValue(true));
1441 ariadna 250
        $getinvocations = $this->exactly(2);
251
        $c->expects($getinvocations)
1 efrain 252
            ->method('get_data')
253
            ->with($cm, false, 100)
1441 ariadna 254
            ->willReturnCallback(function () use ($getinvocations, $current1, $current2) {
255
                return match (self::getInvocationCount($getinvocations)) {
256
                    1 => $current1,
257
                    2 => $current2,
258
                    default => $this->fail('Unexpected invocation count'),
259
                };
260
            });
1 efrain 261
        $changed1 = clone($current1);
262
        $changed1->timemodified = time();
263
        $changed1->completionstate = COMPLETION_COMPLETE;
264
        $changed1->overrideby = 314159;
265
        $comparewith1 = new phpunit_constraint_object_is_equal_with_exceptions($changed1);
266
        $comparewith1->add_exception('timemodified', 'assertGreaterThanOrEqual');
267
        $changed2 = clone($current2);
268
        $changed2->timemodified = time();
269
        $changed2->overrideby = null;
270
        $changed2->completionstate = COMPLETION_INCOMPLETE;
271
        $comparewith2 = new phpunit_constraint_object_is_equal_with_exceptions($changed2);
272
        $comparewith2->add_exception('timemodified', 'assertGreaterThanOrEqual');
1441 ariadna 273
        $setinvocations = $this->exactly(2);
274
        $c->expects($setinvocations)
1 efrain 275
            ->method('internal_set_data')
1441 ariadna 276
            ->willReturnCallback(function ($comparecm, $comparewith) use (
277
                $setinvocations,
278
                $cm,
279
                $comparewith1,
280
                $comparewith2
281
            ): void {
282
                switch (self::getInvocationCount($setinvocations)) {
283
                    case 1:
284
                        $this->assertCompletionEquals($cm, $comparecm);
285
                        $comparewith1->evaluate($comparewith);
286
                        break;
287
                    case 2:
288
                        $this->assertCompletionEquals($cm, $comparecm);
289
                        $comparewith2->evaluate($comparewith);
290
                        break;
291
                    default:
292
                        $this->fail('Unexpected invocation count');
293
                }
294
            });
1 efrain 295
        $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
296
        // And confirm that the status can be changed back to incomplete without an override.
297
        $c->update_state($cm, COMPLETION_INCOMPLETE, 100);
298
 
299
        // Auto, change state via override, incomplete to complete.
300
        $c = $mockbuilder->getMock();
301
        $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
302
        $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
303
        $c->expects($this->once())
304
            ->method('is_enabled')
305
            ->with($cm)
306
            ->will($this->returnValue(true));
307
        $c->expects($this->once()) // Pretend the user has the required capability for overriding completion statuses.
308
            ->method('user_can_override_completion')
309
            ->will($this->returnValue(true));
310
        $c->expects($this->once())
311
            ->method('get_data')
312
            ->with($cm, false, 100)
313
            ->will($this->returnValue($current));
314
        $changed = clone($current);
315
        $changed->timemodified = time();
316
        $changed->completionstate = COMPLETION_COMPLETE;
317
        $changed->overrideby = 314159;
318
        $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
319
        $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
320
        $c->expects($this->once())
321
            ->method('internal_set_data')
322
            ->with($cm, $comparewith);
323
        $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
324
 
325
        // Now confirm the status can be changed back from complete to incomplete using an override.
326
        $c = $mockbuilder->getMock();
327
        $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
328
        $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => 2);
329
        $c->expects($this->once())
330
            ->method('is_enabled')
331
            ->with($cm)
332
            ->will($this->returnValue(true));
333
        $c->expects($this->Once()) // Pretend the user has the required capability for overriding completion statuses.
334
            ->method('user_can_override_completion')
335
            ->will($this->returnValue(true));
336
        $c->expects($this->once())
337
            ->method('get_data')
338
            ->with($cm, false, 100)
339
            ->will($this->returnValue($current));
340
        $changed = clone($current);
341
        $changed->timemodified = time();
342
        $changed->completionstate = COMPLETION_INCOMPLETE;
343
        $changed->overrideby = 314159;
344
        $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
345
        $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
346
        $c->expects($this->once())
347
            ->method('internal_set_data')
348
            ->with($cm, $comparewith);
349
        $c->update_state($cm, COMPLETION_INCOMPLETE, 100, true);
350
    }
351
 
352
    /**
353
     * Data provider for test_internal_get_state().
354
     *
355
     * @return array[]
356
     */
1441 ariadna 357
    public static function internal_get_state_provider(): array {
1 efrain 358
        return [
359
            'View required, but not viewed yet' => [
360
                COMPLETION_VIEW_REQUIRED, 1, '', COMPLETION_INCOMPLETE
361
            ],
362
            'View not required and not viewed yet' => [
363
                COMPLETION_VIEW_NOT_REQUIRED, 1, '', COMPLETION_INCOMPLETE
364
            ],
365
            'View not required, grade required but no grade yet, $cm->modname not set' => [
366
                COMPLETION_VIEW_NOT_REQUIRED, 1, 'modname', COMPLETION_INCOMPLETE
367
            ],
368
            'View not required, grade required but no grade yet, $cm->course not set' => [
369
                COMPLETION_VIEW_NOT_REQUIRED, 1, 'course', COMPLETION_INCOMPLETE
370
            ],
371
            'View not required, grade not required' => [
372
                COMPLETION_VIEW_NOT_REQUIRED, 0, '', COMPLETION_COMPLETE
373
            ],
374
        ];
375
    }
376
 
377
    /**
378
     * Test for completion_info::get_state().
379
     *
380
     * @dataProvider internal_get_state_provider
381
     * @param int $completionview
382
     * @param int $completionusegrade
383
     * @param string $unsetfield
384
     * @param int $expectedstate
385
     * @covers ::internal_get_state
386
     */
11 efrain 387
    public function test_internal_get_state(int $completionview, int $completionusegrade, string $unsetfield, int $expectedstate): void {
1 efrain 388
        $this->setup_data();
389
 
390
        /** @var \mod_assign_generator $assigngenerator */
391
        $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
392
        $assign = $assigngenerator->create_instance([
393
            'course' => $this->course->id,
394
            'completion' => COMPLETION_ENABLED,
395
            'completionview' => $completionview,
396
            'completionusegrade' => $completionusegrade,
397
        ]);
398
 
399
        $userid = $this->user->id;
400
        $this->setUser($userid);
401
 
402
        $cm = get_coursemodule_from_instance('assign', $assign->id);
403
        if ($unsetfield) {
404
            unset($cm->$unsetfield);
405
        }
406
        // If view is required, but they haven't viewed it yet.
407
        $current = (object)['viewed' => COMPLETION_NOT_VIEWED];
408
 
409
        $completioninfo = new completion_info($this->course);
1441 ariadna 410
        $this->assertCompletionEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, $current));
1 efrain 411
    }
412
 
413
    /**
414
     * Provider for the test_internal_get_state_with_grade_criteria.
415
     *
416
     * @return array
417
     */
1441 ariadna 418
    public static function internal_get_state_with_grade_criteria_provider(): array {
1 efrain 419
        return [
420
            "Passing grade enabled and achieve. State should be COMPLETION_COMPLETE_PASS" => [
421
                [
422
                    'completionusegrade' => 1,
423
                    'completionpassgrade' => 1,
424
                    'gradepass' => 50,
425
                ],
426
                50,
427
                COMPLETION_COMPLETE_PASS
428
            ],
429
            "Passing grade enabled and not achieve. State should be COMPLETION_COMPLETE_FAIL" => [
430
                [
431
                    'completionusegrade' => 1,
432
                    'completionpassgrade' => 1,
433
                    'gradepass' => 50,
434
                ],
435
                40,
436
                COMPLETION_COMPLETE_FAIL
437
            ],
438
            "Passing grade not enabled with passing grade set." => [
439
                [
440
                    'completionusegrade' => 1,
441
                    'gradepass' => 50,
442
                ],
443
                50,
444
                COMPLETION_COMPLETE_PASS
445
            ],
446
            "Passing grade not enabled with passing grade not set." => [
447
                [
448
                    'completionusegrade' => 1,
449
                ],
450
                90,
451
                COMPLETION_COMPLETE
452
            ],
453
            "Passing grade not enabled with passing grade not set. No submission made." => [
454
                [
455
                    'completionusegrade' => 1,
456
                ],
457
                null,
458
                COMPLETION_INCOMPLETE
459
            ],
460
        ];
461
    }
462
 
463
    /**
464
     * Tests that the right completion state is being set based on the grade criteria.
465
     *
466
     * @dataProvider internal_get_state_with_grade_criteria_provider
467
     * @param array $completioncriteria The completion criteria to use
468
     * @param int|null $studentgrade Grade to assign to student
469
     * @param int $expectedstate Expected completion state
470
     * @covers ::internal_get_state
471
     */
11 efrain 472
    public function test_internal_get_state_with_grade_criteria(array $completioncriteria, ?int $studentgrade, int $expectedstate): void {
1 efrain 473
        $this->setup_data();
474
 
475
        /** @var \mod_assign_generator $assigngenerator */
476
        $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
477
        $assign = $assigngenerator->create_instance([
478
            'course' => $this->course->id,
479
            'completion' => COMPLETION_ENABLED,
480
        ] + $completioncriteria);
481
 
482
        $userid = $this->user->id;
483
 
484
        $cm = get_coursemodule_from_instance('assign', $assign->id);
485
        $usercm = cm_info::create($cm, $userid);
486
 
487
        // Create a teacher account.
488
        $teacher = $this->getDataGenerator()->create_user();
489
        $this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, 'editingteacher');
490
        // Log in as the teacher.
491
        $this->setUser($teacher);
492
 
493
        // Grade the student for this assignment.
494
        $assign = new assign($usercm->context, $cm, $cm->course);
495
        if ($studentgrade) {
496
            $data = (object)[
497
                'sendstudentnotifications' => false,
498
                'attemptnumber' => 1,
499
                'grade' => $studentgrade,
500
            ];
501
            $assign->save_grade($userid, $data);
502
        }
503
 
504
        // The target user already received a grade, so internal_get_state should be already complete.
505
        $completioninfo = new completion_info($this->course);
1441 ariadna 506
        $this->assertCompletionEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, null));
1 efrain 507
    }
508
 
509
    /**
510
     * Covers the case where internal_get_state() is being called for a user different from the logged in user.
511
     *
512
     * @covers ::internal_get_state
513
     */
11 efrain 514
    public function test_internal_get_state_with_different_user(): void {
1 efrain 515
        $this->setup_data();
516
 
517
        /** @var \mod_assign_generator $assigngenerator */
518
        $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
519
        $assign = $assigngenerator->create_instance([
520
            'course' => $this->course->id,
521
            'completion' => COMPLETION_ENABLED,
522
            'completionusegrade' => 1,
523
        ]);
524
 
525
        $userid = $this->user->id;
526
 
527
        $cm = get_coursemodule_from_instance('assign', $assign->id);
528
        $usercm = cm_info::create($cm, $userid);
529
 
530
        // Create a teacher account.
531
        $teacher = $this->getDataGenerator()->create_user();
532
        $this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, 'editingteacher');
533
        // Log in as the teacher.
534
        $this->setUser($teacher);
535
 
536
        // Grade the student for this assignment.
537
        $assign = new assign($usercm->context, $cm, $cm->course);
538
        $data = (object)[
539
            'sendstudentnotifications' => false,
540
            'attemptnumber' => 1,
541
            'grade' => 90,
542
        ];
543
        $assign->save_grade($userid, $data);
544
 
545
        // The target user already received a grade, so internal_get_state should be already complete.
546
        $completioninfo = new completion_info($this->course);
1441 ariadna 547
        $this->assertCompletionEquals(COMPLETION_COMPLETE, $completioninfo->internal_get_state($cm, $userid, null));
1 efrain 548
 
549
        // As the teacher which does not have a grade in this cm, internal_get_state should return incomplete.
1441 ariadna 550
        $this->assertCompletionEquals(COMPLETION_INCOMPLETE, $completioninfo->internal_get_state($cm, $teacher->id, null));
1 efrain 551
    }
552
 
553
    /**
554
     * Test for internal_get_state() for an activity that supports custom completion.
555
     *
556
     * @covers ::internal_get_state
557
     */
11 efrain 558
    public function test_internal_get_state_with_custom_completion(): void {
1 efrain 559
        $this->setup_data();
560
 
561
        $choicerecord = [
562
            'course' => $this->course,
563
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
564
            'completionsubmit' => COMPLETION_ENABLED,
565
        ];
566
        $choice = $this->getDataGenerator()->create_module('choice', $choicerecord);
567
        $cminfo = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
568
 
569
        $completioninfo = new completion_info($this->course);
570
 
571
        // Fetch completion for the user who hasn't made a choice yet.
572
        $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
1441 ariadna 573
        $this->assertCompletionEquals(COMPLETION_INCOMPLETE, $completion);
1 efrain 574
 
575
        // Have the user make a choice.
576
        $choicewithoptions = choice_get_choice($choice->id);
577
        $optionids = array_keys($choicewithoptions->option);
578
        choice_user_submit_response($optionids[0], $choice, $this->user->id, $this->course, $cminfo);
579
        $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
1441 ariadna 580
        $this->assertCompletionEquals(COMPLETION_COMPLETE, $completion);
1 efrain 581
    }
582
 
583
    /**
584
     * @covers ::set_module_viewed
585
     */
11 efrain 586
    public function test_set_module_viewed(): void {
1 efrain 587
        $this->mock_setup();
588
 
589
        $mockbuilder = $this->getMockBuilder('completion_info');
590
        $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_set_data', 'update_state'));
591
        $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
592
        $cm = (object)array('id' => 13, 'course' => 42);
593
 
594
        // Not tracking completion, should do nothing.
595
        $c = $mockbuilder->getMock();
596
        $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
597
        $c->set_module_viewed($cm);
598
 
599
        // Tracking completion but completion is disabled, should do nothing.
600
        $c = $mockbuilder->getMock();
601
        $cm->completionview = COMPLETION_VIEW_REQUIRED;
602
        $c->expects($this->once())
603
            ->method('is_enabled')
604
            ->with($cm)
605
            ->will($this->returnValue(false));
606
        $c->set_module_viewed($cm);
607
 
608
        // Now it's enabled, we expect it to get data. If data already has
609
        // viewed, still do nothing.
610
        $c = $mockbuilder->getMock();
611
        $c->expects($this->once())
612
            ->method('is_enabled')
613
            ->with($cm)
614
            ->will($this->returnValue(true));
615
        $c->expects($this->once())
616
            ->method('get_data')
617
            ->with($cm, 0)
618
            ->will($this->returnValue((object)array('viewed' => COMPLETION_VIEWED)));
619
        $c->set_module_viewed($cm);
620
 
621
        // OK finally one that hasn't been viewed, now it should set it viewed
622
        // and update state.
623
        $c = $mockbuilder->getMock();
624
        $c->expects($this->once())
625
            ->method('is_enabled')
626
            ->with($cm)
627
            ->will($this->returnValue(true));
628
        $c->expects($this->once())
629
            ->method('get_data')
630
            ->with($cm, false, 1337)
631
            ->will($this->returnValue((object)array('viewed' => COMPLETION_NOT_VIEWED)));
632
        $c->expects($this->once())
633
            ->method('internal_set_data')
634
            ->with($cm, (object)array('viewed' => COMPLETION_VIEWED));
635
        $c->expects($this->once())
636
            ->method('update_state')
637
            ->with($cm, COMPLETION_COMPLETE, 1337);
638
        $c->set_module_viewed($cm, 1337);
639
    }
640
 
641
    /**
642
     * @covers ::count_user_data
643
     */
11 efrain 644
    public function test_count_user_data(): void {
1 efrain 645
        global $DB;
646
        $this->mock_setup();
647
 
648
        $course = (object)array('id' => 13);
649
        $cm = (object)array('id' => 42);
650
 
651
        /** @var $DB PHPUnit_Framework_MockObject_MockObject */
652
        $DB->expects($this->once())
653
            ->method('get_field_sql')
654
            ->will($this->returnValue(666));
655
 
656
        $c = new completion_info($course);
1441 ariadna 657
        $this->assertCompletionEquals(666, $c->count_user_data($cm));
1 efrain 658
    }
659
 
660
    /**
661
     * @covers ::delete_all_state
662
     */
11 efrain 663
    public function test_delete_all_state(): void {
1 efrain 664
        global $DB;
665
        $this->mock_setup();
666
 
667
        $course = (object)array('id' => 13);
668
        $cm = (object)array('id' => 42, 'course' => 13);
669
        $c = new completion_info($course);
670
 
671
        // Check it works ok without data in session.
672
        /** @var $DB PHPUnit_Framework_MockObject_MockObject */
673
        $DB->expects($this->once())
674
            ->method('delete_records')
675
            ->with('course_modules_completion', array('coursemoduleid' => 42))
676
            ->will($this->returnValue(true));
677
        $c->delete_all_state($cm);
678
    }
679
 
680
    /**
681
     * @covers ::reset_all_state
682
     */
11 efrain 683
    public function test_reset_all_state(): void {
1 efrain 684
        global $DB;
685
        $this->mock_setup();
686
 
687
        $mockbuilder = $this->getMockBuilder('completion_info');
688
        $mockbuilder->onlyMethods(array('delete_all_state', 'get_tracked_users', 'update_state'));
689
        $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
690
        $c = $mockbuilder->getMock();
691
 
692
        $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
693
 
694
        /** @var $DB PHPUnit_Framework_MockObject_MockObject */
695
        $DB->expects($this->once())
696
            ->method('get_recordset')
697
            ->will($this->returnValue(
698
                new core_completionlib_fake_recordset(array((object)array('id' => 1, 'userid' => 100),
699
                    (object)array('id' => 2, 'userid' => 101)))));
700
 
701
        $c->expects($this->once())
702
            ->method('delete_all_state')
703
            ->with($cm);
704
 
705
        $c->expects($this->once())
706
            ->method('get_tracked_users')
707
            ->will($this->returnValue(array(
708
            (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
709
            (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
710
 
1441 ariadna 711
        $updateinvocations = $this->exactly(3);
712
        $c->expects($updateinvocations)
1 efrain 713
            ->method('update_state')
1441 ariadna 714
            ->willReturnCallback(function ($comparecm, $state, $userid) use ($updateinvocations, $cm): void {
715
                $this->assertCompletionEquals($cm, $comparecm);
716
                $this->assertCompletionEquals(COMPLETION_UNKNOWN, $state);
717
                switch (self::getInvocationCount($updateinvocations)) {
718
                    case 1:
719
                        $this->assertCompletionEquals(100, $userid);
720
                        break;
721
                    case 2:
722
                        $this->assertCompletionEquals(101, $userid);
723
                        break;
724
                    case 3:
725
                        $this->assertCompletionEquals(201, $userid);
726
                        break;
727
                    default:
728
                        $this->fail('Unexpected invocation count');
729
                }
730
            });
1 efrain 731
 
732
        $c->reset_all_state($cm);
733
    }
734
 
735
    /**
736
     * Data provider for test_get_data().
737
     *
738
     * @return array[]
739
     */
1441 ariadna 740
    public static function get_data_provider(): array {
1 efrain 741
        return [
742
            'No completion record' => [
743
                false, true, false, COMPLETION_INCOMPLETE
744
            ],
745
            'Not completed' => [
746
                false, true, true, COMPLETION_INCOMPLETE
747
            ],
748
            'Completed' => [
749
                false, true, true, COMPLETION_COMPLETE
750
            ],
751
            'Whole course, complete' => [
752
                true, true, true, COMPLETION_COMPLETE
753
            ],
754
            'Get data for another user, result should be not cached' => [
755
                false, false, true,  COMPLETION_INCOMPLETE
756
            ],
757
            'Get data for another user, including whole course, result should be not cached' => [
758
                true, false, true,  COMPLETION_INCOMPLETE
759
            ],
760
        ];
761
    }
762
 
763
    /**
764
     * Tests for completion_info::get_data().
765
     *
766
     * @dataProvider get_data_provider
767
     * @param bool $wholecourse Whole course parameter for get_data().
768
     * @param bool $sameuser Whether the user calling get_data() is the user itself.
769
     * @param bool $hasrecord Whether to create a course_modules_completion record.
770
     * @param int $completion The completion state expected.
771
     * @covers ::get_data
772
     */
11 efrain 773
    public function test_get_data(bool $wholecourse, bool $sameuser, bool $hasrecord, int $completion): void {
1 efrain 774
        global $DB;
775
 
776
        $this->setup_data();
777
        $user = $this->user;
778
 
779
        $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
780
        $choice = $choicegenerator->create_instance([
781
            'course' => $this->course->id,
782
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
783
            'completionview' => true,
784
            'completionsubmit' => true,
785
        ]);
786
 
787
        $cm = get_coursemodule_from_instance('choice', $choice->id);
788
 
789
        // Let's manually create a course completion record instead of going through the hoops to complete an activity.
790
        if ($hasrecord) {
791
            $cmcompletionrecord = (object)[
792
                'coursemoduleid' => $cm->id,
793
                'userid' => $user->id,
794
                'completionstate' => $completion,
795
                'overrideby' => null,
796
                'timemodified' => 0,
797
            ];
798
            $cmcompletionviewrecord = (object)[
799
                'coursemoduleid' => $cm->id,
800
                'userid' => $user->id,
801
                'timecreated' => 0,
802
            ];
803
            $DB->insert_record('course_modules_completion', $cmcompletionrecord);
804
            $DB->insert_record('course_modules_viewed', $cmcompletionviewrecord);
805
        }
806
 
807
        // Whether we expect for the returned completion data to be stored in the cache.
808
        $iscached = true;
809
 
810
        if (!$sameuser) {
811
            $iscached = false;
812
            $this->setAdminUser();
813
        } else {
814
            $this->setUser($user);
815
        }
816
 
817
        // Mock other completion data.
818
        $completioninfo = new completion_info($this->course);
819
 
820
        $result = $completioninfo->get_data($cm, $wholecourse, $user->id);
821
 
822
        // Course module ID of the returned completion data must match this activity's course module ID.
1441 ariadna 823
        $this->assertCompletionEquals($cm->id, $result->coursemoduleid);
1 efrain 824
        // User ID of the returned completion data must match the user's ID.
1441 ariadna 825
        $this->assertCompletionEquals($user->id, $result->userid);
1 efrain 826
        // The completion state of the returned completion data must match the expected completion state.
1441 ariadna 827
        $this->assertCompletionEquals($completion, $result->completionstate);
1 efrain 828
 
829
        // If the user has no completion record, then the default record should be returned.
830
        if (!$hasrecord) {
1441 ariadna 831
            $this->assertCompletionEquals(0, $result->id);
1 efrain 832
        }
833
 
834
        // Check that we are including relevant completion data for the module.
835
        if (!$wholecourse) {
836
            $this->assertTrue(property_exists($result, 'viewed'));
837
            $this->assertTrue(property_exists($result, 'customcompletion'));
838
        }
839
    }
840
 
841
    /**
842
     * @covers ::get_data
843
     */
844
    public function test_get_data_successive_calls(): void {
845
        global $DB;
846
 
847
        $this->setup_data();
848
        $this->setUser($this->user);
849
 
850
        $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
851
        $choice = $choicegenerator->create_instance([
852
            'course' => $this->course->id,
853
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
854
            'completionview' => true,
855
            'completionsubmit' => true,
856
        ]);
857
 
858
        $cm = get_coursemodule_from_instance('choice', $choice->id);
859
 
860
        // Let's manually create a course completion record instead of going through the hoops to complete an activity.
861
        $cmcompletionrecord = (object) [
862
            'coursemoduleid' => $cm->id,
863
            'userid' => $this->user->id,
864
            'completionstate' => COMPLETION_NOT_VIEWED,
865
            'overrideby' => null,
866
            'timemodified' => 0,
867
        ];
868
        $cmcompletionviewrecord = (object)[
869
            'coursemoduleid' => $cm->id,
870
            'userid' => $this->user->id,
871
            'timecreated' => 0,
872
        ];
873
        $DB->insert_record('course_modules_completion', $cmcompletionrecord);
874
        $DB->insert_record('course_modules_viewed', $cmcompletionviewrecord);
875
 
876
        // Mock other completion data.
877
        $completioninfo = new completion_info($this->course);
878
 
879
        $modinfo = get_fast_modinfo($this->course);
880
        $results = [];
881
        foreach ($modinfo->cms as $testcm) {
882
            $result = $completioninfo->get_data($testcm, true);
883
            $this->assertTrue(property_exists($result, 'id'));
884
            $this->assertTrue(property_exists($result, 'coursemoduleid'));
885
            $this->assertTrue(property_exists($result, 'userid'));
886
            $this->assertTrue(property_exists($result, 'completionstate'));
887
            $this->assertTrue(property_exists($result, 'viewed'));
888
            $this->assertTrue(property_exists($result, 'overrideby'));
889
            $this->assertTrue(property_exists($result, 'timemodified'));
890
            $this->assertFalse(property_exists($result, 'other_cm_completion_data_fetched'));
891
 
1441 ariadna 892
            $this->assertCompletionEquals($testcm->id, $result->coursemoduleid);
893
            $this->assertCompletionEquals($this->user->id, $result->userid);
1 efrain 894
 
895
            $results[$testcm->id] = $result;
896
        }
897
 
898
        $result = $completioninfo->get_data($cm);
899
        $this->assertTrue(property_exists($result, 'customcompletion'));
900
 
901
        // The data should match when fetching modules individually.
902
        (cache::make('core', 'completion'))->purge();
903
        foreach ($modinfo->cms as $testcm) {
904
            $result = $completioninfo->get_data($testcm, false);
1441 ariadna 905
            $this->assertCompletionEquals($result, $results[$testcm->id]);
1 efrain 906
        }
907
    }
908
 
909
    /**
910
     * Tests for get_completion_data().
911
     *
912
     * @covers ::get_completion_data
913
     */
11 efrain 914
    public function test_get_completion_data(): void {
915
        $this->setAdminUser();
1 efrain 916
        $this->setup_data();
917
        $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
918
        $choice = $choicegenerator->create_instance([
919
            'course' => $this->course->id,
920
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
921
            'completionview' => true,
922
            'completionsubmit' => true,
923
        ]);
924
        $cm = get_coursemodule_from_instance('choice', $choice->id);
925
 
926
        // Mock other completion data.
927
        $completioninfo = new completion_info($this->course);
928
        // Default data to return when no completion data is found.
929
        $defaultdata = [
930
            'id' => 0,
931
            'coursemoduleid' => $cm->id,
932
            'userid' => $this->user->id,
933
            'completionstate' => 0,
934
            'viewed' => 0,
935
            'overrideby' => null,
936
            'timemodified' => 0,
937
        ];
938
 
939
        $completiondatabeforeview = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata);
940
        $this->assertTrue(array_key_exists('viewed', $completiondatabeforeview));
941
        $this->assertTrue(array_key_exists('coursemoduleid', $completiondatabeforeview));
1441 ariadna 942
        $this->assertCompletionEquals(0, $completiondatabeforeview['viewed']);
943
        $this->assertCompletionEquals($cm->id, $completiondatabeforeview['coursemoduleid']);
1 efrain 944
 
11 efrain 945
        // Mark as completed before viewing it.
946
        $completioninfo->update_state($cm, COMPLETION_COMPLETE, $this->user->id, true);
947
        $completiondatabeforeview = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata);
1441 ariadna 948
        $this->assertCompletionEquals(0, $completiondatabeforeview['viewed']);
11 efrain 949
 
1 efrain 950
        // Set viewed.
951
        $completioninfo->set_module_viewed($cm, $this->user->id);
952
 
953
        $completiondata = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata);
954
        $this->assertTrue(array_key_exists('viewed', $completiondata));
955
        $this->assertTrue(array_key_exists('coursemoduleid', $completiondata));
1441 ariadna 956
        $this->assertCompletionEquals(1, $completiondata['viewed']);
957
        $this->assertCompletionEquals($cm->id, $completiondatabeforeview['coursemoduleid']);
1 efrain 958
 
959
        $completioninfo->reset_all_state($cm);
960
 
961
        $completiondataafterreset = $completioninfo->get_completion_data($cm->id, $this->user->id, $defaultdata);
962
        $this->assertTrue(array_key_exists('viewed', $completiondataafterreset));
963
        $this->assertTrue(array_key_exists('coursemoduleid', $completiondataafterreset));
1441 ariadna 964
        $this->assertCompletionEquals(1, $completiondataafterreset['viewed']);
965
        $this->assertCompletionEquals($cm->id, $completiondatabeforeview['coursemoduleid']);
1 efrain 966
    }
967
 
968
    /**
969
     * Tests for completion_info::get_other_cm_completion_data().
970
     *
971
     * @covers ::get_other_cm_completion_data
972
     */
11 efrain 973
    public function test_get_other_cm_completion_data(): void {
1 efrain 974
        global $DB;
975
 
976
        $this->setup_data();
977
        $user = $this->user;
978
 
979
        $this->setAdminUser();
980
 
981
        $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
982
        $choice = $choicegenerator->create_instance([
983
            'course' => $this->course->id,
984
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
985
            'completionsubmit' => true,
986
        ]);
987
 
988
        $cmchoice = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
989
 
990
        $choice2 = $choicegenerator->create_instance([
991
            'course' => $this->course->id,
992
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
993
        ]);
994
 
995
        $cmchoice2 = cm_info::create(get_coursemodule_from_instance('choice', $choice2->id));
996
 
997
        $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
998
        $workshop = $workshopgenerator->create_instance([
999
            'course' => $this->course->id,
1000
            'completion' => COMPLETION_TRACKING_AUTOMATIC,
1001
            // Submission grade required.
1002
            'completiongradeitemnumber' => 0,
1003
            'completionpassgrade' => 1,
1004
        ]);
1005
 
1006
        $cmworkshop = cm_info::create(get_coursemodule_from_instance('workshop', $workshop->id));
1007
 
1008
        $completioninfo = new completion_info($this->course);
1009
 
1010
        $method = new ReflectionMethod("completion_info", "get_other_cm_completion_data");
1011
 
1012
        // Check that fetching data for a module with custom completion provides its info.
1013
        $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id);
1014
 
1015
        $this->assertArrayHasKey('customcompletion', $choicecompletiondata);
1016
        $this->assertArrayHasKey('completionsubmit', $choicecompletiondata['customcompletion']);
1441 ariadna 1017
        $this->assertCompletionEquals(COMPLETION_INCOMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']);
1 efrain 1018
 
1019
        // Mock a choice answer so user has completed the requirement.
1020
        $choicemockinfo = [
1021
            'choiceid' => $cmchoice->instance,
1022
            'userid' => $this->user->id
1023
        ];
1024
        $DB->insert_record('choice_answers', $choicemockinfo, false);
1025
 
1026
        // Confirm fetching again reflects the completion.
1027
        $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id);
1441 ariadna 1028
        $this->assertCompletionEquals(COMPLETION_COMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']);
1 efrain 1029
 
1030
        // Check that fetching data for a module with no custom completion still provides its grade completion status.
1031
        $workshopcompletiondata = $method->invoke($completioninfo, $cmworkshop, $user->id);
1032
 
1033
        $this->assertArrayHasKey('completiongrade', $workshopcompletiondata);
1034
        $this->assertArrayHasKey('passgrade', $workshopcompletiondata);
1035
        $this->assertArrayNotHasKey('customcompletion', $workshopcompletiondata);
1441 ariadna 1036
        $this->assertCompletionEquals(COMPLETION_INCOMPLETE, $workshopcompletiondata['completiongrade']);
1037
        $this->assertCompletionEquals(COMPLETION_INCOMPLETE, $workshopcompletiondata['passgrade']);
1 efrain 1038
 
1039
        // Check that fetching data for a module with no completion conditions does not provide any data.
1040
        $choice2completiondata = $method->invoke($completioninfo, $cmchoice2, $user->id);
1041
        $this->assertEmpty($choice2completiondata);
1042
    }
1043
 
1044
    /**
1045
     * @covers ::internal_set_data
1046
     */
11 efrain 1047
    public function test_internal_set_data(): void {
1 efrain 1048
        global $DB;
1049
        $this->setup_data();
1050
 
1051
        $this->setUser($this->user);
1052
        $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1053
        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1054
        $cm = get_coursemodule_from_instance('forum', $forum->id);
1055
        $c = new completion_info($this->course);
1056
 
1057
        // 1) Test with new data.
1058
        $data = new stdClass();
1059
        $data->id = 0;
1060
        $data->userid = $this->user->id;
1061
        $data->coursemoduleid = $cm->id;
1062
        $data->completionstate = COMPLETION_COMPLETE;
1063
        $data->timemodified = time();
1064
        $data->viewed = COMPLETION_NOT_VIEWED;
1065
        $data->overrideby = null;
1066
 
1067
        $c->internal_set_data($cm, $data);
1068
        $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id));
1441 ariadna 1069
        $this->assertCompletionEquals($d1, $data->id);
1 efrain 1070
        $cache = cache::make('core', 'completion');
1071
        // Cache was not set for another user.
1072
        $cachevalue = $cache->get("{$data->userid}_{$cm->course}");
1441 ariadna 1073
        $this->assertCompletionEquals([
1 efrain 1074
            'cacherev' => $this->course->cacherev,
1075
            $cm->id => array_merge(
1076
                (array) $data,
1077
                ['other_cm_completion_data_fetched' => true]
1078
            ),
1079
        ],
1080
        $cachevalue);
1081
 
1082
        // 2) Test with existing data and for different user.
1083
        $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1084
        $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
1085
        $newuser = $this->getDataGenerator()->create_user();
1086
 
1087
        $d2 = new stdClass();
1088
        $d2->id = 7;
1089
        $d2->userid = $newuser->id;
1090
        $d2->coursemoduleid = $cm2->id;
1091
        $d2->completionstate = COMPLETION_COMPLETE;
1092
        $d2->timemodified = time();
1093
        $d2->viewed = COMPLETION_NOT_VIEWED;
1094
        $d2->overrideby = null;
1095
        $c->internal_set_data($cm2, $d2);
1096
        // Cache for current user returns the data.
1097
        $cachevalue = $cache->get($data->userid . '_' . $cm->course);
1441 ariadna 1098
        $this->assertCompletionEquals(array_merge(
1 efrain 1099
            (array) $data,
1100
            ['other_cm_completion_data_fetched' => true]
1101
        ), $cachevalue[$cm->id]);
1102
 
1103
        // Cache for another user is not filled.
1441 ariadna 1104
        $this->assertCompletionEquals(false, $cache->get($d2->userid . '_' . $cm2->course));
1 efrain 1105
 
1106
        // 3) Test where it THINKS the data is new (from cache) but actually in the database it has been set since.
1107
        $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1108
        $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
1109
        $newuser2 = $this->getDataGenerator()->create_user();
1110
        $d3 = new stdClass();
1111
        $d3->id = 13;
1112
        $d3->userid = $newuser2->id;
1113
        $d3->coursemoduleid = $cm3->id;
1114
        $d3->completionstate = COMPLETION_COMPLETE;
1115
        $d3->timemodified = time();
1116
        $d3->viewed = COMPLETION_NOT_VIEWED;
1117
        $d3->overrideby = null;
1118
        $DB->insert_record('course_modules_completion', $d3);
1119
        $c->internal_set_data($cm, $data);
1120
 
1121
        // 4) Test instant course completions.
1122
        $dataactivity = $this->getDataGenerator()->create_module('data', array('course' => $this->course->id),
1123
            array('completion' => 1));
1124
        $cm = get_coursemodule_from_instance('data', $dataactivity->id);
1125
        $c = new completion_info($this->course);
1126
        $cmdata = get_coursemodule_from_id('data', $dataactivity->cmid);
1127
 
1128
        // Add activity completion criteria.
1129
        $criteriadata = new stdClass();
1130
        $criteriadata->id = $this->course->id;
1131
        $criteriadata->criteria_activity = array();
1132
        // Some activities.
1133
        $criteriadata->criteria_activity[$cmdata->id] = 1;
1134
        $class = 'completion_criteria_activity';
1135
        $criterion = new $class();
1136
        $criterion->update_config($criteriadata);
1137
 
1138
        $actual = $DB->get_records('course_completions');
1139
        $this->assertEmpty($actual);
1140
 
1141
        $data->coursemoduleid = $cm->id;
1142
        $c->internal_set_data($cm, $data);
1143
        $actual = $DB->get_records('course_completions');
1441 ariadna 1144
        $this->assertCompletionEquals(1, count($actual));
1145
        $this->assertCompletionEquals($this->user->id, reset($actual)->userid);
1 efrain 1146
 
1147
        $data->userid = $newuser2->id;
1148
        $c->internal_set_data($cm, $data, true);
1149
        $actual = $DB->get_records('course_completions');
1441 ariadna 1150
        $this->assertCompletionEquals(1, count($actual));
1151
        $this->assertCompletionEquals($this->user->id, reset($actual)->userid);
1 efrain 1152
    }
1153
 
1154
    /**
1155
     * @covers ::get_progress_all
1156
     */
11 efrain 1157
    public function test_get_progress_all_few(): void {
1 efrain 1158
        global $DB;
1159
        $this->mock_setup();
1160
 
1161
        $mockbuilder = $this->getMockBuilder('completion_info');
1162
        $mockbuilder->onlyMethods(array('get_tracked_users'));
1163
        $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1164
        $c = $mockbuilder->getMock();
1165
 
1166
        // With few results.
1167
        $c->expects($this->once())
1168
            ->method('get_tracked_users')
1169
            ->with(false,  array(),  0,  '',  '',  '',  null)
1170
            ->will($this->returnValue(array(
1171
                (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
1172
                (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
1173
        $DB->expects($this->once())
1174
            ->method('get_in_or_equal')
1175
            ->with(array(100, 201))
1176
            ->will($this->returnValue(array(' IN (100, 201)', array())));
1177
        $progress1 = (object)array('userid' => 100, 'coursemoduleid' => 13);
1178
        $progress2 = (object)array('userid' => 201, 'coursemoduleid' => 14);
1179
        $DB->expects($this->once())
1180
            ->method('get_recordset_sql')
1181
            ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
1182
 
1441 ariadna 1183
        $this->assertCompletionEquals(array(
1 efrain 1184
                100 => (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh',
1185
                    'progress' => array(13 => $progress1)),
1186
                201 => (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy',
1187
                    'progress' => array(14 => $progress2)),
1188
            ), $c->get_progress_all(false));
1189
    }
1190
 
1191
    /**
1192
     * @covers ::get_progress_all
1193
     */
11 efrain 1194
    public function test_get_progress_all_lots(): void {
1 efrain 1195
        global $DB;
1196
        $this->mock_setup();
1197
 
1198
        $mockbuilder = $this->getMockBuilder('completion_info');
1199
        $mockbuilder->onlyMethods(array('get_tracked_users'));
1200
        $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1201
        $c = $mockbuilder->getMock();
1202
 
1203
        $tracked = array();
1204
        $ids = array();
1205
        $progress = array();
1206
        // With more than 1000 results.
1207
        for ($i = 100; $i < 2000; $i++) {
1208
            $tracked[] = (object)array('id' => $i, 'firstname' => 'frog', 'lastname' => $i);
1209
            $ids[] = $i;
1210
            $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 13);
1211
            $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 14);
1212
        }
1213
        $c->expects($this->once())
1214
            ->method('get_tracked_users')
1215
            ->with(true,  3,  0,  '',  '',  '',  null)
1216
            ->will($this->returnValue($tracked));
1441 ariadna 1217
        $inorequalsinvocations = $this->exactly(2);
1218
        $DB->expects($inorequalsinvocations)
1 efrain 1219
            ->method('get_in_or_equal')
1441 ariadna 1220
            ->willReturnCallback(function ($paramids) use ($inorequalsinvocations, $ids) {
1221
                switch (self::getInvocationCount($inorequalsinvocations)) {
1222
                    case 1:
1223
                        $this->assertCompletionEquals(array_slice($ids, 0, 1000), $paramids);
1224
                        return [' IN whatever', []];
1225
                    case 2:
1226
                        $this->assertCompletionEquals(array_slice($ids, 1000), $paramids);
1227
                        return [' IN whatever2', []];
1228
                    default:
1229
                        $this->fail('Unexpected invocation count');
1230
                }
1231
            });
1232
        $getinvocations = $this->exactly(2);
1233
        $DB->expects($getinvocations)
1 efrain 1234
            ->method('get_recordset_sql')
1441 ariadna 1235
            ->willReturnCallback(function () use ($getinvocations, $progress) {
1236
                return match (self::getInvocationCount($getinvocations)) {
1237
                    1 => new core_completionlib_fake_recordset(array_slice($progress, 0, 1000)),
1238
                    2 => new core_completionlib_fake_recordset(array_slice($progress, 1000)),
1239
                    default => $this->fail('Unexpected invocation count'),
1240
                };
1241
            });
1 efrain 1242
 
1243
        $result = $c->get_progress_all(true, 3);
1244
        $resultok = true;
1245
        $resultok = $resultok && ($ids == array_keys($result));
1246
 
1247
        foreach ($result as $userid => $data) {
1248
            $resultok = $resultok && $data->firstname == 'frog';
1249
            $resultok = $resultok && $data->lastname == $userid;
1250
            $resultok = $resultok && $data->id == $userid;
1251
            $cms = $data->progress;
1252
            $resultok = $resultok && (array(13, 14) == array_keys($cms));
1253
            $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 13) == $cms[13]);
1254
            $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 14) == $cms[14]);
1255
        }
1256
        $this->assertTrue($resultok);
1257
        $this->assertCount(count($tracked), $result);
1258
    }
1259
 
1260
    /**
1261
     * @covers ::inform_grade_changed
1262
     */
11 efrain 1263
    public function test_inform_grade_changed(): void {
1 efrain 1264
        $this->mock_setup();
1265
 
1266
        $mockbuilder = $this->getMockBuilder('completion_info');
1267
        $mockbuilder->onlyMethods(array('is_enabled', 'update_state'));
1268
        $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1269
 
1270
        $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => null);
1271
        $item = (object)array('itemnumber' => 3,  'gradepass' => 1,  'hidden' => 0);
1272
        $grade = (object)array('userid' => 31337,  'finalgrade' => 0,  'rawgrade' => 0);
1273
 
1274
        // Not enabled (should do nothing).
1275
        $c = $mockbuilder->getMock();
1276
        $c->expects($this->once())
1277
            ->method('is_enabled')
1278
            ->with($cm)
1279
            ->will($this->returnValue(false));
1280
        $c->inform_grade_changed($cm, $item, $grade, false);
1281
 
1282
        // Enabled but still no grade completion required,  should still do nothing.
1283
        $c = $mockbuilder->getMock();
1284
        $c->expects($this->once())
1285
            ->method('is_enabled')
1286
            ->with($cm)
1287
            ->will($this->returnValue(true));
1288
        $c->inform_grade_changed($cm, $item, $grade, false);
1289
 
1290
        // Enabled and completion required but item number is wrong,  does nothing.
1291
        $c = $mockbuilder->getMock();
1292
        $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 7);
1293
        $c->expects($this->once())
1294
            ->method('is_enabled')
1295
            ->with($cm)
1296
            ->will($this->returnValue(true));
1297
        $c->inform_grade_changed($cm, $item, $grade, false);
1298
 
1299
        // Enabled and completion required and item number right. It is supposed
1300
        // to call update_state with the new potential state being obtained from
1301
        // internal_get_grade_state.
1302
        $c = $mockbuilder->getMock();
1303
        $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1304
        $grade = (object)array('userid' => 31337,  'finalgrade' => 1,  'rawgrade' => 0);
1305
        $c->expects($this->once())
1306
            ->method('is_enabled')
1307
            ->with($cm)
1308
            ->will($this->returnValue(true));
1309
        $c->expects($this->once())
1310
            ->method('update_state')
1311
            ->with($cm, COMPLETION_COMPLETE_PASS, 31337)
1312
            ->will($this->returnValue(true));
1313
        $c->inform_grade_changed($cm, $item, $grade, false);
1314
 
1315
        // Same as above but marked deleted. It is supposed to call update_state
1316
        // with new potential state being COMPLETION_INCOMPLETE.
1317
        $c = $mockbuilder->getMock();
1318
        $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1319
        $grade = (object)array('userid' => 31337,  'finalgrade' => 1,  'rawgrade' => 0);
1320
        $c->expects($this->once())
1321
            ->method('is_enabled')
1322
            ->with($cm)
1323
            ->will($this->returnValue(true));
1324
        $c->expects($this->once())
1325
            ->method('update_state')
1326
            ->with($cm, COMPLETION_INCOMPLETE, 31337)
1327
            ->will($this->returnValue(true));
1328
        $c->inform_grade_changed($cm, $item, $grade, true);
1329
    }
1330
 
1331
    /**
1332
     * @covers ::internal_get_grade_state
1333
     */
11 efrain 1334
    public function test_internal_get_grade_state(): void {
1 efrain 1335
        $this->mock_setup();
1336
 
1337
        $item = new stdClass;
1338
        $grade = new stdClass;
1339
 
1340
        $item->gradepass = 4;
1341
        $item->hidden = 0;
1342
        $grade->rawgrade = 4.0;
1343
        $grade->finalgrade = null;
1344
 
1345
        // Grade has pass mark and is not hidden,  user passes.
1441 ariadna 1346
        $this->assertCompletionEquals(
1 efrain 1347
            COMPLETION_COMPLETE_PASS,
1348
            completion_info::internal_get_grade_state($item, $grade));
1349
 
1350
        // Same but user fails.
1351
        $grade->rawgrade = 3.9;
1441 ariadna 1352
        $this->assertCompletionEquals(
1 efrain 1353
            COMPLETION_COMPLETE_FAIL,
1354
            completion_info::internal_get_grade_state($item, $grade));
1355
 
1356
        // User fails on raw grade but passes on final.
1357
        $grade->finalgrade = 4.0;
1441 ariadna 1358
        $this->assertCompletionEquals(
1 efrain 1359
            COMPLETION_COMPLETE_PASS,
1360
            completion_info::internal_get_grade_state($item, $grade));
1361
 
1362
        // Item is hidden.
1363
        $item->hidden = 1;
1441 ariadna 1364
        $this->assertCompletionEquals(
1 efrain 1365
            COMPLETION_COMPLETE,
1366
            completion_info::internal_get_grade_state($item, $grade));
1367
 
1368
        // Item isn't hidden but has no pass mark.
1369
        $item->hidden = 0;
1370
        $item->gradepass = 0;
1441 ariadna 1371
        $this->assertCompletionEquals(
1 efrain 1372
            COMPLETION_COMPLETE,
1373
            completion_info::internal_get_grade_state($item, $grade));
1374
 
1375
        // Item is hidden, but returnpassfail is true and the grade is passing.
1376
        $item->hidden = 1;
1377
        $item->gradepass = 4;
1378
        $grade->finalgrade = 5.0;
1441 ariadna 1379
        $this->assertCompletionEquals(
1 efrain 1380
            COMPLETION_COMPLETE_PASS,
1381
            completion_info::internal_get_grade_state($item, $grade, true));
1382
 
1383
        // Item is hidden, but returnpassfail is true and the grade is failing.
1384
        $item->hidden = 1;
1385
        $item->gradepass = 4;
1386
        $grade->finalgrade = 3.0;
1441 ariadna 1387
        $this->assertCompletionEquals(
1 efrain 1388
            COMPLETION_COMPLETE_FAIL_HIDDEN,
1389
            completion_info::internal_get_grade_state($item, $grade, true));
1390
 
1391
        // Item is not hidden, but returnpassfail is true and the grade is failing.
1392
        $item->hidden = 0;
1393
        $item->gradepass = 4;
1394
        $grade->finalgrade = 3.0;
1441 ariadna 1395
        $this->assertCompletionEquals(
1 efrain 1396
            COMPLETION_COMPLETE_FAIL,
1397
            completion_info::internal_get_grade_state($item, $grade, true));
1398
    }
1399
 
1400
    /**
1401
     * @test ::get_activities
1402
     */
11 efrain 1403
    public function test_get_activities(): void {
1 efrain 1404
        global $CFG;
1405
        $this->resetAfterTest();
1406
 
1407
        // Enable completion before creating modules, otherwise the completion data is not written in DB.
1408
        $CFG->enablecompletion = true;
1409
 
1410
        // Create a course with mixed auto completion data.
1411
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1412
        $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1413
        $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL);
1414
        $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
1415
        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
1416
        $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionauto);
1417
        $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionmanual);
1418
 
1419
        $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionnone);
1420
        $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionnone);
1421
        $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionnone);
1422
 
1423
        // Create data in another course to make sure it's not considered.
1424
        $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1425
        $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionauto);
1426
        $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id), $completionmanual);
1427
        $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id), $completionnone);
1428
 
1429
        $c = new completion_info($course);
1430
        $activities = $c->get_activities();
1431
        $this->assertCount(3, $activities);
1432
        $this->assertTrue(isset($activities[$forum->cmid]));
1433
        $this->assertSame($forum->name, $activities[$forum->cmid]->name);
1434
        $this->assertTrue(isset($activities[$page->cmid]));
1435
        $this->assertSame($page->name, $activities[$page->cmid]->name);
1436
        $this->assertTrue(isset($activities[$data->cmid]));
1437
        $this->assertSame($data->name, $activities[$data->cmid]->name);
1438
 
1439
        $this->assertFalse(isset($activities[$forum2->cmid]));
1440
        $this->assertFalse(isset($activities[$page2->cmid]));
1441
        $this->assertFalse(isset($activities[$data2->cmid]));
1442
    }
1443
 
1444
    /**
1445
     * @test ::has_activities
1446
     */
11 efrain 1447
    public function test_has_activities(): void {
1 efrain 1448
        global $CFG;
1449
        $this->resetAfterTest();
1450
 
1451
        // Enable completion before creating modules, otherwise the completion data is not written in DB.
1452
        $CFG->enablecompletion = true;
1453
 
1454
        // Create a course with mixed auto completion data.
1455
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1456
        $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1457
        $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1458
        $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
1459
        $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
1460
        $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionnone);
1461
 
1462
        $c1 = new completion_info($course);
1463
        $c2 = new completion_info($course2);
1464
 
1465
        $this->assertTrue($c1->has_activities());
1466
        $this->assertFalse($c2->has_activities());
1467
    }
1468
 
1469
    /**
1470
     * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses
1471
     *
1472
     * @covers ::delete_course_completion_data
1473
     * @covers ::delete_all_completion_data
1474
     */
11 efrain 1475
    public function test_course_delete_prerequisite(): void {
1 efrain 1476
        global $DB;
1477
 
1478
        $this->setup_data();
1479
 
1480
        $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
1481
 
1482
        $criteriadata = (object) [
1483
            'id' => $this->course->id,
1484
            'criteria_course' => [$courseprerequisite->id],
1485
        ];
1486
 
1487
        /** @var completion_criteria_course $criteria */
1488
        $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]);
1489
        $criteria->update_config($criteriadata);
1490
 
1491
        // Sanity test.
1492
        $this->assertTrue($DB->record_exists('course_completion_criteria', [
1493
            'course' => $this->course->id,
1494
            'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1495
            'courseinstance' => $courseprerequisite->id,
1496
        ]));
1497
 
1498
        // Deleting the prerequisite course should remove the completion criteria.
1499
        delete_course($courseprerequisite, false);
1500
 
1501
        $this->assertFalse($DB->record_exists('course_completion_criteria', [
1502
            'course' => $this->course->id,
1503
            'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1504
            'courseinstance' => $courseprerequisite->id,
1505
        ]));
1506
    }
1507
 
1508
    /**
1509
     * Test course module completion update event.
1510
     *
1511
     * @covers \core\event\course_module_completion_updated
1512
     */
11 efrain 1513
    public function test_course_module_completion_updated_event(): void {
1 efrain 1514
        global $USER, $CFG;
1515
 
1516
        $this->setup_data();
1517
 
1518
        $this->setAdminUser();
1519
 
1520
        $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1521
        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1522
 
1523
        $c = new completion_info($this->course);
1524
        $activities = $c->get_activities();
1441 ariadna 1525
        $this->assertCompletionEquals(1, count($activities));
1 efrain 1526
        $this->assertTrue(isset($activities[$forum->cmid]));
1441 ariadna 1527
        $this->assertCompletionEquals($activities[$forum->cmid]->name, $forum->name);
1 efrain 1528
 
1529
        $current = $c->get_data($activities[$forum->cmid], false, $this->user->id);
1530
        $current->completionstate = COMPLETION_COMPLETE;
1531
        $current->timemodified = time();
1532
        $sink = $this->redirectEvents();
1533
        $c->internal_set_data($activities[$forum->cmid], $current);
1534
        $events = $sink->get_events();
1535
        $event = reset($events);
1536
        $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
1441 ariadna 1537
        $this->assertCompletionEquals($forum->cmid,
1 efrain 1538
            $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
1441 ariadna 1539
        $this->assertCompletionEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
1540
        $this->assertCompletionEquals(context_module::instance($forum->cmid), $event->get_context());
1541
        $this->assertCompletionEquals($USER->id, $event->userid);
1542
        $this->assertCompletionEquals($this->user->id, $event->relateduserid);
1 efrain 1543
        $this->assertInstanceOf('moodle_url', $event->get_url());
1544
    }
1545
 
1546
    /**
1547
     * Test course completed event.
1548
     *
1549
     * @covers \core\event\course_completed
1550
     */
11 efrain 1551
    public function test_course_completed_event(): void {
1 efrain 1552
        global $USER;
1553
 
1554
        $this->setup_data();
1555
        $this->setAdminUser();
1556
 
1557
        $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1558
        $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1559
 
1560
        // Mark course as complete and get triggered event.
1561
        $sink = $this->redirectEvents();
1562
        $ccompletion->mark_complete();
1563
        $events = $sink->get_events();
1564
        $event = reset($events);
1565
 
1566
        $this->assertInstanceOf('\core\event\course_completed', $event);
1441 ariadna 1567
        $this->assertCompletionEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course);
1568
        $this->assertCompletionEquals($this->course->id, $event->courseid);
1569
        $this->assertCompletionEquals($USER->id, $event->userid);
1570
        $this->assertCompletionEquals($this->user->id, $event->relateduserid);
1571
        $this->assertCompletionEquals(context_course::instance($this->course->id), $event->get_context());
1 efrain 1572
        $this->assertInstanceOf('moodle_url', $event->get_url());
1573
    }
1574
 
1575
    /**
1576
     * Test course completed message.
1577
     *
1578
     * @covers \core\event\course_completed
1579
     */
11 efrain 1580
    public function test_course_completed_message(): void {
1 efrain 1581
        $this->setup_data();
1582
        $this->setAdminUser();
1583
 
1584
        $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1585
        $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1586
 
1587
        // Mark course as complete and get the message.
1588
        $sink = $this->redirectMessages();
1589
        $ccompletion->mark_complete();
1590
        $messages = $sink->get_messages();
1591
        $sink->close();
1592
 
1593
        $this->assertCount(1, $messages);
1594
        $message = array_pop($messages);
1595
 
1441 ariadna 1596
        $this->assertCompletionEquals(core_user::get_noreply_user()->id, $message->useridfrom);
1597
        $this->assertCompletionEquals($this->user->id, $message->useridto);
1598
        $this->assertCompletionEquals('coursecompleted', $message->eventtype);
1599
        $this->assertCompletionEquals(get_string('coursecompleted', 'completion'), $message->subject);
1 efrain 1600
        $this->assertStringContainsString($this->course->fullname, $message->fullmessage);
1601
    }
1602
 
1603
    /**
1604
     * Test course completed event.
1605
     *
1606
     * @covers \core\event\course_completion_updated
1607
     */
11 efrain 1608
    public function test_course_completion_updated_event(): void {
1 efrain 1609
        $this->setup_data();
1610
        $coursecontext = context_course::instance($this->course->id);
1611
        $coursecompletionevent = \core\event\course_completion_updated::create(
1612
                array(
1613
                    'courseid' => $this->course->id,
1614
                    'context' => $coursecontext
1615
                    )
1616
                );
1617
 
1618
        // Mark course as complete and get triggered event.
1619
        $sink = $this->redirectEvents();
1620
        $coursecompletionevent->trigger();
1621
        $events = $sink->get_events();
1622
        $event = array_pop($events);
1623
        $sink->close();
1624
 
1625
        $this->assertInstanceOf('\core\event\course_completion_updated', $event);
1441 ariadna 1626
        $this->assertCompletionEquals($this->course->id, $event->courseid);
1627
        $this->assertCompletionEquals($coursecontext, $event->get_context());
1 efrain 1628
        $this->assertInstanceOf('moodle_url', $event->get_url());
1629
    }
1630
 
1631
    /**
1632
     * @covers \completion_can_view_data
1633
     */
11 efrain 1634
    public function test_completion_can_view_data(): void {
1 efrain 1635
        $this->setup_data();
1636
 
1637
        $student = $this->getDataGenerator()->create_user();
1638
        $this->getDataGenerator()->enrol_user($student->id, $this->course->id);
1639
 
1640
        $this->setUser($student);
1641
        $this->assertTrue(completion_can_view_data($student->id, $this->course->id));
1642
        $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id));
1643
    }
1644
 
1645
    /**
1646
     * Data provider for test_get_grade_completion().
1647
     *
1648
     * @return array[]
1649
     */
1441 ariadna 1650
    public static function get_grade_completion_provider(): array {
1 efrain 1651
        return [
1652
            'Grade not required' => [false, false, null, null, null],
1653
            'Grade required, but has no grade yet' => [true, false, null, null, COMPLETION_INCOMPLETE],
1654
            'Grade required, grade received' => [true, true, null, null, COMPLETION_COMPLETE],
1655
            'Grade required, passing grade received' => [true, true, 70, null, COMPLETION_COMPLETE_PASS],
1656
            'Grade required, failing grade received' => [true, true, 80, null, COMPLETION_COMPLETE_FAIL],
1657
        ];
1658
    }
1659
 
1660
    /**
1661
     * Test for \completion_info::get_grade_completion().
1662
     *
1663
     * @dataProvider get_grade_completion_provider
1664
     * @param bool $completionusegrade Whether the test activity has grade completion requirement.
1665
     * @param bool $hasgrade Whether to set grade for the user in this activity.
1666
     * @param int|null $passinggrade Passing grade to set for the test activity.
1667
     * @param string|null $expectedexception Expected exception.
1668
     * @param int|null $expectedresult The expected completion status.
1669
     * @covers ::get_grade_completion
1670
     */
1671
    public function test_get_grade_completion(bool $completionusegrade, bool $hasgrade, ?int $passinggrade,
11 efrain 1672
        ?string $expectedexception, ?int $expectedresult): void {
1 efrain 1673
        $this->setup_data();
1674
 
1675
        /** @var \mod_assign_generator $assigngenerator */
1676
        $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
1677
        $assign = $assigngenerator->create_instance([
1678
            'course' => $this->course->id,
1679
            'completion' => COMPLETION_ENABLED,
1680
            'completionusegrade' => $completionusegrade,
1681
            'gradepass' => $passinggrade,
1682
        ]);
1683
 
1684
        $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1685
        if ($completionusegrade && $hasgrade) {
1686
            $assigninstance = new assign($cm->context, $cm, $this->course);
1687
            $grade = $assigninstance->get_user_grade($this->user->id, true);
1688
            $grade->grade = 75;
1689
            $assigninstance->update_grade($grade);
1690
        }
1691
 
1692
        $completioninfo = new completion_info($this->course);
1693
        if ($expectedexception) {
1694
            $this->expectException($expectedexception);
1695
        }
1696
        $gradecompletion = $completioninfo->get_grade_completion($cm, $this->user->id);
1441 ariadna 1697
        $this->assertCompletionEquals($expectedresult, $gradecompletion);
1 efrain 1698
    }
1699
 
1700
    /**
1701
     * Test the return value for cases when the activity module does not have associated grade_item.
1702
     *
1703
     * @covers ::get_grade_completion
1704
     */
11 efrain 1705
    public function test_get_grade_completion_without_grade_item(): void {
1 efrain 1706
        global $DB;
1707
 
1708
        $this->setup_data();
1709
 
1710
        $assign = $this->getDataGenerator()->get_plugin_generator('mod_assign')->create_instance([
1711
            'course' => $this->course->id,
1712
            'completion' => COMPLETION_ENABLED,
1713
            'completionusegrade' => true,
1714
            'gradepass' => 42,
1715
        ]);
1716
 
1717
        $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1718
 
1719
        $DB->delete_records('grade_items', [
1720
            'courseid' => $this->course->id,
1721
            'itemtype' => 'mod',
1722
            'itemmodule' => 'assign',
1723
            'iteminstance' => $assign->id,
1724
        ]);
1725
 
1726
        // Without the grade_item, the activity is considered incomplete.
1727
        $completioninfo = new completion_info($this->course);
1441 ariadna 1728
        $this->assertCompletionEquals(COMPLETION_INCOMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id));
1 efrain 1729
 
1730
        // Once the activity is graded, the grade_item is automatically created.
1731
        $assigninstance = new assign($cm->context, $cm, $this->course);
1732
        $grade = $assigninstance->get_user_grade($this->user->id, true);
1733
        $grade->grade = 40;
1734
        $assigninstance->update_grade($grade);
1735
 
1736
        // The implicitly created grade_item does not have grade to pass defined so it is not distinguished.
1441 ariadna 1737
        $this->assertCompletionEquals(COMPLETION_COMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id));
1 efrain 1738
    }
1739
 
1740
    /**
1741
     * Test for aggregate_completions().
1742
     *
1743
     * @covers \aggregate_completions
1744
     */
11 efrain 1745
    public function test_aggregate_completions(): void {
1 efrain 1746
        global $DB, $CFG;
1747
        require_once($CFG->dirroot.'/completion/criteria/completion_criteria_activity.php');
1748
        $this->resetAfterTest(true);
1749
        $time = time();
1750
 
1751
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1752
 
1753
        for ($i = 0; $i < 4; $i++) {
1754
            $students[] = $this->getDataGenerator()->create_user();
1755
        }
1756
 
1757
        $teacher = $this->getDataGenerator()->create_user();
1758
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1759
        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1760
 
1761
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1762
        foreach ($students as $student) {
1763
            $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1764
        }
1765
 
1766
        $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id),
1767
            array('completion' => 1));
1768
        $cmdata = get_coursemodule_from_id('data', $data->cmid);
1769
 
1770
        // Add activity completion criteria.
1771
        $criteriadata = new stdClass();
1772
        $criteriadata->id = $course->id;
1773
        $criteriadata->criteria_activity = array();
1774
        // Some activities.
1775
        $criteriadata->criteria_activity[$cmdata->id] = 1;
1776
        $class = 'completion_criteria_activity';
1777
        $criterion = new $class();
1778
        $criterion->update_config($criteriadata);
1779
 
1780
        $this->setUser($teacher);
1781
 
1782
        // Mark activity complete for both students.
1783
        $cm = get_coursemodule_from_instance('data', $data->id);
1784
        $completioncriteria = $DB->get_record('course_completion_criteria', []);
1785
        foreach ($students as $student) {
1786
            $cmcompletionrecords[] = (object)[
1787
                'coursemoduleid' => $cm->id,
1788
                'userid' => $student->id,
1789
                'completionstate' => 1,
1790
                'viewed' => 0,
1791
                'overrideby' => null,
1792
                'timemodified' => 0,
1793
            ];
1794
 
1795
            $usercompletions[] = (object)[
1796
                'criteriaid' => $completioncriteria->id,
1797
                'userid' => $student->id,
1798
                'timecompleted' => $time,
1799
            ];
1800
 
1801
            $cc = array(
1802
                'course'    => $course->id,
1803
                'userid'    => $student->id
1804
            );
1805
            $ccompletion = new completion_completion($cc);
1806
            $completion[] = $ccompletion->mark_inprogress($time);
1807
        }
1808
        $DB->insert_records('course_modules_completion', $cmcompletionrecords);
1809
        $DB->insert_records('course_completion_crit_compl', $usercompletions);
1810
 
1811
        // MDL-33320: for instant completions we need aggregate to work in a single run.
1812
        $DB->set_field('course_completions', 'reaggregate', $time - 2);
1813
 
1814
        foreach ($students as $student) {
1815
            $result = $DB->get_record('course_completions', ['userid' => $student->id, 'reaggregate' => 0]);
1816
            $this->assertFalse($result);
1817
        }
1818
 
1819
        aggregate_completions($completion[0]);
1820
 
1821
        $result1 = $DB->get_record('course_completions', ['userid' => $students[0]->id, 'reaggregate' => 0]);
1822
        $result2 = $DB->get_record('course_completions', ['userid' => $students[1]->id, 'reaggregate' => 0]);
1823
        $result3 = $DB->get_record('course_completions', ['userid' => $students[2]->id, 'reaggregate' => 0]);
1824
 
1825
        $this->assertIsObject($result1);
1826
        $this->assertFalse($result2);
1827
        $this->assertFalse($result3);
1828
 
1829
        aggregate_completions(0);
1830
 
1831
        foreach ($students as $student) {
1832
            $result = $DB->get_record('course_completions', ['userid' => $student->id, 'reaggregate' => 0]);
1833
            $this->assertIsObject($result);
1834
        }
1835
    }
1836
 
1837
    /**
1838
     * Test for completion_completion::_save().
1839
     *
1840
     * @covers \completion_completion::_save
1841
     */
11 efrain 1842
    public function test_save(): void {
1 efrain 1843
        global $DB;
1844
        $this->resetAfterTest(true);
1845
 
1846
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1847
 
1848
        $student = $this->getDataGenerator()->create_user();
1849
        $teacher = $this->getDataGenerator()->create_user();
1850
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1851
        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1852
 
1853
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1854
        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1855
 
1856
        $this->setUser($teacher);
1857
 
1858
        $cc = array(
1859
            'course'    => $course->id,
1860
            'userid'    => $student->id
1861
        );
1862
        $ccompletion = new completion_completion($cc);
1863
 
1864
        $completions = $DB->get_records('course_completions');
1865
        $this->assertEmpty($completions);
1866
 
1867
        // We're testing a private method, so we need to setup reflector magic.
1868
        $method = new ReflectionMethod($ccompletion, '_save');
1869
        $completionid = $method->invoke($ccompletion);
1870
        $completions = $DB->get_records('course_completions');
1441 ariadna 1871
        $this->assertCompletionEquals(count($completions), 1);
1872
        $this->assertCompletionEquals(reset($completions)->id, $completionid);
1 efrain 1873
 
1874
        $ccompletion->id = 0;
1875
        $method = new ReflectionMethod($ccompletion, '_save');
1876
        $completionid = $method->invoke($ccompletion);
1877
        $this->assertDebuggingCalled('Can not update data object, no id!');
1878
        $this->assertNull($completionid);
1879
    }
1880
 
1881
    /**
1882
     * Test for completion_completion::mark_enrolled().
1883
     *
1884
     * @covers \completion_completion::mark_enrolled
1885
     */
11 efrain 1886
    public function test_mark_enrolled(): void {
1 efrain 1887
        global $DB;
1888
        $this->resetAfterTest(true);
1889
 
1890
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1891
 
1892
        $student = $this->getDataGenerator()->create_user();
1893
        $teacher = $this->getDataGenerator()->create_user();
1894
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1895
        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1896
 
1897
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1898
        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1899
 
1900
        $this->setUser($teacher);
1901
 
1902
        $cc = array(
1903
            'course'    => $course->id,
1904
            'userid'    => $student->id
1905
        );
1906
        $ccompletion = new completion_completion($cc);
1907
 
1908
        $completions = $DB->get_records('course_completions');
1909
        $this->assertEmpty($completions);
1910
 
1911
        $completionid = $ccompletion->mark_enrolled();
1912
        $completions = $DB->get_records('course_completions');
1441 ariadna 1913
        $this->assertCompletionEquals(count($completions), 1);
1914
        $this->assertCompletionEquals(reset($completions)->id, $completionid);
1 efrain 1915
 
1916
        $ccompletion->id = 0;
1917
        $completionid = $ccompletion->mark_enrolled();
1918
        $this->assertDebuggingCalled('Can not update data object, no id!');
1919
        $this->assertNull($completionid);
1920
        $completions = $DB->get_records('course_completions');
1441 ariadna 1921
        $this->assertCompletionEquals(1, count($completions));
1 efrain 1922
    }
1923
 
1924
    /**
1925
     * Test for completion_completion::mark_inprogress().
1926
     *
1927
     * @covers \completion_completion::mark_inprogress
1928
     */
11 efrain 1929
    public function test_mark_inprogress(): void {
1 efrain 1930
        global $DB;
1931
        $this->resetAfterTest(true);
1932
 
1933
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1934
 
1935
        $student = $this->getDataGenerator()->create_user();
1936
        $teacher = $this->getDataGenerator()->create_user();
1937
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1938
        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1939
 
1940
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1941
        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1942
 
1943
        $this->setUser($teacher);
1944
 
1945
        $cc = array(
1946
            'course'    => $course->id,
1947
            'userid'    => $student->id
1948
        );
1949
        $ccompletion = new completion_completion($cc);
1950
 
1951
        $completions = $DB->get_records('course_completions');
1952
        $this->assertEmpty($completions);
1953
 
1954
        $completionid = $ccompletion->mark_inprogress();
1955
        $completions = $DB->get_records('course_completions');
1441 ariadna 1956
        $this->assertCompletionEquals(1, count($completions));
1957
        $this->assertCompletionEquals(reset($completions)->id, $completionid);
1 efrain 1958
 
1959
        $ccompletion->id = 0;
1960
        $completionid = $ccompletion->mark_inprogress();
1961
        $this->assertDebuggingCalled('Can not update data object, no id!');
1962
        $this->assertNull($completionid);
1963
        $completions = $DB->get_records('course_completions');
1441 ariadna 1964
        $this->assertCompletionEquals(1, count($completions));
1 efrain 1965
    }
1966
 
1967
    /**
1968
     * Test for completion_completion::mark_complete().
1969
     *
1970
     * @covers \completion_completion::mark_complete
1971
     */
11 efrain 1972
    public function test_mark_complete(): void {
1 efrain 1973
        global $DB;
1974
        $this->resetAfterTest(true);
1975
 
1976
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1977
 
1978
        $student = $this->getDataGenerator()->create_user();
1979
        $teacher = $this->getDataGenerator()->create_user();
1980
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1981
        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1982
 
1983
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1984
        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1985
 
1986
        $this->setUser($teacher);
1987
 
1988
        $cc = array(
1989
            'course'    => $course->id,
1990
            'userid'    => $student->id
1991
        );
1992
        $ccompletion = new completion_completion($cc);
1993
 
1994
        $completions = $DB->get_records('course_completions');
1995
        $this->assertEmpty($completions);
1996
 
1997
        $completionid = $ccompletion->mark_complete();
1998
        $completions = $DB->get_records('course_completions');
1441 ariadna 1999
        $this->assertCompletionEquals(1, count($completions));
2000
        $this->assertCompletionEquals(reset($completions)->id, $completionid);
1 efrain 2001
 
2002
        $ccompletion->id = 0;
2003
        $completionid = $ccompletion->mark_complete();
2004
        $this->assertNull($completionid);
2005
        $completions = $DB->get_records('course_completions');
1441 ariadna 2006
        $this->assertCompletionEquals(1, count($completions));
1 efrain 2007
    }
2008
 
2009
    /**
2010
     * Test for completion_criteria_completion::mark_complete().
2011
     *
2012
     * @covers \completion_criteria_completion::mark_complete
2013
     */
11 efrain 2014
    public function test_criteria_mark_complete(): void {
1 efrain 2015
        global $DB;
2016
        $this->resetAfterTest(true);
2017
 
2018
        $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
2019
 
2020
        $student = $this->getDataGenerator()->create_user();
2021
        $teacher = $this->getDataGenerator()->create_user();
2022
        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
2023
        $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
2024
 
2025
        $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
2026
        $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
2027
 
2028
        $this->setUser($teacher);
2029
 
2030
        $record = [
2031
            'course'    => $course->id,
2032
            'criteriaid'    => 1,
2033
            'userid'    => $student->id,
2034
            'timecompleted' => time()
2035
        ];
2036
        $completion = new completion_criteria_completion($record, DATA_OBJECT_FETCH_BY_KEY);
2037
 
2038
        $completions = $DB->get_records('course_completions');
2039
        $this->assertEmpty($completions);
2040
 
2041
        $completionid = $completion->mark_complete($record['timecompleted']);
2042
        $completions = $DB->get_records('course_completions');
1441 ariadna 2043
        $this->assertCompletionEquals(1, count($completions));
2044
        $this->assertCompletionEquals(reset($completions)->id, $completionid);
1 efrain 2045
    }
2046
 
2047
    /**
2048
     * Test that data is cleaned when we reset a course completion data
2049
     *
2050
     * @covers ::delete_all_completion_data
2051
     */
11 efrain 2052
    public function test_course_reset_completion(): void {
1 efrain 2053
        global $DB;
2054
 
2055
        $this->setup_data();
2056
 
2057
        $page = $this->getDataGenerator()->create_module('page', [
2058
            'course' => $this->course->id,
2059
            'completion' => COMPLETION_ENABLED,
2060
            'completionview' => COMPLETION_VIEW_REQUIRED,
2061
        ]);
2062
        $cm = cm_info::create(get_coursemodule_from_instance('page', $page->id));
2063
        $completion = new completion_info($this->course);
2064
        $completion->set_module_viewed($cm, $this->user->id);
2065
        // Sanity test.
2066
        $this->assertTrue($DB->record_exists_select('course_modules_completion',
2067
            'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)',
2068
            ['course' => $this->course->id]
2069
        ));
2070
        $this->assertTrue($DB->record_exists_select('course_modules_viewed',
2071
            'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)',
2072
            ['course' => $this->course->id]
2073
        ));
2074
        // Deleting the prerequisite course should remove the completion criteria.
2075
        $resetdata = new \stdClass();
2076
        $resetdata->id = $this->course->id;
2077
        $resetdata->reset_completion = true;
2078
        reset_course_userdata($resetdata);
2079
 
2080
        $this->assertFalse($DB->record_exists_select('course_modules_completion',
2081
            'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)',
2082
            ['course' => $this->course->id]
2083
        ));
2084
        $this->assertFalse($DB->record_exists_select('course_modules_viewed',
2085
            'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=:course)',
2086
            ['course' => $this->course->id]
2087
        ));
2088
    }
1441 ariadna 2089
 
2090
    /**
2091
     * Data provider for test_count_modules_completed().
2092
     *
2093
     * @return array[]
2094
     */
2095
    public static function count_modules_completed_provider(): array {
2096
        return [
2097
            'Multiple users with two different modules but only one completed' => [
2098
                'existinguser' => true,
2099
                'totalusers' => 3,
2100
                'modules' => [
2101
                    [
2102
                        'name' => 'assign',
2103
                        'completionstate' => COMPLETION_COMPLETE,
2104
                    ],
2105
                    [
2106
                        'name' => 'choice',
2107
                        'completionstate' => COMPLETION_INCOMPLETE,
2108
                    ],
2109
                ],
2110
                'expectedcount' => 1,
2111
            ],
2112
            'Multiple users with three different modules but only two completed' => [
2113
                'existinguser' => true,
2114
                'totalusers' => 4,
2115
                'modules' => [
2116
                    [
2117
                        'name' => 'assign',
2118
                        'completionstate' => COMPLETION_COMPLETE,
2119
                    ],
2120
                    [
2121
                        'name' => 'choice',
2122
                        'completionstate' => COMPLETION_INCOMPLETE,
2123
                    ],
2124
                    [
2125
                        'name' => 'workshop',
2126
                        'completionstate' => COMPLETION_COMPLETE,
2127
                    ],
2128
                ],
2129
                'expectedcount' => 2,
2130
            ],
2131
            'Multiple users with one completion each' => [
2132
                'existinguser' => true,
2133
                'totalusers' => 5,
2134
                'modules' => [
2135
                    [
2136
                        'name' => 'assign',
2137
                        'completionstate' => COMPLETION_COMPLETE,
2138
                    ],
2139
                ],
2140
                'expectedcount' => 1,
2141
            ],
2142
            'One user with one completion' => [
2143
                'existinguser' => true,
2144
                'totalusers' => 1,
2145
                'modules' => [
2146
                    [
2147
                        'name' => 'assign',
2148
                        'completionstate' => COMPLETION_COMPLETE,
2149
                    ],
2150
                ],
2151
                'expectedcount' => 1,
2152
            ],
2153
            'Multiple users without completion' => [
2154
                'existinguser' => true,
2155
                'totalusers' => 3,
2156
                'modules' => [
2157
                    [
2158
                        'name' => 'assign',
2159
                        'completionstate' => COMPLETION_INCOMPLETE,
2160
                    ],
2161
                ],
2162
                'expectedcount' => 0,
2163
            ],
2164
            'Non-existing user' => [
2165
                'existinguser' => false,
2166
                'totalusers' => 1,
2167
                'modules' => [
2168
                    [
2169
                        'name' => 'assign',
2170
                        'completionstate' => COMPLETION_INCOMPLETE,
2171
                    ],
2172
                ],
2173
                'expectedcount' => 0,
2174
            ],
2175
        ];
2176
    }
2177
 
2178
    /**
2179
     * Test for count_modules_completed().
2180
     *
2181
     * @dataProvider count_modules_completed_provider
2182
     * @param bool $existinguser Whether the given user exists or not.
2183
     * @param int $totalusers The amount of users to check completion.
2184
     * @param array $modules The course modules with its completion state.
2185
     * @param int $expectedcount Expected total of modules completed.
2186
     * @covers ::count_modules_completed
2187
     */
2188
    public function test_count_modules_completed(bool $existinguser, int $totalusers, array $modules,
2189
        int $expectedcount): void {
2190
        global $DB;
2191
 
2192
        $this->setAdminUser();
2193
        $this->setup_data();
2194
 
2195
        // Loop through the provided modules array and set the id key based on the generated module.
2196
        $modules = array_map(function(array $module): array {
2197
            $generator = $this->getDataGenerator()->get_plugin_generator('mod_' . $module['name']);
2198
            $modinstance = $generator->create_instance([
2199
                'course' => $this->course->id,
2200
                'completion' => COMPLETION_TRACKING_AUTOMATIC,
2201
                'completionsubmit' => true,
2202
            ]);
2203
            $cminstance = get_coursemodule_from_instance($module['name'], $modinstance->id);
2204
 
2205
            $module['id'] = $cminstance->id;
2206
            return $module;
2207
        }, $modules);
2208
 
2209
        $completion = new completion_info($this->course);
2210
 
2211
        if ($existinguser) {
2212
            // Create users, assign them to a course and define the completion record.
2213
            for ($i = 0; $i < $totalusers; $i++) {
2214
                $user = $this->getDataGenerator()->create_user();
2215
                $this->getDataGenerator()->enrol_user($user->id, $this->course->id);
2216
                $users[] = $user;
2217
 
2218
                foreach ($modules as $module) {
2219
                    $cmcompletionrecords[] = (object)[
2220
                        'coursemoduleid' => $module['id'],
2221
                        'userid' => $user->id,
2222
                        'completionstate' => $module['completionstate'],
2223
                        'timemodified' => 0,
2224
                    ];
2225
                }
2226
            }
2227
 
2228
            $DB->insert_records('course_modules_completion', $cmcompletionrecords);
2229
 
2230
            foreach ($users as $user) {
2231
                $this->assertEquals($expectedcount, $completion->count_modules_completed($user->id));
2232
            }
2233
        } else {
2234
            $nonexistinguserid = 123;
2235
            $this->assertEquals($expectedcount, $completion->count_modules_completed($nonexistinguserid));
2236
        }
2237
    }
1 efrain 2238
}
2239
 
2240
class core_completionlib_fake_recordset implements Iterator {
2241
    protected $closed;
2242
    protected $values, $index;
2243
 
2244
    public function __construct($values) {
2245
        $this->values = $values;
2246
        $this->index = 0;
2247
    }
2248
 
2249
    #[\ReturnTypeWillChange]
2250
    public function current() {
2251
        return $this->values[$this->index];
2252
    }
2253
 
2254
    #[\ReturnTypeWillChange]
2255
    public function key() {
2256
        return $this->values[$this->index];
2257
    }
2258
 
2259
    public function next(): void {
2260
        $this->index++;
2261
    }
2262
 
2263
    public function rewind(): void {
2264
        $this->index = 0;
2265
    }
2266
 
2267
    public function valid(): bool {
2268
        return count($this->values) > $this->index;
2269
    }
2270
 
2271
    public function close() {
2272
        $this->closed = true;
2273
    }
2274
 
2275
    public function was_closed() {
2276
        return $this->closed;
2277
    }
2278
}