Merge branch 'MDL-76985-MOODLE_400_STABLE' of https://github.com/sh-csg/moodle into...
[moodle.git] / lib / tests / completionlib_test.php
blob506141a377fccf0f4df1f77c98dc5872b1d0d599
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.
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 defined('MOODLE_INTERNAL') || die();
19 global $CFG;
20 require_once($CFG->libdir.'/completionlib.php');
22 /**
23 * Completion tests.
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
32 class completionlib_test 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);
48 /**
49 * Create course with user and activities.
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));
68 /**
69 * Asserts that two variables are equal.
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
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_array($actual) && (is_object($expected) || is_array($expected))) {
83 $actual = (object) $actual;
84 $expected = (object) $expected;
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;
94 parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
97 /**
98 * @covers ::is_enabled_for_site
99 * @covers ::is_enabled
101 public function test_is_enabled() {
102 global $CFG;
103 $this->mock_setup();
105 // Config alone.
106 $CFG->enablecompletion = COMPLETION_DISABLED;
107 $this->assertEquals(COMPLETION_DISABLED, completion_info::is_enabled_for_site());
108 $CFG->enablecompletion = COMPLETION_ENABLED;
109 $this->assertEquals(COMPLETION_ENABLED, completion_info::is_enabled_for_site());
111 // Course.
112 $course = (object)array('id' => 13);
113 $c = new completion_info($course);
114 $course->enablecompletion = COMPLETION_DISABLED;
115 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
116 $course->enablecompletion = COMPLETION_ENABLED;
117 $this->assertEquals(COMPLETION_ENABLED, $c->is_enabled());
118 $CFG->enablecompletion = COMPLETION_DISABLED;
119 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
121 // Course and CM.
122 $cm = new stdClass();
123 $cm->completion = COMPLETION_TRACKING_MANUAL;
124 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
125 $CFG->enablecompletion = COMPLETION_ENABLED;
126 $course->enablecompletion = COMPLETION_DISABLED;
127 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
128 $course->enablecompletion = COMPLETION_ENABLED;
129 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $c->is_enabled($cm));
130 $cm->completion = COMPLETION_TRACKING_NONE;
131 $this->assertEquals(COMPLETION_TRACKING_NONE, $c->is_enabled($cm));
132 $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
133 $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $c->is_enabled($cm));
137 * @covers ::update_state
139 public function test_update_state() {
140 $this->mock_setup();
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);
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);
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);
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);
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);
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);
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);
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));
250 $c->expects($this->exactly(2))
251 ->method('get_data')
252 ->with($cm, false, 100)
253 ->willReturnOnConsecutiveCalls($current1, $current2);
254 $changed1 = clone($current1);
255 $changed1->timemodified = time();
256 $changed1->completionstate = COMPLETION_COMPLETE;
257 $changed1->overrideby = 314159;
258 $comparewith1 = new phpunit_constraint_object_is_equal_with_exceptions($changed1);
259 $comparewith1->add_exception('timemodified', 'assertGreaterThanOrEqual');
260 $changed2 = clone($current2);
261 $changed2->timemodified = time();
262 $changed2->overrideby = null;
263 $changed2->completionstate = COMPLETION_INCOMPLETE;
264 $comparewith2 = new phpunit_constraint_object_is_equal_with_exceptions($changed2);
265 $comparewith2->add_exception('timemodified', 'assertGreaterThanOrEqual');
266 $c->expects($this->exactly(2))
267 ->method('internal_set_data')
268 ->withConsecutive(
269 array($cm, $comparewith1),
270 array($cm, $comparewith2)
272 $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
273 // And confirm that the status can be changed back to incomplete without an override.
274 $c->update_state($cm, COMPLETION_INCOMPLETE, 100);
276 // Auto, change state via override, incomplete to complete.
277 $c = $mockbuilder->getMock();
278 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
279 $current = (object)array('completionstate' => COMPLETION_INCOMPLETE, 'overrideby' => null);
280 $c->expects($this->once())
281 ->method('is_enabled')
282 ->with($cm)
283 ->will($this->returnValue(true));
284 $c->expects($this->once()) // Pretend the user has the required capability for overriding completion statuses.
285 ->method('user_can_override_completion')
286 ->will($this->returnValue(true));
287 $c->expects($this->once())
288 ->method('get_data')
289 ->with($cm, false, 100)
290 ->will($this->returnValue($current));
291 $changed = clone($current);
292 $changed->timemodified = time();
293 $changed->completionstate = COMPLETION_COMPLETE;
294 $changed->overrideby = 314159;
295 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
296 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
297 $c->expects($this->once())
298 ->method('internal_set_data')
299 ->with($cm, $comparewith);
300 $c->update_state($cm, COMPLETION_COMPLETE, 100, true);
302 // Now confirm the status can be changed back from complete to incomplete using an override.
303 $c = $mockbuilder->getMock();
304 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
305 $current = (object)array('completionstate' => COMPLETION_COMPLETE, 'overrideby' => 2);
306 $c->expects($this->once())
307 ->method('is_enabled')
308 ->with($cm)
309 ->will($this->returnValue(true));
310 $c->expects($this->Once()) // Pretend the user has the required capability for overriding completion statuses.
311 ->method('user_can_override_completion')
312 ->will($this->returnValue(true));
313 $c->expects($this->once())
314 ->method('get_data')
315 ->with($cm, false, 100)
316 ->will($this->returnValue($current));
317 $changed = clone($current);
318 $changed->timemodified = time();
319 $changed->completionstate = COMPLETION_INCOMPLETE;
320 $changed->overrideby = 314159;
321 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
322 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
323 $c->expects($this->once())
324 ->method('internal_set_data')
325 ->with($cm, $comparewith);
326 $c->update_state($cm, COMPLETION_INCOMPLETE, 100, true);
330 * Data provider for test_internal_get_state().
332 * @return array[]
334 public function internal_get_state_provider() {
335 return [
336 'View required, but not viewed yet' => [
337 COMPLETION_VIEW_REQUIRED, 1, '', COMPLETION_INCOMPLETE
339 'View not required and not viewed yet' => [
340 COMPLETION_VIEW_NOT_REQUIRED, 1, '', COMPLETION_INCOMPLETE
342 'View not required, grade required but no grade yet, $cm->modname not set' => [
343 COMPLETION_VIEW_NOT_REQUIRED, 1, 'modname', COMPLETION_INCOMPLETE
345 'View not required, grade required but no grade yet, $cm->course not set' => [
346 COMPLETION_VIEW_NOT_REQUIRED, 1, 'course', COMPLETION_INCOMPLETE
348 'View not required, grade not required' => [
349 COMPLETION_VIEW_NOT_REQUIRED, 0, '', COMPLETION_COMPLETE
355 * Test for completion_info::get_state().
357 * @dataProvider internal_get_state_provider
358 * @param int $completionview
359 * @param int $completionusegrade
360 * @param string $unsetfield
361 * @param int $expectedstate
362 * @covers ::internal_get_state
364 public function test_internal_get_state(int $completionview, int $completionusegrade, string $unsetfield, int $expectedstate) {
365 $this->setup_data();
367 /** @var \mod_assign_generator $assigngenerator */
368 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
369 $assign = $assigngenerator->create_instance([
370 'course' => $this->course->id,
371 'completion' => COMPLETION_ENABLED,
372 'completionview' => $completionview,
373 'completionusegrade' => $completionusegrade,
376 $userid = $this->user->id;
377 $this->setUser($userid);
379 $cm = get_coursemodule_from_instance('assign', $assign->id);
380 if ($unsetfield) {
381 unset($cm->$unsetfield);
383 // If view is required, but they haven't viewed it yet.
384 $current = (object)['viewed' => COMPLETION_NOT_VIEWED];
386 $completioninfo = new completion_info($this->course);
387 $this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, $current));
391 * Provider for the test_internal_get_state_with_grade_criteria.
393 * @return array
395 public function internal_get_state_with_grade_criteria_provider() {
396 return [
397 "Passing grade enabled and achieve. State should be COMPLETION_COMPLETE_PASS" => [
399 'completionusegrade' => 1,
400 'completionpassgrade' => 1,
401 'gradepass' => 50,
404 COMPLETION_COMPLETE_PASS
406 "Passing grade enabled and not achieve. State should be COMPLETION_COMPLETE_FAIL" => [
408 'completionusegrade' => 1,
409 'completionpassgrade' => 1,
410 'gradepass' => 50,
413 COMPLETION_COMPLETE_FAIL
415 "Passing grade not enabled with passing grade set." => [
417 'completionusegrade' => 1,
418 'gradepass' => 50,
421 COMPLETION_COMPLETE_PASS
423 "Passing grade not enabled with passing grade not set." => [
425 'completionusegrade' => 1,
428 COMPLETION_COMPLETE
430 "Passing grade not enabled with passing grade not set. No submission made." => [
432 'completionusegrade' => 1,
434 null,
435 COMPLETION_INCOMPLETE
441 * Tests that the right completion state is being set based on the grade criteria.
443 * @dataProvider internal_get_state_with_grade_criteria_provider
444 * @param array $completioncriteria The completion criteria to use
445 * @param int|null $studentgrade Grade to assign to student
446 * @param int $expectedstate Expected completion state
447 * @covers ::internal_get_state
449 public function test_internal_get_state_with_grade_criteria(array $completioncriteria, ?int $studentgrade, int $expectedstate) {
450 $this->setup_data();
452 /** @var \mod_assign_generator $assigngenerator */
453 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
454 $assign = $assigngenerator->create_instance([
455 'course' => $this->course->id,
456 'completion' => COMPLETION_ENABLED,
457 ] + $completioncriteria);
459 $userid = $this->user->id;
461 $cm = get_coursemodule_from_instance('assign', $assign->id);
462 $usercm = cm_info::create($cm, $userid);
464 // Create a teacher account.
465 $teacher = $this->getDataGenerator()->create_user();
466 $this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, 'editingteacher');
467 // Log in as the teacher.
468 $this->setUser($teacher);
470 // Grade the student for this assignment.
471 $assign = new assign($usercm->context, $cm, $cm->course);
472 if ($studentgrade) {
473 $data = (object)[
474 'sendstudentnotifications' => false,
475 'attemptnumber' => 1,
476 'grade' => $studentgrade,
478 $assign->save_grade($userid, $data);
481 // The target user already received a grade, so internal_get_state should be already complete.
482 $completioninfo = new completion_info($this->course);
483 $this->assertEquals($expectedstate, $completioninfo->internal_get_state($cm, $userid, null));
487 * Covers the case where internal_get_state() is being called for a user different from the logged in user.
489 * @covers ::internal_get_state
491 public function test_internal_get_state_with_different_user() {
492 $this->setup_data();
494 /** @var \mod_assign_generator $assigngenerator */
495 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
496 $assign = $assigngenerator->create_instance([
497 'course' => $this->course->id,
498 'completion' => COMPLETION_ENABLED,
499 'completionusegrade' => 1,
502 $userid = $this->user->id;
504 $cm = get_coursemodule_from_instance('assign', $assign->id);
505 $usercm = cm_info::create($cm, $userid);
507 // Create a teacher account.
508 $teacher = $this->getDataGenerator()->create_user();
509 $this->getDataGenerator()->enrol_user($teacher->id, $this->course->id, 'editingteacher');
510 // Log in as the teacher.
511 $this->setUser($teacher);
513 // Grade the student for this assignment.
514 $assign = new assign($usercm->context, $cm, $cm->course);
515 $data = (object)[
516 'sendstudentnotifications' => false,
517 'attemptnumber' => 1,
518 'grade' => 90,
520 $assign->save_grade($userid, $data);
522 // The target user already received a grade, so internal_get_state should be already complete.
523 $completioninfo = new completion_info($this->course);
524 $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->internal_get_state($cm, $userid, null));
526 // As the teacher which does not have a grade in this cm, internal_get_state should return incomplete.
527 $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->internal_get_state($cm, $teacher->id, null));
531 * Test for internal_get_state() for an activity that supports custom completion.
533 * @covers ::internal_get_state
535 public function test_internal_get_state_with_custom_completion() {
536 $this->setup_data();
538 $choicerecord = [
539 'course' => $this->course,
540 'completion' => COMPLETION_TRACKING_AUTOMATIC,
541 'completionsubmit' => COMPLETION_ENABLED,
543 $choice = $this->getDataGenerator()->create_module('choice', $choicerecord);
544 $cminfo = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
546 $completioninfo = new completion_info($this->course);
548 // Fetch completion for the user who hasn't made a choice yet.
549 $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
550 $this->assertEquals(COMPLETION_INCOMPLETE, $completion);
552 // Have the user make a choice.
553 $choicewithoptions = choice_get_choice($choice->id);
554 $optionids = array_keys($choicewithoptions->option);
555 choice_user_submit_response($optionids[0], $choice, $this->user->id, $this->course, $cminfo);
556 $completion = $completioninfo->internal_get_state($cminfo, $this->user->id, COMPLETION_INCOMPLETE);
557 $this->assertEquals(COMPLETION_COMPLETE, $completion);
561 * @covers ::set_module_viewed
563 public function test_set_module_viewed() {
564 $this->mock_setup();
566 $mockbuilder = $this->getMockBuilder('completion_info');
567 $mockbuilder->onlyMethods(array('is_enabled', 'get_data', 'internal_set_data', 'update_state'));
568 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
569 $cm = (object)array('id' => 13, 'course' => 42);
571 // Not tracking completion, should do nothing.
572 $c = $mockbuilder->getMock();
573 $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
574 $c->set_module_viewed($cm);
576 // Tracking completion but completion is disabled, should do nothing.
577 $c = $mockbuilder->getMock();
578 $cm->completionview = COMPLETION_VIEW_REQUIRED;
579 $c->expects($this->once())
580 ->method('is_enabled')
581 ->with($cm)
582 ->will($this->returnValue(false));
583 $c->set_module_viewed($cm);
585 // Now it's enabled, we expect it to get data. If data already has
586 // viewed, still do nothing.
587 $c = $mockbuilder->getMock();
588 $c->expects($this->once())
589 ->method('is_enabled')
590 ->with($cm)
591 ->will($this->returnValue(true));
592 $c->expects($this->once())
593 ->method('get_data')
594 ->with($cm, 0)
595 ->will($this->returnValue((object)array('viewed' => COMPLETION_VIEWED)));
596 $c->set_module_viewed($cm);
598 // OK finally one that hasn't been viewed, now it should set it viewed
599 // and update state.
600 $c = $mockbuilder->getMock();
601 $c->expects($this->once())
602 ->method('is_enabled')
603 ->with($cm)
604 ->will($this->returnValue(true));
605 $c->expects($this->once())
606 ->method('get_data')
607 ->with($cm, false, 1337)
608 ->will($this->returnValue((object)array('viewed' => COMPLETION_NOT_VIEWED)));
609 $c->expects($this->once())
610 ->method('internal_set_data')
611 ->with($cm, (object)array('viewed' => COMPLETION_VIEWED));
612 $c->expects($this->once())
613 ->method('update_state')
614 ->with($cm, COMPLETION_COMPLETE, 1337);
615 $c->set_module_viewed($cm, 1337);
619 * @covers ::count_user_data
621 public function test_count_user_data() {
622 global $DB;
623 $this->mock_setup();
625 $course = (object)array('id' => 13);
626 $cm = (object)array('id' => 42);
628 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
629 $DB->expects($this->once())
630 ->method('get_field_sql')
631 ->will($this->returnValue(666));
633 $c = new completion_info($course);
634 $this->assertEquals(666, $c->count_user_data($cm));
638 * @covers ::delete_all_state
640 public function test_delete_all_state() {
641 global $DB;
642 $this->mock_setup();
644 $course = (object)array('id' => 13);
645 $cm = (object)array('id' => 42, 'course' => 13);
646 $c = new completion_info($course);
648 // Check it works ok without data in session.
649 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
650 $DB->expects($this->once())
651 ->method('delete_records')
652 ->with('course_modules_completion', array('coursemoduleid' => 42))
653 ->will($this->returnValue(true));
654 $c->delete_all_state($cm);
658 * @covers ::reset_all_state
660 public function test_reset_all_state() {
661 global $DB;
662 $this->mock_setup();
664 $mockbuilder = $this->getMockBuilder('completion_info');
665 $mockbuilder->onlyMethods(array('delete_all_state', 'get_tracked_users', 'update_state'));
666 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
667 $c = $mockbuilder->getMock();
669 $cm = (object)array('id' => 13, 'course' => 42, 'completion' => COMPLETION_TRACKING_AUTOMATIC);
671 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
672 $DB->expects($this->once())
673 ->method('get_recordset')
674 ->will($this->returnValue(
675 new core_completionlib_fake_recordset(array((object)array('id' => 1, 'userid' => 100),
676 (object)array('id' => 2, 'userid' => 101)))));
678 $c->expects($this->once())
679 ->method('delete_all_state')
680 ->with($cm);
682 $c->expects($this->once())
683 ->method('get_tracked_users')
684 ->will($this->returnValue(array(
685 (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
686 (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
688 $c->expects($this->exactly(3))
689 ->method('update_state')
690 ->withConsecutive(
691 array($cm, COMPLETION_UNKNOWN, 100),
692 array($cm, COMPLETION_UNKNOWN, 101),
693 array($cm, COMPLETION_UNKNOWN, 201)
696 $c->reset_all_state($cm);
700 * Data provider for test_get_data().
702 * @return array[]
704 public function get_data_provider() {
705 return [
706 'No completion record' => [
707 false, true, false, COMPLETION_INCOMPLETE
709 'Not completed' => [
710 false, true, true, COMPLETION_INCOMPLETE
712 'Completed' => [
713 false, true, true, COMPLETION_COMPLETE
715 'Whole course, complete' => [
716 true, true, true, COMPLETION_COMPLETE
718 'Get data for another user, result should be not cached' => [
719 false, false, true, COMPLETION_INCOMPLETE
721 'Get data for another user, including whole course, result should be not cached' => [
722 true, false, true, COMPLETION_INCOMPLETE
728 * Tests for completion_info::get_data().
730 * @dataProvider get_data_provider
731 * @param bool $wholecourse Whole course parameter for get_data().
732 * @param bool $sameuser Whether the user calling get_data() is the user itself.
733 * @param bool $hasrecord Whether to create a course_modules_completion record.
734 * @param int $completion The completion state expected.
735 * @covers ::get_data
737 public function test_get_data(bool $wholecourse, bool $sameuser, bool $hasrecord, int $completion) {
738 global $DB;
740 $this->setup_data();
741 $user = $this->user;
743 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
744 $choice = $choicegenerator->create_instance([
745 'course' => $this->course->id,
746 'completion' => COMPLETION_TRACKING_AUTOMATIC,
747 'completionview' => true,
748 'completionsubmit' => true,
751 $cm = get_coursemodule_from_instance('choice', $choice->id);
753 // Let's manually create a course completion record instead of going through the hoops to complete an activity.
754 if ($hasrecord) {
755 $cmcompletionrecord = (object)[
756 'coursemoduleid' => $cm->id,
757 'userid' => $user->id,
758 'completionstate' => $completion,
759 'viewed' => 0,
760 'overrideby' => null,
761 'timemodified' => 0,
763 $DB->insert_record('course_modules_completion', $cmcompletionrecord);
766 // Whether we expect for the returned completion data to be stored in the cache.
767 $iscached = true;
769 if (!$sameuser) {
770 $iscached = false;
771 $this->setAdminUser();
772 } else {
773 $this->setUser($user);
776 // Mock other completion data.
777 $completioninfo = new completion_info($this->course);
779 $result = $completioninfo->get_data($cm, $wholecourse, $user->id);
781 // Course module ID of the returned completion data must match this activity's course module ID.
782 $this->assertEquals($cm->id, $result->coursemoduleid);
783 // User ID of the returned completion data must match the user's ID.
784 $this->assertEquals($user->id, $result->userid);
785 // The completion state of the returned completion data must match the expected completion state.
786 $this->assertEquals($completion, $result->completionstate);
788 // If the user has no completion record, then the default record should be returned.
789 if (!$hasrecord) {
790 $this->assertEquals(0, $result->id);
793 // Check that we are including relevant completion data for the module.
794 if (!$wholecourse) {
795 $this->assertTrue(property_exists($result, 'viewed'));
796 $this->assertTrue(property_exists($result, 'customcompletion'));
801 * @covers ::get_data
803 public function test_get_data_successive_calls(): void {
804 global $DB;
806 $this->setup_data();
807 $this->setUser($this->user);
809 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
810 $choice = $choicegenerator->create_instance([
811 'course' => $this->course->id,
812 'completion' => COMPLETION_TRACKING_AUTOMATIC,
813 'completionview' => true,
814 'completionsubmit' => true,
817 $cm = get_coursemodule_from_instance('choice', $choice->id);
819 // Let's manually create a course completion record instead of going through the hoops to complete an activity.
820 $cmcompletionrecord = (object) [
821 'coursemoduleid' => $cm->id,
822 'userid' => $this->user->id,
823 'completionstate' => COMPLETION_NOT_VIEWED,
824 'viewed' => 0,
825 'overrideby' => null,
826 'timemodified' => 0,
828 $DB->insert_record('course_modules_completion', $cmcompletionrecord);
830 // Mock other completion data.
831 $completioninfo = new completion_info($this->course);
833 $modinfo = get_fast_modinfo($this->course);
834 $results = [];
835 foreach ($modinfo->cms as $testcm) {
836 $result = $completioninfo->get_data($testcm, true);
837 $this->assertTrue(property_exists($result, 'id'));
838 $this->assertTrue(property_exists($result, 'coursemoduleid'));
839 $this->assertTrue(property_exists($result, 'userid'));
840 $this->assertTrue(property_exists($result, 'completionstate'));
841 $this->assertTrue(property_exists($result, 'viewed'));
842 $this->assertTrue(property_exists($result, 'overrideby'));
843 $this->assertTrue(property_exists($result, 'timemodified'));
844 $this->assertFalse(property_exists($result, 'other_cm_completion_data_fetched'));
846 $this->assertEquals($testcm->id, $result->coursemoduleid);
847 $this->assertEquals($this->user->id, $result->userid);
848 $this->assertEquals(0, $result->viewed);
850 $results[$testcm->id] = $result;
853 $result = $completioninfo->get_data($cm);
854 $this->assertTrue(property_exists($result, 'customcompletion'));
856 // The data should match when fetching modules individually.
857 (cache::make('core', 'completion'))->purge();
858 foreach ($modinfo->cms as $testcm) {
859 $result = $completioninfo->get_data($testcm, false);
860 $this->assertEquals($result, $results[$testcm->id]);
865 * Tests for completion_info::get_other_cm_completion_data().
867 * @covers ::get_other_cm_completion_data
869 public function test_get_other_cm_completion_data() {
870 global $DB;
872 $this->setup_data();
873 $user = $this->user;
875 $this->setAdminUser();
877 $choicegenerator = $this->getDataGenerator()->get_plugin_generator('mod_choice');
878 $choice = $choicegenerator->create_instance([
879 'course' => $this->course->id,
880 'completion' => COMPLETION_TRACKING_AUTOMATIC,
881 'completionsubmit' => true,
884 $cmchoice = cm_info::create(get_coursemodule_from_instance('choice', $choice->id));
886 $choice2 = $choicegenerator->create_instance([
887 'course' => $this->course->id,
888 'completion' => COMPLETION_TRACKING_AUTOMATIC,
891 $cmchoice2 = cm_info::create(get_coursemodule_from_instance('choice', $choice2->id));
893 $workshopgenerator = $this->getDataGenerator()->get_plugin_generator('mod_workshop');
894 $workshop = $workshopgenerator->create_instance([
895 'course' => $this->course->id,
896 'completion' => COMPLETION_TRACKING_AUTOMATIC,
897 // Submission grade required.
898 'completiongradeitemnumber' => 0,
899 'completionpassgrade' => 1,
902 $cmworkshop = cm_info::create(get_coursemodule_from_instance('workshop', $workshop->id));
904 $completioninfo = new completion_info($this->course);
906 $method = new ReflectionMethod("completion_info", "get_other_cm_completion_data");
907 $method->setAccessible(true);
909 // Check that fetching data for a module with custom completion provides its info.
910 $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id);
912 $this->assertArrayHasKey('customcompletion', $choicecompletiondata);
913 $this->assertArrayHasKey('completionsubmit', $choicecompletiondata['customcompletion']);
914 $this->assertEquals(COMPLETION_INCOMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']);
916 // Mock a choice answer so user has completed the requirement.
917 $choicemockinfo = [
918 'choiceid' => $cmchoice->instance,
919 'userid' => $this->user->id
921 $DB->insert_record('choice_answers', $choicemockinfo, false);
923 // Confirm fetching again reflects the completion.
924 $choicecompletiondata = $method->invoke($completioninfo, $cmchoice, $user->id);
925 $this->assertEquals(COMPLETION_COMPLETE, $choicecompletiondata['customcompletion']['completionsubmit']);
927 // Check that fetching data for a module with no custom completion still provides its grade completion status.
928 $workshopcompletiondata = $method->invoke($completioninfo, $cmworkshop, $user->id);
930 $this->assertArrayHasKey('completiongrade', $workshopcompletiondata);
931 $this->assertArrayHasKey('passgrade', $workshopcompletiondata);
932 $this->assertArrayNotHasKey('customcompletion', $workshopcompletiondata);
933 $this->assertEquals(COMPLETION_INCOMPLETE, $workshopcompletiondata['completiongrade']);
934 $this->assertEquals(COMPLETION_INCOMPLETE, $workshopcompletiondata['passgrade']);
936 // Check that fetching data for a module with no completion conditions does not provide any data.
937 $choice2completiondata = $method->invoke($completioninfo, $cmchoice2, $user->id);
938 $this->assertEmpty($choice2completiondata);
942 * @covers ::internal_set_data
944 public function test_internal_set_data() {
945 global $DB;
946 $this->setup_data();
948 $this->setUser($this->user);
949 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
950 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
951 $cm = get_coursemodule_from_instance('forum', $forum->id);
952 $c = new completion_info($this->course);
954 // 1) Test with new data.
955 $data = new stdClass();
956 $data->id = 0;
957 $data->userid = $this->user->id;
958 $data->coursemoduleid = $cm->id;
959 $data->completionstate = COMPLETION_COMPLETE;
960 $data->timemodified = time();
961 $data->viewed = COMPLETION_NOT_VIEWED;
962 $data->overrideby = null;
964 $c->internal_set_data($cm, $data);
965 $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id));
966 $this->assertEquals($d1, $data->id);
967 $cache = cache::make('core', 'completion');
968 // Cache was not set for another user.
969 $cachevalue = $cache->get("{$data->userid}_{$cm->course}");
970 $this->assertEquals([
971 'cacherev' => $this->course->cacherev,
972 $cm->id => array_merge(
973 (array) $data,
974 ['other_cm_completion_data_fetched' => true]
977 $cachevalue);
979 // 2) Test with existing data and for different user.
980 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
981 $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
982 $newuser = $this->getDataGenerator()->create_user();
984 $d2 = new stdClass();
985 $d2->id = 7;
986 $d2->userid = $newuser->id;
987 $d2->coursemoduleid = $cm2->id;
988 $d2->completionstate = COMPLETION_COMPLETE;
989 $d2->timemodified = time();
990 $d2->viewed = COMPLETION_NOT_VIEWED;
991 $d2->overrideby = null;
992 $c->internal_set_data($cm2, $d2);
993 // Cache for current user returns the data.
994 $cachevalue = $cache->get($data->userid . '_' . $cm->course);
995 $this->assertEquals(array_merge(
996 (array) $data,
997 ['other_cm_completion_data_fetched' => true]
998 ), $cachevalue[$cm->id]);
1000 // Cache for another user is not filled.
1001 $this->assertEquals(false, $cache->get($d2->userid . '_' . $cm2->course));
1003 // 3) Test where it THINKS the data is new (from cache) but actually in the database it has been set since.
1004 $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1005 $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
1006 $newuser2 = $this->getDataGenerator()->create_user();
1007 $d3 = new stdClass();
1008 $d3->id = 13;
1009 $d3->userid = $newuser2->id;
1010 $d3->coursemoduleid = $cm3->id;
1011 $d3->completionstate = COMPLETION_COMPLETE;
1012 $d3->timemodified = time();
1013 $d3->viewed = COMPLETION_NOT_VIEWED;
1014 $d3->overrideby = null;
1015 $DB->insert_record('course_modules_completion', $d3);
1016 $c->internal_set_data($cm, $data);
1018 // 4) Test instant course completions.
1019 $dataactivity = $this->getDataGenerator()->create_module('data', array('course' => $this->course->id),
1020 array('completion' => 1));
1021 $cm = get_coursemodule_from_instance('data', $dataactivity->id);
1022 $c = new completion_info($this->course);
1023 $cmdata = get_coursemodule_from_id('data', $dataactivity->cmid);
1025 // Add activity completion criteria.
1026 $criteriadata = new stdClass();
1027 $criteriadata->id = $this->course->id;
1028 $criteriadata->criteria_activity = array();
1029 // Some activities.
1030 $criteriadata->criteria_activity[$cmdata->id] = 1;
1031 $class = 'completion_criteria_activity';
1032 $criterion = new $class();
1033 $criterion->update_config($criteriadata);
1035 $actual = $DB->get_records('course_completions');
1036 $this->assertEmpty($actual);
1038 $data->coursemoduleid = $cm->id;
1039 $c->internal_set_data($cm, $data);
1040 $actual = $DB->get_records('course_completions');
1041 $this->assertEquals(1, count($actual));
1042 $this->assertEquals($this->user->id, reset($actual)->userid);
1044 $data->userid = $newuser2->id;
1045 $c->internal_set_data($cm, $data, true);
1046 $actual = $DB->get_records('course_completions');
1047 $this->assertEquals(1, count($actual));
1048 $this->assertEquals($this->user->id, reset($actual)->userid);
1052 * @covers ::get_progress_all
1054 public function test_get_progress_all_few() {
1055 global $DB;
1056 $this->mock_setup();
1058 $mockbuilder = $this->getMockBuilder('completion_info');
1059 $mockbuilder->onlyMethods(array('get_tracked_users'));
1060 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1061 $c = $mockbuilder->getMock();
1063 // With few results.
1064 $c->expects($this->once())
1065 ->method('get_tracked_users')
1066 ->with(false, array(), 0, '', '', '', null)
1067 ->will($this->returnValue(array(
1068 (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh'),
1069 (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy'))));
1070 $DB->expects($this->once())
1071 ->method('get_in_or_equal')
1072 ->with(array(100, 201))
1073 ->will($this->returnValue(array(' IN (100, 201)', array())));
1074 $progress1 = (object)array('userid' => 100, 'coursemoduleid' => 13);
1075 $progress2 = (object)array('userid' => 201, 'coursemoduleid' => 14);
1076 $DB->expects($this->once())
1077 ->method('get_recordset_sql')
1078 ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
1080 $this->assertEquals(array(
1081 100 => (object)array('id' => 100, 'firstname' => 'Woot', 'lastname' => 'Plugh',
1082 'progress' => array(13 => $progress1)),
1083 201 => (object)array('id' => 201, 'firstname' => 'Vroom', 'lastname' => 'Xyzzy',
1084 'progress' => array(14 => $progress2)),
1085 ), $c->get_progress_all(false));
1089 * @covers ::get_progress_all
1091 public function test_get_progress_all_lots() {
1092 global $DB;
1093 $this->mock_setup();
1095 $mockbuilder = $this->getMockBuilder('completion_info');
1096 $mockbuilder->onlyMethods(array('get_tracked_users'));
1097 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1098 $c = $mockbuilder->getMock();
1100 $tracked = array();
1101 $ids = array();
1102 $progress = array();
1103 // With more than 1000 results.
1104 for ($i = 100; $i < 2000; $i++) {
1105 $tracked[] = (object)array('id' => $i, 'firstname' => 'frog', 'lastname' => $i);
1106 $ids[] = $i;
1107 $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 13);
1108 $progress[] = (object)array('userid' => $i, 'coursemoduleid' => 14);
1110 $c->expects($this->once())
1111 ->method('get_tracked_users')
1112 ->with(true, 3, 0, '', '', '', null)
1113 ->will($this->returnValue($tracked));
1114 $DB->expects($this->exactly(2))
1115 ->method('get_in_or_equal')
1116 ->withConsecutive(
1117 array(array_slice($ids, 0, 1000)),
1118 array(array_slice($ids, 1000))
1120 ->willReturnOnConsecutiveCalls(
1121 array(' IN whatever', array()),
1122 array(' IN whatever2', array()));
1123 $DB->expects($this->exactly(2))
1124 ->method('get_recordset_sql')
1125 ->willReturnOnConsecutiveCalls(
1126 new core_completionlib_fake_recordset(array_slice($progress, 0, 1000)),
1127 new core_completionlib_fake_recordset(array_slice($progress, 1000)));
1129 $result = $c->get_progress_all(true, 3);
1130 $resultok = true;
1131 $resultok = $resultok && ($ids == array_keys($result));
1133 foreach ($result as $userid => $data) {
1134 $resultok = $resultok && $data->firstname == 'frog';
1135 $resultok = $resultok && $data->lastname == $userid;
1136 $resultok = $resultok && $data->id == $userid;
1137 $cms = $data->progress;
1138 $resultok = $resultok && (array(13, 14) == array_keys($cms));
1139 $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 13) == $cms[13]);
1140 $resultok = $resultok && ((object)array('userid' => $userid, 'coursemoduleid' => 14) == $cms[14]);
1142 $this->assertTrue($resultok);
1143 $this->assertCount(count($tracked), $result);
1147 * @covers ::inform_grade_changed
1149 public function test_inform_grade_changed() {
1150 $this->mock_setup();
1152 $mockbuilder = $this->getMockBuilder('completion_info');
1153 $mockbuilder->onlyMethods(array('is_enabled', 'update_state'));
1154 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
1156 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => null);
1157 $item = (object)array('itemnumber' => 3, 'gradepass' => 1, 'hidden' => 0);
1158 $grade = (object)array('userid' => 31337, 'finalgrade' => 0, 'rawgrade' => 0);
1160 // Not enabled (should do nothing).
1161 $c = $mockbuilder->getMock();
1162 $c->expects($this->once())
1163 ->method('is_enabled')
1164 ->with($cm)
1165 ->will($this->returnValue(false));
1166 $c->inform_grade_changed($cm, $item, $grade, false);
1168 // Enabled but still no grade completion required, should still do nothing.
1169 $c = $mockbuilder->getMock();
1170 $c->expects($this->once())
1171 ->method('is_enabled')
1172 ->with($cm)
1173 ->will($this->returnValue(true));
1174 $c->inform_grade_changed($cm, $item, $grade, false);
1176 // Enabled and completion required but item number is wrong, does nothing.
1177 $c = $mockbuilder->getMock();
1178 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 7);
1179 $c->expects($this->once())
1180 ->method('is_enabled')
1181 ->with($cm)
1182 ->will($this->returnValue(true));
1183 $c->inform_grade_changed($cm, $item, $grade, false);
1185 // Enabled and completion required and item number right. It is supposed
1186 // to call update_state with the new potential state being obtained from
1187 // internal_get_grade_state.
1188 $c = $mockbuilder->getMock();
1189 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1190 $grade = (object)array('userid' => 31337, 'finalgrade' => 1, 'rawgrade' => 0);
1191 $c->expects($this->once())
1192 ->method('is_enabled')
1193 ->with($cm)
1194 ->will($this->returnValue(true));
1195 $c->expects($this->once())
1196 ->method('update_state')
1197 ->with($cm, COMPLETION_COMPLETE_PASS, 31337)
1198 ->will($this->returnValue(true));
1199 $c->inform_grade_changed($cm, $item, $grade, false);
1201 // Same as above but marked deleted. It is supposed to call update_state
1202 // with new potential state being COMPLETION_INCOMPLETE.
1203 $c = $mockbuilder->getMock();
1204 $cm = (object)array('course' => 42, 'id' => 13, 'completion' => 0, 'completiongradeitemnumber' => 3);
1205 $grade = (object)array('userid' => 31337, 'finalgrade' => 1, 'rawgrade' => 0);
1206 $c->expects($this->once())
1207 ->method('is_enabled')
1208 ->with($cm)
1209 ->will($this->returnValue(true));
1210 $c->expects($this->once())
1211 ->method('update_state')
1212 ->with($cm, COMPLETION_INCOMPLETE, 31337)
1213 ->will($this->returnValue(true));
1214 $c->inform_grade_changed($cm, $item, $grade, true);
1218 * @covers ::internal_get_grade_state
1220 public function test_internal_get_grade_state() {
1221 $this->mock_setup();
1223 $item = new stdClass;
1224 $grade = new stdClass;
1226 $item->gradepass = 4;
1227 $item->hidden = 0;
1228 $grade->rawgrade = 4.0;
1229 $grade->finalgrade = null;
1231 // Grade has pass mark and is not hidden, user passes.
1232 $this->assertEquals(
1233 COMPLETION_COMPLETE_PASS,
1234 completion_info::internal_get_grade_state($item, $grade));
1236 // Same but user fails.
1237 $grade->rawgrade = 3.9;
1238 $this->assertEquals(
1239 COMPLETION_COMPLETE_FAIL,
1240 completion_info::internal_get_grade_state($item, $grade));
1242 // User fails on raw grade but passes on final.
1243 $grade->finalgrade = 4.0;
1244 $this->assertEquals(
1245 COMPLETION_COMPLETE_PASS,
1246 completion_info::internal_get_grade_state($item, $grade));
1248 // Item is hidden.
1249 $item->hidden = 1;
1250 $this->assertEquals(
1251 COMPLETION_COMPLETE,
1252 completion_info::internal_get_grade_state($item, $grade));
1254 // Item isn't hidden but has no pass mark.
1255 $item->hidden = 0;
1256 $item->gradepass = 0;
1257 $this->assertEquals(
1258 COMPLETION_COMPLETE,
1259 completion_info::internal_get_grade_state($item, $grade));
1263 * @test ::get_activities
1265 public function test_get_activities() {
1266 global $CFG;
1267 $this->resetAfterTest();
1269 // Enable completion before creating modules, otherwise the completion data is not written in DB.
1270 $CFG->enablecompletion = true;
1272 // Create a course with mixed auto completion data.
1273 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1274 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1275 $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL);
1276 $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
1277 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
1278 $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionauto);
1279 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionmanual);
1281 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionnone);
1282 $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionnone);
1283 $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionnone);
1285 // Create data in another course to make sure it's not considered.
1286 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1287 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionauto);
1288 $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id), $completionmanual);
1289 $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id), $completionnone);
1291 $c = new completion_info($course);
1292 $activities = $c->get_activities();
1293 $this->assertCount(3, $activities);
1294 $this->assertTrue(isset($activities[$forum->cmid]));
1295 $this->assertSame($forum->name, $activities[$forum->cmid]->name);
1296 $this->assertTrue(isset($activities[$page->cmid]));
1297 $this->assertSame($page->name, $activities[$page->cmid]->name);
1298 $this->assertTrue(isset($activities[$data->cmid]));
1299 $this->assertSame($data->name, $activities[$data->cmid]->name);
1301 $this->assertFalse(isset($activities[$forum2->cmid]));
1302 $this->assertFalse(isset($activities[$page2->cmid]));
1303 $this->assertFalse(isset($activities[$data2->cmid]));
1307 * @test ::has_activities
1309 public function test_has_activities() {
1310 global $CFG;
1311 $this->resetAfterTest();
1313 // Enable completion before creating modules, otherwise the completion data is not written in DB.
1314 $CFG->enablecompletion = true;
1316 // Create a course with mixed auto completion data.
1317 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1318 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
1319 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1320 $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
1321 $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
1322 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionnone);
1324 $c1 = new completion_info($course);
1325 $c2 = new completion_info($course2);
1327 $this->assertTrue($c1->has_activities());
1328 $this->assertFalse($c2->has_activities());
1332 * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses
1334 * @covers ::delete_course_completion_data
1335 * @covers ::delete_all_completion_data
1337 public function test_course_delete_prerequisite() {
1338 global $DB;
1340 $this->setup_data();
1342 $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
1344 $criteriadata = (object) [
1345 'id' => $this->course->id,
1346 'criteria_course' => [$courseprerequisite->id],
1349 /** @var completion_criteria_course $criteria */
1350 $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]);
1351 $criteria->update_config($criteriadata);
1353 // Sanity test.
1354 $this->assertTrue($DB->record_exists('course_completion_criteria', [
1355 'course' => $this->course->id,
1356 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1357 'courseinstance' => $courseprerequisite->id,
1358 ]));
1360 // Deleting the prerequisite course should remove the completion criteria.
1361 delete_course($courseprerequisite, false);
1363 $this->assertFalse($DB->record_exists('course_completion_criteria', [
1364 'course' => $this->course->id,
1365 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
1366 'courseinstance' => $courseprerequisite->id,
1367 ]));
1371 * Test course module completion update event.
1373 * @covers \core\event\course_module_completion_updated
1375 public function test_course_module_completion_updated_event() {
1376 global $USER, $CFG;
1378 $this->setup_data();
1380 $this->setAdminUser();
1382 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1383 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
1385 $c = new completion_info($this->course);
1386 $activities = $c->get_activities();
1387 $this->assertEquals(1, count($activities));
1388 $this->assertTrue(isset($activities[$forum->cmid]));
1389 $this->assertEquals($activities[$forum->cmid]->name, $forum->name);
1391 $current = $c->get_data($activities[$forum->cmid], false, $this->user->id);
1392 $current->completionstate = COMPLETION_COMPLETE;
1393 $current->timemodified = time();
1394 $sink = $this->redirectEvents();
1395 $c->internal_set_data($activities[$forum->cmid], $current);
1396 $events = $sink->get_events();
1397 $event = reset($events);
1398 $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
1399 $this->assertEquals($forum->cmid,
1400 $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
1401 $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
1402 $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
1403 $this->assertEquals($USER->id, $event->userid);
1404 $this->assertEquals($this->user->id, $event->relateduserid);
1405 $this->assertInstanceOf('moodle_url', $event->get_url());
1406 $this->assertEventLegacyData($current, $event);
1410 * Test course completed event.
1412 * @covers \core\event\course_completed
1414 public function test_course_completed_event() {
1415 global $USER;
1417 $this->setup_data();
1418 $this->setAdminUser();
1420 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1421 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1423 // Mark course as complete and get triggered event.
1424 $sink = $this->redirectEvents();
1425 $ccompletion->mark_complete();
1426 $events = $sink->get_events();
1427 $event = reset($events);
1429 $this->assertInstanceOf('\core\event\course_completed', $event);
1430 $this->assertEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course);
1431 $this->assertEquals($this->course->id, $event->courseid);
1432 $this->assertEquals($USER->id, $event->userid);
1433 $this->assertEquals($this->user->id, $event->relateduserid);
1434 $this->assertEquals(context_course::instance($this->course->id), $event->get_context());
1435 $this->assertInstanceOf('moodle_url', $event->get_url());
1436 $data = $ccompletion->get_record_data();
1437 $this->assertEventLegacyData($data, $event);
1441 * Test course completed message.
1443 * @covers \core\event\course_completed
1445 public function test_course_completed_message() {
1446 $this->setup_data();
1447 $this->setAdminUser();
1449 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
1450 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
1452 // Mark course as complete and get the message.
1453 $sink = $this->redirectMessages();
1454 $ccompletion->mark_complete();
1455 $messages = $sink->get_messages();
1456 $sink->close();
1458 $this->assertCount(1, $messages);
1459 $message = array_pop($messages);
1461 $this->assertEquals(core_user::get_noreply_user()->id, $message->useridfrom);
1462 $this->assertEquals($this->user->id, $message->useridto);
1463 $this->assertEquals('coursecompleted', $message->eventtype);
1464 $this->assertEquals(get_string('coursecompleted', 'completion'), $message->subject);
1465 $this->assertStringContainsString($this->course->fullname, $message->fullmessage);
1469 * Test course completed event.
1471 * @covers \core\event\course_completion_updated
1473 public function test_course_completion_updated_event() {
1474 $this->setup_data();
1475 $coursecontext = context_course::instance($this->course->id);
1476 $coursecompletionevent = \core\event\course_completion_updated::create(
1477 array(
1478 'courseid' => $this->course->id,
1479 'context' => $coursecontext
1483 // Mark course as complete and get triggered event.
1484 $sink = $this->redirectEvents();
1485 $coursecompletionevent->trigger();
1486 $events = $sink->get_events();
1487 $event = array_pop($events);
1488 $sink->close();
1490 $this->assertInstanceOf('\core\event\course_completion_updated', $event);
1491 $this->assertEquals($this->course->id, $event->courseid);
1492 $this->assertEquals($coursecontext, $event->get_context());
1493 $this->assertInstanceOf('moodle_url', $event->get_url());
1494 $expectedlegacylog = array($this->course->id, 'course', 'completion updated', 'completion.php?id='.$this->course->id);
1495 $this->assertEventLegacyLogData($expectedlegacylog, $event);
1499 * @covers \completion_can_view_data
1501 public function test_completion_can_view_data() {
1502 $this->setup_data();
1504 $student = $this->getDataGenerator()->create_user();
1505 $this->getDataGenerator()->enrol_user($student->id, $this->course->id);
1507 $this->setUser($student);
1508 $this->assertTrue(completion_can_view_data($student->id, $this->course->id));
1509 $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id));
1513 * Data provider for test_get_grade_completion().
1515 * @return array[]
1517 public function get_grade_completion_provider() {
1518 return [
1519 'Grade not required' => [false, false, null, null, null],
1520 'Grade required, but has no grade yet' => [true, false, null, null, COMPLETION_INCOMPLETE],
1521 'Grade required, grade received' => [true, true, null, null, COMPLETION_COMPLETE],
1522 'Grade required, passing grade received' => [true, true, 70, null, COMPLETION_COMPLETE_PASS],
1523 'Grade required, failing grade received' => [true, true, 80, null, COMPLETION_COMPLETE_FAIL],
1528 * Test for \completion_info::get_grade_completion().
1530 * @dataProvider get_grade_completion_provider
1531 * @param bool $completionusegrade Whether the test activity has grade completion requirement.
1532 * @param bool $hasgrade Whether to set grade for the user in this activity.
1533 * @param int|null $passinggrade Passing grade to set for the test activity.
1534 * @param string|null $expectedexception Expected exception.
1535 * @param int|null $expectedresult The expected completion status.
1536 * @covers ::get_grade_completion
1538 public function test_get_grade_completion(bool $completionusegrade, bool $hasgrade, ?int $passinggrade,
1539 ?string $expectedexception, ?int $expectedresult) {
1540 $this->setup_data();
1542 /** @var \mod_assign_generator $assigngenerator */
1543 $assigngenerator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
1544 $assign = $assigngenerator->create_instance([
1545 'course' => $this->course->id,
1546 'completion' => COMPLETION_ENABLED,
1547 'completionusegrade' => $completionusegrade,
1548 'gradepass' => $passinggrade,
1551 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1552 if ($completionusegrade && $hasgrade) {
1553 $assigninstance = new assign($cm->context, $cm, $this->course);
1554 $grade = $assigninstance->get_user_grade($this->user->id, true);
1555 $grade->grade = 75;
1556 $assigninstance->update_grade($grade);
1559 $completioninfo = new completion_info($this->course);
1560 if ($expectedexception) {
1561 $this->expectException($expectedexception);
1563 $gradecompletion = $completioninfo->get_grade_completion($cm, $this->user->id);
1564 $this->assertEquals($expectedresult, $gradecompletion);
1568 * Test the return value for cases when the activity module does not have associated grade_item.
1570 * @covers ::get_grade_completion
1572 public function test_get_grade_completion_without_grade_item() {
1573 global $DB;
1575 $this->setup_data();
1577 $assign = $this->getDataGenerator()->get_plugin_generator('mod_assign')->create_instance([
1578 'course' => $this->course->id,
1579 'completion' => COMPLETION_ENABLED,
1580 'completionusegrade' => true,
1581 'gradepass' => 42,
1584 $cm = cm_info::create(get_coursemodule_from_instance('assign', $assign->id));
1586 $DB->delete_records('grade_items', [
1587 'courseid' => $this->course->id,
1588 'itemtype' => 'mod',
1589 'itemmodule' => 'assign',
1590 'iteminstance' => $assign->id,
1593 // Without the grade_item, the activity is considered incomplete.
1594 $completioninfo = new completion_info($this->course);
1595 $this->assertEquals(COMPLETION_INCOMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id));
1597 // Once the activity is graded, the grade_item is automatically created.
1598 $assigninstance = new assign($cm->context, $cm, $this->course);
1599 $grade = $assigninstance->get_user_grade($this->user->id, true);
1600 $grade->grade = 40;
1601 $assigninstance->update_grade($grade);
1603 // The implicitly created grade_item does not have grade to pass defined so it is not distinguished.
1604 $this->assertEquals(COMPLETION_COMPLETE, $completioninfo->get_grade_completion($cm, $this->user->id));
1608 * Test for aggregate_completions().
1610 * @covers \aggregate_completions
1612 public function test_aggregate_completions() {
1613 global $DB;
1614 $this->resetAfterTest(true);
1615 $time = time();
1617 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1619 for ($i = 0; $i < 4; $i++) {
1620 $students[] = $this->getDataGenerator()->create_user();
1623 $teacher = $this->getDataGenerator()->create_user();
1624 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1625 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1627 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1628 foreach ($students as $student) {
1629 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1632 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id),
1633 array('completion' => 1));
1634 $cmdata = get_coursemodule_from_id('data', $data->cmid);
1636 // Add activity completion criteria.
1637 $criteriadata = new stdClass();
1638 $criteriadata->id = $course->id;
1639 $criteriadata->criteria_activity = array();
1640 // Some activities.
1641 $criteriadata->criteria_activity[$cmdata->id] = 1;
1642 $class = 'completion_criteria_activity';
1643 $criterion = new $class();
1644 $criterion->update_config($criteriadata);
1646 $this->setUser($teacher);
1648 // Mark activity complete for both students.
1649 $cm = get_coursemodule_from_instance('data', $data->id);
1650 $completioncriteria = $DB->get_record('course_completion_criteria', []);
1651 foreach ($students as $student) {
1652 $cmcompletionrecords[] = (object)[
1653 'coursemoduleid' => $cm->id,
1654 'userid' => $student->id,
1655 'completionstate' => 1,
1656 'viewed' => 0,
1657 'overrideby' => null,
1658 'timemodified' => 0,
1661 $usercompletions[] = (object)[
1662 'criteriaid' => $completioncriteria->id,
1663 'userid' => $student->id,
1664 'timecompleted' => $time,
1667 $cc = array(
1668 'course' => $course->id,
1669 'userid' => $student->id
1671 $ccompletion = new completion_completion($cc);
1672 $completion[] = $ccompletion->mark_inprogress($time);
1674 $DB->insert_records('course_modules_completion', $cmcompletionrecords);
1675 $DB->insert_records('course_completion_crit_compl', $usercompletions);
1677 // MDL-33320: for instant completions we need aggregate to work in a single run.
1678 $DB->set_field('course_completions', 'reaggregate', $time - 2);
1680 foreach ($students as $student) {
1681 $result = $DB->get_record('course_completions', ['userid' => $student->id, 'reaggregate' => 0]);
1682 $this->assertFalse($result);
1685 aggregate_completions($completion[0]);
1687 $result1 = $DB->get_record('course_completions', ['userid' => $students[0]->id, 'reaggregate' => 0]);
1688 $result2 = $DB->get_record('course_completions', ['userid' => $students[1]->id, 'reaggregate' => 0]);
1689 $result3 = $DB->get_record('course_completions', ['userid' => $students[2]->id, 'reaggregate' => 0]);
1691 $this->assertIsObject($result1);
1692 $this->assertFalse($result2);
1693 $this->assertFalse($result3);
1695 aggregate_completions(0);
1697 foreach ($students as $student) {
1698 $result = $DB->get_record('course_completions', ['userid' => $student->id, 'reaggregate' => 0]);
1699 $this->assertIsObject($result);
1704 * Test for completion_completion::_save().
1706 * @covers \completion_completion::_save
1708 public function test_save() {
1709 global $DB;
1710 $this->resetAfterTest(true);
1712 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1714 $student = $this->getDataGenerator()->create_user();
1715 $teacher = $this->getDataGenerator()->create_user();
1716 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1717 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1719 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1720 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1722 $this->setUser($teacher);
1724 $cc = array(
1725 'course' => $course->id,
1726 'userid' => $student->id
1728 $ccompletion = new completion_completion($cc);
1730 $completions = $DB->get_records('course_completions');
1731 $this->assertEmpty($completions);
1733 // We're testing a private method, so we need to setup reflector magic.
1734 $method = new ReflectionMethod($ccompletion, '_save');
1735 $method->setAccessible(true); // Allow accessing of private method.
1736 $completionid = $method->invoke($ccompletion);
1737 $completions = $DB->get_records('course_completions');
1738 $this->assertEquals(count($completions), 1);
1739 $this->assertEquals(reset($completions)->id, $completionid);
1741 $ccompletion->id = 0;
1742 $method = new ReflectionMethod($ccompletion, '_save');
1743 $method->setAccessible(true); // Allow accessing of private method.
1744 $completionid = $method->invoke($ccompletion);
1745 $this->assertDebuggingCalled('Can not update data object, no id!');
1746 $this->assertNull($completionid);
1750 * Test for completion_completion::mark_enrolled().
1752 * @covers \completion_completion::mark_enrolled
1754 public function test_mark_enrolled() {
1755 global $DB;
1756 $this->resetAfterTest(true);
1758 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1760 $student = $this->getDataGenerator()->create_user();
1761 $teacher = $this->getDataGenerator()->create_user();
1762 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1763 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1765 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1766 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1768 $this->setUser($teacher);
1770 $cc = array(
1771 'course' => $course->id,
1772 'userid' => $student->id
1774 $ccompletion = new completion_completion($cc);
1776 $completions = $DB->get_records('course_completions');
1777 $this->assertEmpty($completions);
1779 $completionid = $ccompletion->mark_enrolled();
1780 $completions = $DB->get_records('course_completions');
1781 $this->assertEquals(count($completions), 1);
1782 $this->assertEquals(reset($completions)->id, $completionid);
1784 $ccompletion->id = 0;
1785 $completionid = $ccompletion->mark_enrolled();
1786 $this->assertDebuggingCalled('Can not update data object, no id!');
1787 $this->assertNull($completionid);
1788 $completions = $DB->get_records('course_completions');
1789 $this->assertEquals(1, count($completions));
1793 * Test for completion_completion::mark_inprogress().
1795 * @covers \completion_completion::mark_inprogress
1797 public function test_mark_inprogress() {
1798 global $DB;
1799 $this->resetAfterTest(true);
1801 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1803 $student = $this->getDataGenerator()->create_user();
1804 $teacher = $this->getDataGenerator()->create_user();
1805 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1806 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1808 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1809 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1811 $this->setUser($teacher);
1813 $cc = array(
1814 'course' => $course->id,
1815 'userid' => $student->id
1817 $ccompletion = new completion_completion($cc);
1819 $completions = $DB->get_records('course_completions');
1820 $this->assertEmpty($completions);
1822 $completionid = $ccompletion->mark_inprogress();
1823 $completions = $DB->get_records('course_completions');
1824 $this->assertEquals(1, count($completions));
1825 $this->assertEquals(reset($completions)->id, $completionid);
1827 $ccompletion->id = 0;
1828 $completionid = $ccompletion->mark_inprogress();
1829 $this->assertDebuggingCalled('Can not update data object, no id!');
1830 $this->assertNull($completionid);
1831 $completions = $DB->get_records('course_completions');
1832 $this->assertEquals(1, count($completions));
1836 * Test for completion_completion::mark_complete().
1838 * @covers \completion_completion::mark_complete
1840 public function test_mark_complete() {
1841 global $DB;
1842 $this->resetAfterTest(true);
1844 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1846 $student = $this->getDataGenerator()->create_user();
1847 $teacher = $this->getDataGenerator()->create_user();
1848 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1849 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1851 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1852 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1854 $this->setUser($teacher);
1856 $cc = array(
1857 'course' => $course->id,
1858 'userid' => $student->id
1860 $ccompletion = new completion_completion($cc);
1862 $completions = $DB->get_records('course_completions');
1863 $this->assertEmpty($completions);
1865 $completionid = $ccompletion->mark_complete();
1866 $completions = $DB->get_records('course_completions');
1867 $this->assertEquals(1, count($completions));
1868 $this->assertEquals(reset($completions)->id, $completionid);
1870 $ccompletion->id = 0;
1871 $completionid = $ccompletion->mark_complete();
1872 $this->assertNull($completionid);
1873 $completions = $DB->get_records('course_completions');
1874 $this->assertEquals(1, count($completions));
1878 * Test for completion_criteria_completion::mark_complete().
1880 * @covers \completion_criteria_completion::mark_complete
1882 public function test_criteria_mark_complete() {
1883 global $DB;
1884 $this->resetAfterTest(true);
1886 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
1888 $student = $this->getDataGenerator()->create_user();
1889 $teacher = $this->getDataGenerator()->create_user();
1890 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1891 $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
1893 $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
1894 $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id);
1896 $this->setUser($teacher);
1898 $record = [
1899 'course' => $course->id,
1900 'criteriaid' => 1,
1901 'userid' => $student->id,
1902 'timecompleted' => time()
1904 $completion = new completion_criteria_completion($record, DATA_OBJECT_FETCH_BY_KEY);
1906 $completions = $DB->get_records('course_completions');
1907 $this->assertEmpty($completions);
1909 $completionid = $completion->mark_complete($record['timecompleted']);
1910 $completions = $DB->get_records('course_completions');
1911 $this->assertEquals(1, count($completions));
1912 $this->assertEquals(reset($completions)->id, $completionid);
1916 class core_completionlib_fake_recordset implements Iterator {
1917 protected $closed;
1918 protected $values, $index;
1920 public function __construct($values) {
1921 $this->values = $values;
1922 $this->index = 0;
1925 public function current() {
1926 return $this->values[$this->index];
1929 public function key() {
1930 return $this->values[$this->index];
1933 public function next() {
1934 $this->index++;
1937 public function rewind() {
1938 $this->index = 0;
1941 public function valid() {
1942 return count($this->values) > $this->index;
1945 public function close() {
1946 $this->closed = true;
1949 public function was_closed() {
1950 return $this->closed;