MDL-70815 completion: Test internal_get_state() with custom completion
[moodle.git] / lib / tests / completionlib_test.php
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/>.
17 /**
18  * Completion tests.
19  *
20  * @package    core_completion
21  * @category   phpunit
22  * @copyright  2008 Sam Marshall
23  * @copyright  2013 Frédéric Massart
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 defined('MOODLE_INTERNAL') || die();
29 global $CFG;
30 require_once($CFG->libdir.'/completionlib.php');
32 class core_completionlib_testcase extends advanced_testcase {
33     protected $course;
34     protected $user;
35     protected $module1;
36     protected $module2;
38     protected function mock_setup() {
39         global $DB, $CFG, $USER;
41         $this->resetAfterTest();
43         $DB = $this->createMock(get_class($DB));
44         $CFG->enablecompletion = COMPLETION_ENABLED;
45         $USER = (object)array('id' =>314159);
46     }
48     /**
49      * Create course with user and activities.
50      */
51     protected function setup_data() {
52         global $DB, $CFG;
54         $this->resetAfterTest();
56         // Enable completion before creating modules, otherwise the completion data is not written in DB.
57         $CFG->enablecompletion = true;
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);
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     }
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      */
79     public static function assertEquals($expected, $actual, string $message = '', float $delta = 0, int $maxDepth = 10,
80                                         bool $canonicalize = false, bool $ignoreCase = false): void {
81         // Nasty cheating hack: prevent random failures on timemodified field.
82         if (is_object($expected) and is_object($actual)) {
83             if (property_exists($expected, 'timemodified') and property_exists($actual, 'timemodified')) {
84                 if ($expected->timemodified + 1 == $actual->timemodified) {
85                     $expected = clone($expected);
86                     $expected->timemodified = $actual->timemodified;
87                 }
88             }
89         }
90         parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
91     }
93     public function test_is_enabled() {
94         global $CFG;
95         $this->mock_setup();
97         // Config alone.
98         $CFG->enablecompletion = COMPLETION_DISABLED;
99         $this->assertEquals(COMPLETION_DISABLED, completion_info::is_enabled_for_site());
100         $CFG->enablecompletion = COMPLETION_ENABLED;
101         $this->assertEquals(COMPLETION_ENABLED, completion_info::is_enabled_for_site());
103         // Course.
104         $course = (object)array('id' =>13);
105         $c = new completion_info($course);
106         $course->enablecompletion = COMPLETION_DISABLED;
107         $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
108         $course->enablecompletion = COMPLETION_ENABLED;
109         $this->assertEquals(COMPLETION_ENABLED, $c->is_enabled());
110         $CFG->enablecompletion = COMPLETION_DISABLED;
111         $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
113         // Course and CM.
114         $cm = new stdClass();
115         $cm->completion = COMPLETION_TRACKING_MANUAL;
116         $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
117         $CFG->enablecompletion = COMPLETION_ENABLED;
118         $course->enablecompletion = COMPLETION_DISABLED;
119         $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
120         $course->enablecompletion = COMPLETION_ENABLED;
121         $this->assertEquals(COMPLETION_TRACKING_MANUAL, $c->is_enabled($cm));
122         $cm->completion = COMPLETION_TRACKING_NONE;
123         $this->assertEquals(COMPLETION_TRACKING_NONE, $c->is_enabled($cm));
124         $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
125         $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $c->is_enabled($cm));
126     }
128     public function test_update_state() {
129         $this->mock_setup();
131         $mockbuilder = $this->getMockBuilder('completion_info');
132         $mockbuilder->setMethods(array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data',
133                                        'user_can_override_completion'));
134         $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
135         $c = $mockbuilder->getMock();
136         $cm = (object)array('id'=>13, 'course'=>42);
138         // Not enabled, should do nothing.
139         $c->expects($this->at(0))
140             ->method('is_enabled')
141             ->with($cm)
142             ->will($this->returnValue(false));
143         $c->update_state($cm);
145         // Enabled, but current state is same as possible result, do nothing.
146         $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
147         $c->expects($this->at(0))
148             ->method('is_enabled')
149             ->with($cm)
150             ->will($this->returnValue(true));
151         $c->expects($this->at(1))
152             ->method('get_data')
153             ->with($cm, false, 0)
154             ->will($this->returnValue($current));
155         $c->update_state($cm, COMPLETION_COMPLETE);
157         // Enabled, but current state is a specific one and new state is just
158         // complete, so do nothing.
159         $current->completionstate = COMPLETION_COMPLETE_PASS;
160         $c->expects($this->at(0))
161             ->method('is_enabled')
162             ->with($cm)
163             ->will($this->returnValue(true));
164         $c->expects($this->at(1))
165             ->method('get_data')
166             ->with($cm, false, 0)
167             ->will($this->returnValue($current));
168         $c->update_state($cm, COMPLETION_COMPLETE);
170         // Manual, change state (no change).
171         $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_MANUAL);
172         $current->completionstate=COMPLETION_COMPLETE;
173         $c->expects($this->at(0))
174             ->method('is_enabled')
175             ->with($cm)
176             ->will($this->returnValue(true));
177         $c->expects($this->at(1))
178             ->method('get_data')
179             ->with($cm, false, 0)
180             ->will($this->returnValue($current));
181         $c->update_state($cm, COMPLETION_COMPLETE);
183         // Manual, change state (change).
184         $c->expects($this->at(0))
185             ->method('is_enabled')
186             ->with($cm)
187             ->will($this->returnValue(true));
188         $c->expects($this->at(1))
189             ->method('get_data')
190             ->with($cm, false, 0)
191             ->will($this->returnValue($current));
192         $changed = clone($current);
193         $changed->timemodified = time();
194         $changed->completionstate = COMPLETION_INCOMPLETE;
195         $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
196         $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
197         $c->expects($this->at(2))
198             ->method('internal_set_data')
199             ->with($cm, $comparewith);
200         $c->update_state($cm, COMPLETION_INCOMPLETE);
202         // Auto, change state.
203         $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
204         $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => null);
205         $c->expects($this->at(0))
206             ->method('is_enabled')
207             ->with($cm)
208             ->will($this->returnValue(true));
209         $c->expects($this->at(1))
210             ->method('get_data')
211             ->with($cm, false, 0)
212             ->will($this->returnValue($current));
213         $c->expects($this->at(2))
214             ->method('internal_get_state')
215             ->will($this->returnValue(COMPLETION_COMPLETE_PASS));
216         $changed = clone($current);
217         $changed->timemodified = time();
218         $changed->completionstate = COMPLETION_COMPLETE_PASS;
219         $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
220         $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
221         $c->expects($this->at(3))
222             ->method('internal_set_data')
223             ->with($cm, $comparewith);
224         $c->update_state($cm, COMPLETION_COMPLETE_PASS);
226         // Manual tracking, change state by overriding it manually.
227         $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_MANUAL);
228         $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
229         $c->expects($this->at(0))
230             ->method('is_enabled')
231             ->with($cm)
232             ->will($this->returnValue(true));
233         $c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses.
234             ->method('user_can_override_completion')
235             ->will($this->returnValue(true));
236         $c->expects($this->at(2))
237             ->method('get_data')
238             ->with($cm, false, 100)
239             ->will($this->returnValue($current));
240         $changed = clone($current);
241         $changed->timemodified = time();
242         $changed->completionstate = COMPLETION_COMPLETE;
243         $changed->overrideby = 314159;
244         $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
245         $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
246         $c->expects($this->at(3))
247             ->method('internal_set_data')
248             ->with($cm, $comparewith);
249         $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
250         // And confirm that the status can be changed back to incomplete without an override.
251         $c->update_state($cm, COMPLETION_INCOMPLETE, 100);
252         $c->expects($this->at(0))
253             ->method('get_data')
254             ->with($cm, false, 100)
255             ->will($this->returnValue($current));
256         $c->get_data($cm, false, 100);
258         // Auto, change state via override, incomplete to complete.
259         $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
260         $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
261         $c->expects($this->at(0))
262             ->method('is_enabled')
263             ->with($cm)
264             ->will($this->returnValue(true));
265         $c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses.
266             ->method('user_can_override_completion')
267             ->will($this->returnValue(true));
268         $c->expects($this->at(2))
269             ->method('get_data')
270             ->with($cm, false, 100)
271             ->will($this->returnValue($current));
272         $changed = clone($current);
273         $changed->timemodified = time();
274         $changed->completionstate = COMPLETION_COMPLETE;
275         $changed->overrideby = 314159;
276         $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
277         $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
278         $c->expects($this->at(3))
279             ->method('internal_set_data')
280             ->with($cm, $comparewith);
281         $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
282         $c->expects($this->at(0))
283             ->method('get_data')
284             ->with($cm, false, 100)
285             ->will($this->returnValue($changed));
286         $c->get_data($cm, false, 100);
288         // Now confirm that the status cannot be changed back to incomplete without an override.
289         // I.e. test that automatic completion won't trigger a change back to COMPLETION_INCOMPLETE when overridden.
290         $c->update_state($cm, COMPLETION_INCOMPLETE, 100);
291         $c->expects($this->at(0))
292             ->method('get_data')
293             ->with($cm, false, 100)
294             ->will($this->returnValue($changed));
295         $c->get_data($cm, false, 100);
297         // Now confirm the status can be changed back from complete to incomplete using an override.
298         $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
299         $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => 2);
300         $c->expects($this->at(0))
301             ->method('is_enabled')
302             ->with($cm)
303             ->will($this->returnValue(true));
304         $c->expects($this->at(1)) // Pretend the user has the required capability for overriding completion statuses.
305         ->method('user_can_override_completion')
306             ->will($this->returnValue(true));
307         $c->expects($this->at(2))
308             ->method('get_data')
309             ->with($cm, false, 100)
310             ->will($this->returnValue($current));
311         $changed = clone($current);
312         $changed->timemodified = time();
313         $changed->completionstate = COMPLETION_INCOMPLETE;
314         $changed->overrideby = 314159;
315         $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
316         $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
317         $c->expects($this->at(3))
318             ->method('internal_set_data')
319             ->with($cm, $comparewith);
320         $c->update_state($cm, COMPLETION_INCOMPLETE, 100, true);
321         $c->expects($this->at(0))
322             ->method('get_data')
323             ->with($cm, false, 100)
324             ->will($this->returnValue($changed));
325         $c->get_data($cm, false, 100);
326     }
328     /**
329      * Data provider for test_internal_get_state().
330      *
331      * @return array[]
332      */
333     public function internal_get_state_provider() {
334         return [
335             'View required, but not viewed yet' => [
336                 COMPLETION_VIEW_REQUIRED, 1, '', COMPLETION_INCOMPLETE
337             ],
338             'View not required and not viewed yet' => [
339                 COMPLETION_VIEW_NOT_REQUIRED, 1, '', COMPLETION_INCOMPLETE
340             ],
341             'View not required, grade required but no grade yet, $cm->modname not set' => [
342                 COMPLETION_VIEW_NOT_REQUIRED, 1, 'modname', COMPLETION_INCOMPLETE
343             ],
344             'View not required, grade required but no grade yet, $cm->course not set' => [
345                 COMPLETION_VIEW_NOT_REQUIRED, 1, 'course', COMPLETION_INCOMPLETE
346             ],
347             'View not required, grade not required' => [
348                 COMPLETION_VIEW_NOT_REQUIRED, 0, '', COMPLETION_COMPLETE
349             ],
350         ];
351     }
353     /**
354      * Test for completion_info::get_state().
355      *
356      * @dataProvider internal_get_state_provider
357      * @param int $completionview
358      * @param int $completionusegrade
359      * @param string $unsetfield
360      * @param int $expectedstate
361      */
362     public function test_internal_get_state(int $completionview, int $completionusegrade, string $unsetfield, int $expectedstate) {
363         $this->setup_data();
365         /** @var \mod_assign_generator $assigngenerator */
366         $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
367         $assign = $assigngenerator->create_instance([
368             'course' => $this->course->id,
369             'completion' => COMPLETION_ENABLED,
370             'completionview' => $completionview,
371             'completionusegrade' => $completionusegrade,
372         ]);
374         $userid = $this->user->id;
375         $this->setUser($userid);
377         $cm = get_coursemodule_from_instance('assign', $assign->id);
378         if ($unsetfield) {
379             unset($cm->$unsetfield);
380         }
381         // If view is required, but they haven't viewed it yet.
382         $current = (object)['viewed' => COMPLETION_NOT_VIEWED];
384         $completioninfo = new completion_info($this->course);
385         $this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, $current));
386     }
388     /**
389      * Covers the case where internal_get_state() is being called for a user different from the logged in user.
390      */
391     public function test_internal_get_state_with_different_user() {
392         $this->setup_data();
394         /** @var \mod_assign_generator $assigngenerator */
395         $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
396         $assign = $assigngenerator->create_instance([
397             'course' => $this->course->id,
398             'completion' => COMPLETION_ENABLED,
399             'completionusegrade' => 1,
400         ]);
402         $userid = $this->user->id;
404         $cm = get_coursemodule_from_instance('assign', $assign->id);
405         $usercm = cm_info::create($cm, $userid);
407         // Create a teacher account.
408         $teacher = $this->getDataGenerator()->create_user();
409         $this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, 'editingteacher');
410         // Log in as the teacher.
411         $this->setUser($teacher);
413         // Grade the student for this assignment.
414         $assign = new assign($usercm->context, $cm, $cm->course);
415         $data = (object)[
416             'sendstudentnotifications' => false,
417             'attemptnumber' => 1,
418             'grade' => 90,
419         ];
420         $assign->save_grade($userid, $data);
422         // The target user already received a grade, so internal_get_state should be already complete.
423         $completioninfo = new completion_info($this->course);
424         $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->internal_get_state($cm, $userid, null));
426         // As the teacher which does not have a grade in this cm, internal_get_state should return incomplete.
427         $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->internal_get_state($cm, $teacher->id, null));
428     }
430     /**
431      * Test for internal_get_state() for an activity that supports custom completion.
432      */
433     public function test_internal_get_state_with_custom_completion() {
434         $this->setup_data();
436         $choicerecord = [
437             'course' => $this->course,
438             'completion' => COMPLETION_TRACKING_AUTOMATIC,
439             'completionsubmit' => COMPLETION_ENABLED,
440         ];
441         $choice = $this->getDataGenerator()->create_module('choice', $choicerecord);
442         $cminfo = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
444         $completioninfo = new completion_info($this->course);
446         // Fetch completion for the user who hasn't made a choice yet.
447         $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
448         $this->assertEquals(COMPLETION_INCOMPLETE, $completion);
450         // Have the user make a choice.
451         $choicewithoptions = choice_get_choice($choice->id);
452         $optionids = array_keys($choicewithoptions->option);
453         choice_user_submit_response($optionids[0], $choice, $this->user->id, $this->course, $cminfo);
454         $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
455         $this->assertEquals(COMPLETION_COMPLETE, $completion);
456     }
458     public function test_set_module_viewed() {
459         $this->mock_setup();
461         $mockbuilder = $this->getMockBuilder('completion_info');
462         $mockbuilder->setMethods(array('is_enabled', 'get_data', 'internal_set_data', 'update_state'));
463         $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
464         $c = $mockbuilder->getMock();
465         $cm = (object)array('id'=>13, 'course'=>42);
467         // Not tracking completion, should do nothing.
468         $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
469         $c->set_module_viewed($cm);
471         // Tracking completion but completion is disabled, should do nothing.
472         $cm->completionview = COMPLETION_VIEW_REQUIRED;
473         $c->expects($this->at(0))
474             ->method('is_enabled')
475             ->with($cm)
476             ->will($this->returnValue(false));
477         $c->set_module_viewed($cm);
479         // Now it's enabled, we expect it to get data. If data already has
480         // viewed, still do nothing.
481         $c->expects($this->at(0))
482             ->method('is_enabled')
483             ->with($cm)
484             ->will($this->returnValue(true));
485         $c->expects($this->at(1))
486             ->method('get_data')
487             ->with($cm, 0)
488             ->will($this->returnValue((object)array('viewed'=>COMPLETION_VIEWED)));
489         $c->set_module_viewed($cm);
491         // OK finally one that hasn't been viewed, now it should set it viewed
492         // and update state.
493         $c->expects($this->at(0))
494             ->method('is_enabled')
495             ->with($cm)
496             ->will($this->returnValue(true));
497         $c->expects($this->at(1))
498             ->method('get_data')
499             ->with($cm, false, 1337)
500             ->will($this->returnValue((object)array('viewed'=>COMPLETION_NOT_VIEWED)));
501         $c->expects($this->at(2))
502             ->method('internal_set_data')
503             ->with($cm, (object)array('viewed'=>COMPLETION_VIEWED));
504         $c->expects($this->at(3))
505             ->method('update_state')
506             ->with($cm, COMPLETION_COMPLETE, 1337);
507         $c->set_module_viewed($cm, 1337);
508     }
510     public function test_count_user_data() {
511         global $DB;
512         $this->mock_setup();
514         $course = (object)array('id'=>13);
515         $cm = (object)array('id'=>42);
517         /** @var $DB PHPUnit_Framework_MockObject_MockObject */
518         $DB->expects($this->at(0))
519             ->method('get_field_sql')
520             ->will($this->returnValue(666));
522         $c = new completion_info($course);
523         $this->assertEquals(666, $c->count_user_data($cm));
524     }
526     public function test_delete_all_state() {
527         global $DB;
528         $this->mock_setup();
530         $course = (object)array('id'=>13);
531         $cm = (object)array('id'=>42, 'course'=>13);
532         $c = new completion_info($course);
534         // Check it works ok without data in session.
535         /** @var $DB PHPUnit_Framework_MockObject_MockObject */
536         $DB->expects($this->at(0))
537             ->method('delete_records')
538             ->with('course_modules_completion', array('coursemoduleid'=>42))
539             ->will($this->returnValue(true));
540         $c->delete_all_state($cm);
541     }
543     public function test_reset_all_state() {
544         global $DB;
545         $this->mock_setup();
547         $mockbuilder = $this->getMockBuilder('completion_info');
548         $mockbuilder->setMethods(array('delete_all_state', 'get_tracked_users', 'update_state'));
549         $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
550         $c = $mockbuilder->getMock();
552         $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
554         /** @var $DB PHPUnit_Framework_MockObject_MockObject */
555         $DB->expects($this->at(0))
556             ->method('get_recordset')
557             ->will($this->returnValue(
558                 new core_completionlib_fake_recordset(array((object)array('id'=>1, 'userid'=>100), (object)array('id'=>2, 'userid'=>101)))));
560         $c->expects($this->at(0))
561             ->method('delete_all_state')
562             ->with($cm);
564         $c->expects($this->at(1))
565             ->method('get_tracked_users')
566             ->will($this->returnValue(array(
567             (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'),
568             (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy'))));
570         $c->expects($this->at(2))
571             ->method('update_state')
572             ->with($cm, COMPLETION_UNKNOWN, 100);
573         $c->expects($this->at(3))
574             ->method('update_state')
575             ->with($cm, COMPLETION_UNKNOWN, 101);
576         $c->expects($this->at(4))
577             ->method('update_state')
578             ->with($cm, COMPLETION_UNKNOWN, 201);
580         $c->reset_all_state($cm);
581     }
583     /**
584      * Data provider for test_get_data().
585      *
586      * @return array[]
587      */
588     public function get_data_provider() {
589         return [
590             'No completion record' => [
591                 false, false, false, COMPLETION_INCOMPLETE
592             ],
593             'Not completed' => [
594                 false, false, true, COMPLETION_INCOMPLETE
595             ],
596             'Completed' => [
597                 false, false, true, COMPLETION_COMPLETE
598             ],
599             'Whole course, complete' => [
600                 true, false, true, COMPLETION_COMPLETE
601             ],
602             'Get data for another user, result should be not cached' => [
603                 false, true, true,  COMPLETION_INCOMPLETE
604             ],
605         ];
606     }
608     /**
609      * Tests for completion_info::get_data().
610      *
611      * @dataProvider get_data_provider
612      * @param bool $wholecourse Whole course parameter for get_data().
613      * @param bool $sameuser Whether the user calling get_data() is the user itself.
614      * @param bool $hasrecord Whether to create a course_modules_completion record.
615      * @param int $completion The completion state expected.
616      */
617     public function test_get_data(bool $wholecourse, bool $sameuser, bool $hasrecord, int $completion) {
618         global $DB;
620         $this->setup_data();
621         $user = $this->user;
623         /** @var \mod_choice_generator $choicegenerator */
624         $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
625         $choice = $choicegenerator->create_instance([
626             'course' => $this->course->id,
627             'completion' => true,
628             'completionview' => true,
629         ]);
631         $cm = get_coursemodule_from_instance('choice', $choice->id);
633         // Let's manually create a course completion record instead of going thru the hoops to complete an activity.
634         if ($hasrecord) {
635             $cmcompletionrecord = (object)[
636                 'coursemoduleid' => $cm->id,
637                 'userid' => $user->id,
638                 'completionstate' => $completion,
639                 'viewed' => 0,
640                 'overrideby' => null,
641                 'timemodified' => 0,
642             ];
643             $DB->insert_record('course_modules_completion', $cmcompletionrecord);
644         }
646         // Whether we expect for the returned completion data to be stored in the cache.
647         $iscached = true;
649         if (!$sameuser) {
650             $iscached = false;
651             $this->setAdminUser();
652         } else {
653             $this->setUser($user);
654         }
656         // Mock other completion data.
657         $completioninfo = new completion_info($this->course);
659         $result = $completioninfo->get_data($cm, $wholecourse, $user->id);
660         // Course module ID of the returned completion data must match this activity's course module ID.
661         $this->assertEquals($cm->id, $result->coursemoduleid);
662         // User ID of the returned completion data must match the user's ID.
663         $this->assertEquals($user->id, $result->userid);
664         // The completion state of the returned completion data must match the expected completion state.
665         $this->assertEquals($completion, $result->completionstate);
667         // If the user has no completion record, then the default record should be returned.
668         if (!$hasrecord) {
669             $iscached = false;
670             $this->assertEquals(0, $result->id);
671         }
673         // Check caching.
674         $key = "{$user->id}_{$this->course->id}";
675         $cache = cache::make('core', 'completion');
676         if ($iscached) {
677             // If we expect this to be cached, then fetching the result must match the cached data.
678             $this->assertEquals($result, (object)$cache->get($key)[$cm->id]);
680             // Check cached data for other course modules in the course.
681             // The sample module created in setup_data() should suffice to confirm this.
682             if ($wholecourse) {
683                 $this->assertArrayHasKey($this->module1->id, $cache->get($key));
684             } else {
685                 $this->assertArrayNotHasKey($this->module1->id, $cache->get($key));
686             }
687         } else {
688             // Otherwise, this should not be cached.
689             $this->assertFalse($cache->get($key));
690         }
691     }
693     public function test_internal_set_data() {
694         global $DB;
695         $this->setup_data();
697         $this->setUser($this->user);
698         $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
699         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
700         $cm = get_coursemodule_from_instance('forum', $forum->id);
701         $c = new completion_info($this->course);
703         // 1) Test with new data.
704         $data = new stdClass();
705         $data->id = 0;
706         $data->userid = $this->user->id;
707         $data->coursemoduleid = $cm->id;
708         $data->completionstate = COMPLETION_COMPLETE;
709         $data->timemodified = time();
710         $data->viewed = COMPLETION_NOT_VIEWED;
711         $data->overrideby = null;
713         $c->internal_set_data($cm, $data);
714         $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id));
715         $this->assertEquals($d1, $data->id);
716         $cache = cache::make('core', 'completion');
717         // Cache was not set for another user.
718         $this->assertEquals(array('cacherev' => $this->course->cacherev, $cm->id => $data),
719             $cache->get($data->userid . '_' . $cm->course));
721         // 2) Test with existing data and for different user.
722         $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
723         $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
724         $newuser = $this->getDataGenerator()->create_user();
726         $d2 = new stdClass();
727         $d2->id = 7;
728         $d2->userid = $newuser->id;
729         $d2->coursemoduleid = $cm2->id;
730         $d2->completionstate = COMPLETION_COMPLETE;
731         $d2->timemodified = time();
732         $d2->viewed = COMPLETION_NOT_VIEWED;
733         $d2->overrideby = null;
734         $c->internal_set_data($cm2, $d2);
735         // Cache for current user returns the data.
736         $cachevalue = $cache->get($data->userid . '_' . $cm->course);
737         $this->assertEquals($data, $cachevalue[$cm->id]);
738         // Cache for another user is not filled.
739         $this->assertEquals(false, $cache->get($d2->userid . '_' . $cm2->course));
741         // 3) Test where it THINKS the data is new (from cache) but actually
742         //    in the database it has been set since.
743         // 1) Test with new data.
744         $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
745         $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
746         $newuser2 = $this->getDataGenerator()->create_user();
747         $d3 = new stdClass();
748         $d3->id = 13;
749         $d3->userid = $newuser2->id;
750         $d3->coursemoduleid = $cm3->id;
751         $d3->completionstate = COMPLETION_COMPLETE;
752         $d3->timemodified = time();
753         $d3->viewed = COMPLETION_NOT_VIEWED;
754         $d3->overrideby = null;
755         $DB->insert_record('course_modules_completion', $d3);
756         $c->internal_set_data($cm, $data);
757     }
759     public function test_get_progress_all() {
760         global $DB;
761         $this->mock_setup();
763         $mockbuilder = $this->getMockBuilder('completion_info');
764         $mockbuilder->setMethods(array('get_tracked_users'));
765         $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
766         $c = $mockbuilder->getMock();
768         // 1) Basic usage.
769         $c->expects($this->at(0))
770             ->method('get_tracked_users')
771             ->with(false,  array(),  0,  '',  '',  '',  null)
772             ->will($this->returnValue(array(
773                 (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'),
774                 (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy'))));
775         $DB->expects($this->at(0))
776             ->method('get_in_or_equal')
777             ->with(array(100, 201))
778             ->will($this->returnValue(array(' IN (100, 201)', array())));
779         $progress1 = (object)array('userid'=>100, 'coursemoduleid'=>13);
780         $progress2 = (object)array('userid'=>201, 'coursemoduleid'=>14);
781         $DB->expects($this->at(1))
782             ->method('get_recordset_sql')
783             ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
785         $this->assertEquals(array(
786                 100 => (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh',
787                     'progress'=>array(13=>$progress1)),
788                 201 => (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy',
789                     'progress'=>array(14=>$progress2)),
790             ), $c->get_progress_all(false));
792         // 2) With more than 1, 000 results.
793         $tracked = array();
794         $ids = array();
795         $progress = array();
796         for ($i = 100; $i<2000; $i++) {
797             $tracked[] = (object)array('id'=>$i, 'firstname'=>'frog', 'lastname'=>$i);
798             $ids[] = $i;
799             $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>13);
800             $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>14);
801         }
802         $c->expects($this->at(0))
803             ->method('get_tracked_users')
804             ->with(true,  3,  0,  '',  '',  '',  null)
805             ->will($this->returnValue($tracked));
806         $DB->expects($this->at(0))
807             ->method('get_in_or_equal')
808             ->with(array_slice($ids, 0, 1000))
809             ->will($this->returnValue(array(' IN whatever', array())));
810         $DB->expects($this->at(1))
811             ->method('get_recordset_sql')
812             ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 0, 1000))));
814         $DB->expects($this->at(2))
815             ->method('get_in_or_equal')
816             ->with(array_slice($ids, 1000))
817             ->will($this->returnValue(array(' IN whatever2', array())));
818         $DB->expects($this->at(3))
819             ->method('get_recordset_sql')
820             ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 1000))));
822         $result = $c->get_progress_all(true, 3);
823         $resultok = true;
824         $resultok  =  $resultok && ($ids == array_keys($result));
826         foreach ($result as $userid => $data) {
827             $resultok  =  $resultok && $data->firstname == 'frog';
828             $resultok  =  $resultok && $data->lastname == $userid;
829             $resultok  =  $resultok && $data->id == $userid;
830             $cms = $data->progress;
831             $resultok =  $resultok && (array(13, 14) == array_keys($cms));
832             $resultok =  $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>13) == $cms[13]);
833             $resultok =  $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>14) == $cms[14]);
834         }
835         $this->assertTrue($resultok);
836     }
838     public function test_inform_grade_changed() {
839         $this->mock_setup();
841         $mockbuilder = $this->getMockBuilder('completion_info');
842         $mockbuilder->setMethods(array('is_enabled', 'update_state'));
843         $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
844         $c = $mockbuilder->getMock();
846         $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>null);
847         $item = (object)array('itemnumber'=>3,  'gradepass'=>1,  'hidden'=>0);
848         $grade = (object)array('userid'=>31337,  'finalgrade'=>0,  'rawgrade'=>0);
850         // Not enabled (should do nothing).
851         $c->expects($this->at(0))
852             ->method('is_enabled')
853             ->with($cm)
854             ->will($this->returnValue(false));
855         $c->inform_grade_changed($cm, $item, $grade, false);
857         // Enabled but still no grade completion required,  should still do nothing.
858         $c->expects($this->at(0))
859             ->method('is_enabled')
860             ->with($cm)
861             ->will($this->returnValue(true));
862         $c->inform_grade_changed($cm, $item, $grade, false);
864         // Enabled and completion required but item number is wrong,  does nothing.
865         $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>7);
866         $c->expects($this->at(0))
867             ->method('is_enabled')
868             ->with($cm)
869             ->will($this->returnValue(true));
870         $c->inform_grade_changed($cm, $item, $grade, false);
872         // Enabled and completion required and item number right. It is supposed
873         // to call update_state with the new potential state being obtained from
874         // internal_get_grade_state.
875         $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3);
876         $grade = (object)array('userid'=>31337,  'finalgrade'=>1,  'rawgrade'=>0);
877         $c->expects($this->at(0))
878             ->method('is_enabled')
879             ->with($cm)
880             ->will($this->returnValue(true));
881         $c->expects($this->at(1))
882             ->method('update_state')
883             ->with($cm, COMPLETION_COMPLETE_PASS, 31337)
884             ->will($this->returnValue(true));
885         $c->inform_grade_changed($cm, $item, $grade, false);
887         // Same as above but marked deleted. It is supposed to call update_state
888         // with new potential state being COMPLETION_INCOMPLETE.
889         $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3);
890         $grade = (object)array('userid'=>31337,  'finalgrade'=>1,  'rawgrade'=>0);
891         $c->expects($this->at(0))
892             ->method('is_enabled')
893             ->with($cm)
894             ->will($this->returnValue(true));
895         $c->expects($this->at(1))
896             ->method('update_state')
897             ->with($cm, COMPLETION_INCOMPLETE, 31337)
898             ->will($this->returnValue(true));
899         $c->inform_grade_changed($cm, $item, $grade, true);
900     }
902     public function test_internal_get_grade_state() {
903         $this->mock_setup();
905         $item = new stdClass;
906         $grade = new stdClass;
908         $item->gradepass = 4;
909         $item->hidden = 0;
910         $grade->rawgrade = 4.0;
911         $grade->finalgrade = null;
913         // Grade has pass mark and is not hidden,  user passes.
914         $this->assertEquals(
915             COMPLETION_COMPLETE_PASS,
916             completion_info::internal_get_grade_state($item, $grade));
918         // Same but user fails.
919         $grade->rawgrade = 3.9;
920         $this->assertEquals(
921             COMPLETION_COMPLETE_FAIL,
922             completion_info::internal_get_grade_state($item, $grade));
924         // User fails on raw grade but passes on final.
925         $grade->finalgrade = 4.0;
926         $this->assertEquals(
927             COMPLETION_COMPLETE_PASS,
928             completion_info::internal_get_grade_state($item, $grade));
930         // Item is hidden.
931         $item->hidden = 1;
932         $this->assertEquals(
933             COMPLETION_COMPLETE,
934             completion_info::internal_get_grade_state($item, $grade));
936         // Item isn't hidden but has no pass mark.
937         $item->hidden = 0;
938         $item->gradepass = 0;
939         $this->assertEquals(
940             COMPLETION_COMPLETE,
941             completion_info::internal_get_grade_state($item, $grade));
942     }
944     public function test_get_activities() {
945         global $CFG;
946         $this->resetAfterTest();
948         // Enable completion before creating modules, otherwise the completion data is not written in DB.
949         $CFG->enablecompletion = true;
951         // Create a course with mixed auto completion data.
952         $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
953         $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
954         $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL);
955         $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
956         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
957         $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionauto);
958         $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionmanual);
960         $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionnone);
961         $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionnone);
962         $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionnone);
964         // Create data in another course to make sure it's not considered.
965         $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
966         $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionauto);
967         $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id), $completionmanual);
968         $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id), $completionnone);
970         $c = new completion_info($course);
971         $activities = $c->get_activities();
972         $this->assertCount(3, $activities);
973         $this->assertTrue(isset($activities[$forum->cmid]));
974         $this->assertSame($forum->name, $activities[$forum->cmid]->name);
975         $this->assertTrue(isset($activities[$page->cmid]));
976         $this->assertSame($page->name, $activities[$page->cmid]->name);
977         $this->assertTrue(isset($activities[$data->cmid]));
978         $this->assertSame($data->name, $activities[$data->cmid]->name);
980         $this->assertFalse(isset($activities[$forum2->cmid]));
981         $this->assertFalse(isset($activities[$page2->cmid]));
982         $this->assertFalse(isset($activities[$data2->cmid]));
983     }
985     public function test_has_activities() {
986         global $CFG;
987         $this->resetAfterTest();
989         // Enable completion before creating modules, otherwise the completion data is not written in DB.
990         $CFG->enablecompletion = true;
992         // Create a course with mixed auto completion data.
993         $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
994         $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
995         $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
996         $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
997         $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
998         $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionnone);
1000         $c1 = new completion_info($course);
1001         $c2 = new completion_info($course2);
1003         $this->assertTrue($c1->has_activities());
1004         $this->assertFalse($c2->has_activities());
1005     }
1007     /**
1008      * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses
1009      *
1010      * @return void
1011      */
1012     public function test_course_delete_prerequisite() {
1013         global $DB;
1015         $this->setup_data();
1017         $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
1019         $criteriadata = (object) [
1020             'id' => $this->course->id,
1021             'criteria_course' => [$courseprerequisite->id],
1022         ];
1024         /** @var completion_criteria_course $criteria */
1025         $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]);
1026         $criteria->update_config($criteriadata);
1028         // Sanity test.
1029         $this->assertTrue($DB->record_exists('course_completion_criteria', [
1030             'course' => $this->course->id,
1031             'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1032             'courseinstance' => $courseprerequisite->id,
1033         ]));
1035         // Deleting the prerequisite course should remove the completion criteria.
1036         delete_course($courseprerequisite, false);
1038         $this->assertFalse($DB->record_exists('course_completion_criteria', [
1039             'course' => $this->course->id,
1040             'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1041             'courseinstance' => $courseprerequisite->id,
1042         ]));
1043     }
1045     /**
1046      * Test course module completion update event.
1047      */
1048     public function test_course_module_completion_updated_event() {
1049         global $USER, $CFG;
1051         $this->setup_data();
1053         $this->setAdminUser();
1055         $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1056         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1058         $c = new completion_info($this->course);
1059         $activities = $c->get_activities();
1060         $this->assertEquals(1, count($activities));
1061         $this->assertTrue(isset($activities[$forum->cmid]));
1062         $this->assertEquals($activities[$forum->cmid]->name, $forum->name);
1064         $current = $c->get_data($activities[$forum->cmid], false, $this->user->id);
1065         $current->completionstate = COMPLETION_COMPLETE;
1066         $current->timemodified = time();
1067         $sink = $this->redirectEvents();
1068         $c->internal_set_data($activities[$forum->cmid], $current);
1069         $events = $sink->get_events();
1070         $event = reset($events);
1071         $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
1072         $this->assertEquals($forum->cmid, $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
1073         $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
1074         $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
1075         $this->assertEquals($USER->id, $event->userid);
1076         $this->assertEquals($this->user->id, $event->relateduserid);
1077         $this->assertInstanceOf('moodle_url', $event->get_url());
1078         $this->assertEventLegacyData($current, $event);
1079     }
1081     /**
1082      * Test course completed event.
1083      */
1084     public function test_course_completed_event() {
1085         global $USER;
1087         $this->setup_data();
1088         $this->setAdminUser();
1090         $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1091         $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1093         // Mark course as complete and get triggered event.
1094         $sink = $this->redirectEvents();
1095         $ccompletion->mark_complete();
1096         $events = $sink->get_events();
1097         $event = reset($events);
1099         $this->assertInstanceOf('\core\event\course_completed', $event);
1100         $this->assertEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course);
1101         $this->assertEquals($this->course->id, $event->courseid);
1102         $this->assertEquals($USER->id, $event->userid);
1103         $this->assertEquals($this->user->id, $event->relateduserid);
1104         $this->assertEquals(context_course::instance($this->course->id), $event->get_context());
1105         $this->assertInstanceOf('moodle_url', $event->get_url());
1106         $data = $ccompletion->get_record_data();
1107         $this->assertEventLegacyData($data, $event);
1108     }
1110     /**
1111      * Test course completed message.
1112      */
1113     public function test_course_completed_message() {
1114         $this->setup_data();
1115         $this->setAdminUser();
1117         $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1118         $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1120         // Mark course as complete and get the message.
1121         $sink = $this->redirectMessages();
1122         $ccompletion->mark_complete();
1123         $messages = $sink->get_messages();
1124         $sink->close();
1126         $this->assertCount(1, $messages);
1127         $message = array_pop($messages);
1129         $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom);
1130         $this->assertEquals($this->user->id, $message->useridto);
1131         $this->assertEquals('coursecompleted', $message->eventtype);
1132         $this->assertEquals(get_string('coursecompleted', 'completion'), $message->subject);
1133         $this->assertStringContainsString($this->course->fullname, $message->fullmessage);
1134     }
1136     /**
1137      * Test course completed event.
1138      */
1139     public function test_course_completion_updated_event() {
1140         $this->setup_data();
1141         $coursecontext = context_course::instance($this->course->id);
1142         $coursecompletionevent = \core\event\course_completion_updated::create(
1143                 array(
1144                     'courseid' => $this->course->id,
1145                     'context' => $coursecontext
1146                     )
1147                 );
1149         // Mark course as complete and get triggered event.
1150         $sink = $this->redirectEvents();
1151         $coursecompletionevent->trigger();
1152         $events = $sink->get_events();
1153         $event = array_pop($events);
1154         $sink->close();
1156         $this->assertInstanceOf('\core\event\course_completion_updated', $event);
1157         $this->assertEquals($this->course->id, $event->courseid);
1158         $this->assertEquals($coursecontext, $event->get_context());
1159         $this->assertInstanceOf('moodle_url', $event->get_url());
1160         $expectedlegacylog = array($this->course->id, 'course', 'completion updated', 'completion.php?id='.$this->course->id);
1161         $this->assertEventLegacyLogData($expectedlegacylog, $event);
1162     }
1164     public function test_completion_can_view_data() {
1165         $this->setup_data();
1167         $student = $this->getDataGenerator()->create_user();
1168         $this->getDataGenerator()->enrol_user($student->id, $this->course->id);
1170         $this->setUser($student);
1171         $this->assertTrue(completion_can_view_data($student->id, $this->course->id));
1172         $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id));
1173     }
1175     /**
1176      * Data provider for test_get_grade_completion().
1177      *
1178      * @return array[]
1179      */
1180     public function get_grade_completion_provider() {
1181         return [
1182             'Grade not required' => [false, false, null, moodle_exception::class, null],
1183             'Grade required, but has no grade yet' => [true, false, null, null, COMPLETION_INCOMPLETE],
1184             'Grade required, grade received' => [true, true, null, null, COMPLETION_COMPLETE],
1185             'Grade required, passing grade received' => [true, true, 70, null, COMPLETION_COMPLETE_PASS],
1186             'Grade required, failing grade received' => [true, true, 80, null, COMPLETION_COMPLETE_FAIL],
1187         ];
1188     }
1190     /**
1191      * Test for \completion_info::get_grade_completion().
1192      *
1193      * @dataProvider get_grade_completion_provider
1194      * @param bool $completionusegrade Whether the test activity has grade completion requirement.
1195      * @param bool $hasgrade Whether to set grade for the user in this activity.
1196      * @param int|null $passinggrade Passing grade to set for the test activity.
1197      * @param string|null $expectedexception Expected exception.
1198      * @param int|null $expectedresult The expected completion status.
1199      */
1200     public function test_get_grade_completion(bool $completionusegrade, bool $hasgrade, ?int $passinggrade, ?string $expectedexception,
1201             ?int $expectedresult) {
1202         $this->setup_data();
1204         /** @var \mod_assign_generator $assigngenerator */
1205         $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
1206         $assign = $assigngenerator->create_instance([
1207             'course' => $this->course->id,
1208             'completion' => COMPLETION_ENABLED,
1209             'completionusegrade' => $completionusegrade,
1210             'gradepass' => $passinggrade,
1211         ]);
1213         $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1214         if ($completionusegrade && $hasgrade) {
1215             $assigninstance = new assign($cm->context, $cm, $this->course);
1216             $grade = $assigninstance->get_user_grade($this->user->id, true);
1217             $grade->grade = 75;
1218             $assigninstance->update_grade($grade);
1219         }
1221         $completioninfo = new completion_info($this->course);
1222         if ($expectedexception) {
1223             $this->expectException($expectedexception);
1224         }
1225         $gradecompletion = $completioninfo->get_grade_completion($cm, $this->user->id);
1226         $this->assertEquals($expectedresult, $gradecompletion);
1227     }
1230 class core_completionlib_fake_recordset implements Iterator {
1231     protected $closed;
1232     protected $values, $index;
1234     public function __construct($values) {
1235         $this->values = $values;
1236         $this->index = 0;
1237     }
1239     public function current() {
1240         return $this->values[$this->index];
1241     }
1243     public function key() {
1244         return $this->values[$this->index];
1245     }
1247     public function next() {
1248         $this->index++;
1249     }
1251     public function rewind() {
1252         $this->index = 0;
1253     }
1255     public function valid() {
1256         return count($this->values) > $this->index;
1257     }
1259     public function close() {
1260         $this->closed = true;
1261     }
1263     public function was_closed() {
1264         return $this->closed;
1265     }