MDL-68525 behat: chrome/switches caps not allowed in Chrome > 81
[moodle.git] / lib / tests / completionlib_test.php
blob53e4071882fe8c6342b313d536fb73f179cdf610
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 /**
18 * Completion tests.
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
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);
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_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;
90 parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
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));
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);
328 public function test_internal_get_state() {
329 global $DB;
330 $this->mock_setup();
332 $mockbuilder = $this->getMockBuilder('completion_info');
333 $mockbuilder->setMethods(array('internal_get_grade_state'));
334 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
335 $c = $mockbuilder->getMock();
337 $cm = (object)array('id'=>13, 'course'=>42, 'completiongradeitemnumber'=>null);
339 // If view is required, but they haven't viewed it yet.
340 $cm->completionview = COMPLETION_VIEW_REQUIRED;
341 $current = (object)array('viewed'=>COMPLETION_NOT_VIEWED);
342 $this->assertEquals(COMPLETION_INCOMPLETE, $c->internal_get_state($cm, 123, $current));
344 // OK set view not required.
345 $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
347 // Test not getting module name.
348 $cm->modname='label';
349 $this->assertEquals(COMPLETION_COMPLETE, $c->internal_get_state($cm, 123, $current));
351 // Test getting module name.
352 $cm->module = 13;
353 unset($cm->modname);
354 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
355 $DB->expects($this->once())
356 ->method('get_field')
357 ->with('modules', 'name', array('id'=>13))
358 ->will($this->returnValue('lable'));
359 $this->assertEquals(COMPLETION_COMPLETE, $c->internal_get_state($cm, 123, $current));
361 // Note: This function is not fully tested (including kind of the main part) because:
362 // * the grade_item/grade_grade calls are static and can't be mocked,
363 // * the plugin_supports call is static and can't be mocked.
366 public function test_set_module_viewed() {
367 $this->mock_setup();
369 $mockbuilder = $this->getMockBuilder('completion_info');
370 $mockbuilder->setMethods(array('is_enabled', 'get_data', 'internal_set_data', 'update_state'));
371 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
372 $c = $mockbuilder->getMock();
373 $cm = (object)array('id'=>13, 'course'=>42);
375 // Not tracking completion, should do nothing.
376 $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
377 $c->set_module_viewed($cm);
379 // Tracking completion but completion is disabled, should do nothing.
380 $cm->completionview = COMPLETION_VIEW_REQUIRED;
381 $c->expects($this->at(0))
382 ->method('is_enabled')
383 ->with($cm)
384 ->will($this->returnValue(false));
385 $c->set_module_viewed($cm);
387 // Now it's enabled, we expect it to get data. If data already has
388 // viewed, still do nothing.
389 $c->expects($this->at(0))
390 ->method('is_enabled')
391 ->with($cm)
392 ->will($this->returnValue(true));
393 $c->expects($this->at(1))
394 ->method('get_data')
395 ->with($cm, 0)
396 ->will($this->returnValue((object)array('viewed'=>COMPLETION_VIEWED)));
397 $c->set_module_viewed($cm);
399 // OK finally one that hasn't been viewed, now it should set it viewed
400 // and update state.
401 $c->expects($this->at(0))
402 ->method('is_enabled')
403 ->with($cm)
404 ->will($this->returnValue(true));
405 $c->expects($this->at(1))
406 ->method('get_data')
407 ->with($cm, false, 1337)
408 ->will($this->returnValue((object)array('viewed'=>COMPLETION_NOT_VIEWED)));
409 $c->expects($this->at(2))
410 ->method('internal_set_data')
411 ->with($cm, (object)array('viewed'=>COMPLETION_VIEWED));
412 $c->expects($this->at(3))
413 ->method('update_state')
414 ->with($cm, COMPLETION_COMPLETE, 1337);
415 $c->set_module_viewed($cm, 1337);
418 public function test_count_user_data() {
419 global $DB;
420 $this->mock_setup();
422 $course = (object)array('id'=>13);
423 $cm = (object)array('id'=>42);
425 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
426 $DB->expects($this->at(0))
427 ->method('get_field_sql')
428 ->will($this->returnValue(666));
430 $c = new completion_info($course);
431 $this->assertEquals(666, $c->count_user_data($cm));
434 public function test_delete_all_state() {
435 global $DB;
436 $this->mock_setup();
438 $course = (object)array('id'=>13);
439 $cm = (object)array('id'=>42, 'course'=>13);
440 $c = new completion_info($course);
442 // Check it works ok without data in session.
443 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
444 $DB->expects($this->at(0))
445 ->method('delete_records')
446 ->with('course_modules_completion', array('coursemoduleid'=>42))
447 ->will($this->returnValue(true));
448 $c->delete_all_state($cm);
451 public function test_reset_all_state() {
452 global $DB;
453 $this->mock_setup();
455 $mockbuilder = $this->getMockBuilder('completion_info');
456 $mockbuilder->setMethods(array('delete_all_state', 'get_tracked_users', 'update_state'));
457 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
458 $c = $mockbuilder->getMock();
460 $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
462 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
463 $DB->expects($this->at(0))
464 ->method('get_recordset')
465 ->will($this->returnValue(
466 new core_completionlib_fake_recordset(array((object)array('id'=>1, 'userid'=>100), (object)array('id'=>2, 'userid'=>101)))));
468 $c->expects($this->at(0))
469 ->method('delete_all_state')
470 ->with($cm);
472 $c->expects($this->at(1))
473 ->method('get_tracked_users')
474 ->will($this->returnValue(array(
475 (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'),
476 (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy'))));
478 $c->expects($this->at(2))
479 ->method('update_state')
480 ->with($cm, COMPLETION_UNKNOWN, 100);
481 $c->expects($this->at(3))
482 ->method('update_state')
483 ->with($cm, COMPLETION_UNKNOWN, 101);
484 $c->expects($this->at(4))
485 ->method('update_state')
486 ->with($cm, COMPLETION_UNKNOWN, 201);
488 $c->reset_all_state($cm);
491 public function test_get_data() {
492 global $DB;
493 $this->mock_setup();
495 $cache = cache::make('core', 'completion');
497 $c = new completion_info((object)array('id'=>42, 'cacherev'=>1));
498 $cm = (object)array('id'=>13, 'course'=>42);
500 // 1. Not current user, record exists.
501 $sillyrecord = (object)array('frog'=>'kermit');
503 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
504 $DB->expects($this->at(0))
505 ->method('get_record')
506 ->with('course_modules_completion', array('coursemoduleid'=>13, 'userid'=>123))
507 ->will($this->returnValue($sillyrecord));
508 $result = $c->get_data($cm, false, 123);
509 $this->assertEquals($sillyrecord, $result);
510 $this->assertEquals(false, $cache->get('123_42')); // Not current user is not cached.
512 // 2. Not current user, default record, whole course.
513 $cache->purge();
514 $DB->expects($this->at(0))
515 ->method('get_records_sql')
516 ->will($this->returnValue(array()));
517 $modinfo = new stdClass();
518 $modinfo->cms = array((object)array('id'=>13));
519 $result=$c->get_data($cm, true, 123, $modinfo);
520 $this->assertEquals((object)array(
521 'id' => '0', 'coursemoduleid' => 13, 'userid' => 123, 'completionstate' => 0,
522 'viewed' => 0, 'timemodified' => 0, 'overrideby' => 0), $result);
523 $this->assertEquals(false, $cache->get('123_42')); // Not current user is not cached.
525 // 3. Current user, single record, not from cache.
526 $DB->expects($this->at(0))
527 ->method('get_record')
528 ->with('course_modules_completion', array('coursemoduleid'=>13, 'userid'=>314159))
529 ->will($this->returnValue($sillyrecord));
530 $result = $c->get_data($cm);
531 $this->assertEquals($sillyrecord, $result);
532 $cachevalue = $cache->get('314159_42');
533 $this->assertEquals((array)$sillyrecord, $cachevalue[13]);
535 // 4. Current user, 'whole course', but from cache.
536 $result = $c->get_data($cm, true);
537 $this->assertEquals($sillyrecord, $result);
539 // 5. Current user, 'whole course' and record not in cache.
540 $cache->purge();
542 // Scenario: Completion data exists for one CMid.
543 $basicrecord = (object)array('coursemoduleid'=>13);
544 $DB->expects($this->at(0))
545 ->method('get_records_sql')
546 ->will($this->returnValue(array('1'=>$basicrecord)));
548 // There are two CMids in total, the one we had data for and another one.
549 $modinfo = new stdClass();
550 $modinfo->cms = array((object)array('id'=>13), (object)array('id'=>14));
551 $result = $c->get_data($cm, true, 0, $modinfo);
553 // Check result.
554 $this->assertEquals($basicrecord, $result);
556 // Check the cache contents.
557 $cachevalue = $cache->get('314159_42');
558 $this->assertEquals($basicrecord, (object)$cachevalue[13]);
559 $this->assertEquals(array('id' => '0', 'coursemoduleid' => 14,
560 'userid' => 314159, 'completionstate' => 0, 'viewed' => 0, 'overrideby' => 0, 'timemodified' => 0),
561 $cachevalue[14]);
564 public function test_internal_set_data() {
565 global $DB;
566 $this->setup_data();
568 $this->setUser($this->user);
569 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
570 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
571 $cm = get_coursemodule_from_instance('forum', $forum->id);
572 $c = new completion_info($this->course);
574 // 1) Test with new data.
575 $data = new stdClass();
576 $data->id = 0;
577 $data->userid = $this->user->id;
578 $data->coursemoduleid = $cm->id;
579 $data->completionstate = COMPLETION_COMPLETE;
580 $data->timemodified = time();
581 $data->viewed = COMPLETION_NOT_VIEWED;
582 $data->overrideby = null;
584 $c->internal_set_data($cm, $data);
585 $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id));
586 $this->assertEquals($d1, $data->id);
587 $cache = cache::make('core', 'completion');
588 // Cache was not set for another user.
589 $this->assertEquals(array('cacherev' => $this->course->cacherev, $cm->id => $data),
590 $cache->get($data->userid . '_' . $cm->course));
592 // 2) Test with existing data and for different user.
593 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
594 $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
595 $newuser = $this->getDataGenerator()->create_user();
597 $d2 = new stdClass();
598 $d2->id = 7;
599 $d2->userid = $newuser->id;
600 $d2->coursemoduleid = $cm2->id;
601 $d2->completionstate = COMPLETION_COMPLETE;
602 $d2->timemodified = time();
603 $d2->viewed = COMPLETION_NOT_VIEWED;
604 $d2->overrideby = null;
605 $c->internal_set_data($cm2, $d2);
606 // Cache for current user returns the data.
607 $cachevalue = $cache->get($data->userid . '_' . $cm->course);
608 $this->assertEquals($data, $cachevalue[$cm->id]);
609 // Cache for another user is not filled.
610 $this->assertEquals(false, $cache->get($d2->userid . '_' . $cm2->course));
612 // 3) Test where it THINKS the data is new (from cache) but actually
613 // in the database it has been set since.
614 // 1) Test with new data.
615 $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
616 $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
617 $newuser2 = $this->getDataGenerator()->create_user();
618 $d3 = new stdClass();
619 $d3->id = 13;
620 $d3->userid = $newuser2->id;
621 $d3->coursemoduleid = $cm3->id;
622 $d3->completionstate = COMPLETION_COMPLETE;
623 $d3->timemodified = time();
624 $d3->viewed = COMPLETION_NOT_VIEWED;
625 $d3->overrideby = null;
626 $DB->insert_record('course_modules_completion', $d3);
627 $c->internal_set_data($cm, $data);
630 public function test_get_progress_all() {
631 global $DB;
632 $this->mock_setup();
634 $mockbuilder = $this->getMockBuilder('completion_info');
635 $mockbuilder->setMethods(array('get_tracked_users'));
636 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
637 $c = $mockbuilder->getMock();
639 // 1) Basic usage.
640 $c->expects($this->at(0))
641 ->method('get_tracked_users')
642 ->with(false, array(), 0, '', '', '', null)
643 ->will($this->returnValue(array(
644 (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'),
645 (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy'))));
646 $DB->expects($this->at(0))
647 ->method('get_in_or_equal')
648 ->with(array(100, 201))
649 ->will($this->returnValue(array(' IN (100, 201)', array())));
650 $progress1 = (object)array('userid'=>100, 'coursemoduleid'=>13);
651 $progress2 = (object)array('userid'=>201, 'coursemoduleid'=>14);
652 $DB->expects($this->at(1))
653 ->method('get_recordset_sql')
654 ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
656 $this->assertEquals(array(
657 100 => (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh',
658 'progress'=>array(13=>$progress1)),
659 201 => (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy',
660 'progress'=>array(14=>$progress2)),
661 ), $c->get_progress_all(false));
663 // 2) With more than 1, 000 results.
664 $tracked = array();
665 $ids = array();
666 $progress = array();
667 for ($i = 100; $i<2000; $i++) {
668 $tracked[] = (object)array('id'=>$i, 'firstname'=>'frog', 'lastname'=>$i);
669 $ids[] = $i;
670 $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>13);
671 $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>14);
673 $c->expects($this->at(0))
674 ->method('get_tracked_users')
675 ->with(true, 3, 0, '', '', '', null)
676 ->will($this->returnValue($tracked));
677 $DB->expects($this->at(0))
678 ->method('get_in_or_equal')
679 ->with(array_slice($ids, 0, 1000))
680 ->will($this->returnValue(array(' IN whatever', array())));
681 $DB->expects($this->at(1))
682 ->method('get_recordset_sql')
683 ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 0, 1000))));
685 $DB->expects($this->at(2))
686 ->method('get_in_or_equal')
687 ->with(array_slice($ids, 1000))
688 ->will($this->returnValue(array(' IN whatever2', array())));
689 $DB->expects($this->at(3))
690 ->method('get_recordset_sql')
691 ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 1000))));
693 $result = $c->get_progress_all(true, 3);
694 $resultok = true;
695 $resultok = $resultok && ($ids == array_keys($result));
697 foreach ($result as $userid => $data) {
698 $resultok = $resultok && $data->firstname == 'frog';
699 $resultok = $resultok && $data->lastname == $userid;
700 $resultok = $resultok && $data->id == $userid;
701 $cms = $data->progress;
702 $resultok = $resultok && (array(13, 14) == array_keys($cms));
703 $resultok = $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>13) == $cms[13]);
704 $resultok = $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>14) == $cms[14]);
706 $this->assertTrue($resultok);
709 public function test_inform_grade_changed() {
710 $this->mock_setup();
712 $mockbuilder = $this->getMockBuilder('completion_info');
713 $mockbuilder->setMethods(array('is_enabled', 'update_state'));
714 $mockbuilder->setConstructorArgs(array((object)array('id' => 42)));
715 $c = $mockbuilder->getMock();
717 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>null);
718 $item = (object)array('itemnumber'=>3, 'gradepass'=>1, 'hidden'=>0);
719 $grade = (object)array('userid'=>31337, 'finalgrade'=>0, 'rawgrade'=>0);
721 // Not enabled (should do nothing).
722 $c->expects($this->at(0))
723 ->method('is_enabled')
724 ->with($cm)
725 ->will($this->returnValue(false));
726 $c->inform_grade_changed($cm, $item, $grade, false);
728 // Enabled but still no grade completion required, should still do nothing.
729 $c->expects($this->at(0))
730 ->method('is_enabled')
731 ->with($cm)
732 ->will($this->returnValue(true));
733 $c->inform_grade_changed($cm, $item, $grade, false);
735 // Enabled and completion required but item number is wrong, does nothing.
736 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>7);
737 $c->expects($this->at(0))
738 ->method('is_enabled')
739 ->with($cm)
740 ->will($this->returnValue(true));
741 $c->inform_grade_changed($cm, $item, $grade, false);
743 // Enabled and completion required and item number right. It is supposed
744 // to call update_state with the new potential state being obtained from
745 // internal_get_grade_state.
746 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3);
747 $grade = (object)array('userid'=>31337, 'finalgrade'=>1, 'rawgrade'=>0);
748 $c->expects($this->at(0))
749 ->method('is_enabled')
750 ->with($cm)
751 ->will($this->returnValue(true));
752 $c->expects($this->at(1))
753 ->method('update_state')
754 ->with($cm, COMPLETION_COMPLETE_PASS, 31337)
755 ->will($this->returnValue(true));
756 $c->inform_grade_changed($cm, $item, $grade, false);
758 // Same as above but marked deleted. It is supposed to call update_state
759 // with new potential state being COMPLETION_INCOMPLETE.
760 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3);
761 $grade = (object)array('userid'=>31337, 'finalgrade'=>1, 'rawgrade'=>0);
762 $c->expects($this->at(0))
763 ->method('is_enabled')
764 ->with($cm)
765 ->will($this->returnValue(true));
766 $c->expects($this->at(1))
767 ->method('update_state')
768 ->with($cm, COMPLETION_INCOMPLETE, 31337)
769 ->will($this->returnValue(true));
770 $c->inform_grade_changed($cm, $item, $grade, true);
773 public function test_internal_get_grade_state() {
774 $this->mock_setup();
776 $item = new stdClass;
777 $grade = new stdClass;
779 $item->gradepass = 4;
780 $item->hidden = 0;
781 $grade->rawgrade = 4.0;
782 $grade->finalgrade = null;
784 // Grade has pass mark and is not hidden, user passes.
785 $this->assertEquals(
786 COMPLETION_COMPLETE_PASS,
787 completion_info::internal_get_grade_state($item, $grade));
789 // Same but user fails.
790 $grade->rawgrade = 3.9;
791 $this->assertEquals(
792 COMPLETION_COMPLETE_FAIL,
793 completion_info::internal_get_grade_state($item, $grade));
795 // User fails on raw grade but passes on final.
796 $grade->finalgrade = 4.0;
797 $this->assertEquals(
798 COMPLETION_COMPLETE_PASS,
799 completion_info::internal_get_grade_state($item, $grade));
801 // Item is hidden.
802 $item->hidden = 1;
803 $this->assertEquals(
804 COMPLETION_COMPLETE,
805 completion_info::internal_get_grade_state($item, $grade));
807 // Item isn't hidden but has no pass mark.
808 $item->hidden = 0;
809 $item->gradepass = 0;
810 $this->assertEquals(
811 COMPLETION_COMPLETE,
812 completion_info::internal_get_grade_state($item, $grade));
815 public function test_get_activities() {
816 global $CFG;
817 $this->resetAfterTest();
819 // Enable completion before creating modules, otherwise the completion data is not written in DB.
820 $CFG->enablecompletion = true;
822 // Create a course with mixed auto completion data.
823 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
824 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
825 $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL);
826 $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
827 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
828 $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionauto);
829 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionmanual);
831 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionnone);
832 $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionnone);
833 $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionnone);
835 // Create data in another course to make sure it's not considered.
836 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
837 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionauto);
838 $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id), $completionmanual);
839 $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id), $completionnone);
841 $c = new completion_info($course);
842 $activities = $c->get_activities();
843 $this->assertCount(3, $activities);
844 $this->assertTrue(isset($activities[$forum->cmid]));
845 $this->assertSame($forum->name, $activities[$forum->cmid]->name);
846 $this->assertTrue(isset($activities[$page->cmid]));
847 $this->assertSame($page->name, $activities[$page->cmid]->name);
848 $this->assertTrue(isset($activities[$data->cmid]));
849 $this->assertSame($data->name, $activities[$data->cmid]->name);
851 $this->assertFalse(isset($activities[$forum2->cmid]));
852 $this->assertFalse(isset($activities[$page2->cmid]));
853 $this->assertFalse(isset($activities[$data2->cmid]));
856 public function test_has_activities() {
857 global $CFG;
858 $this->resetAfterTest();
860 // Enable completion before creating modules, otherwise the completion data is not written in DB.
861 $CFG->enablecompletion = true;
863 // Create a course with mixed auto completion data.
864 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
865 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
866 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
867 $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
868 $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
869 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionnone);
871 $c1 = new completion_info($course);
872 $c2 = new completion_info($course2);
874 $this->assertTrue($c1->has_activities());
875 $this->assertFalse($c2->has_activities());
879 * Test that data is cleaned up when we delete courses that are set as completion criteria for other courses
881 * @return void
883 public function test_course_delete_prerequisite() {
884 global $DB;
886 $this->setup_data();
888 $courseprerequisite = $this->getDataGenerator()->create_course(['enablecompletion' => true]);
890 $criteriadata = (object) [
891 'id' => $this->course->id,
892 'criteria_course' => [$courseprerequisite->id],
895 /** @var completion_criteria_course $criteria */
896 $criteria = completion_criteria::factory(['criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE]);
897 $criteria->update_config($criteriadata);
899 // Sanity test.
900 $this->assertTrue($DB->record_exists('course_completion_criteria', [
901 'course' => $this->course->id,
902 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
903 'courseinstance' => $courseprerequisite->id,
904 ]));
906 // Deleting the prerequisite course should remove the completion criteria.
907 delete_course($courseprerequisite, false);
909 $this->assertFalse($DB->record_exists('course_completion_criteria', [
910 'course' => $this->course->id,
911 'criteriatype' => COMPLETION_CRITERIA_TYPE_COURSE,
912 'courseinstance' => $courseprerequisite->id,
913 ]));
917 * Test course module completion update event.
919 public function test_course_module_completion_updated_event() {
920 global $USER, $CFG;
922 $this->setup_data();
924 $this->setAdminUser();
926 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
927 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
929 $c = new completion_info($this->course);
930 $activities = $c->get_activities();
931 $this->assertEquals(1, count($activities));
932 $this->assertTrue(isset($activities[$forum->cmid]));
933 $this->assertEquals($activities[$forum->cmid]->name, $forum->name);
935 $current = $c->get_data($activities[$forum->cmid], false, $this->user->id);
936 $current->completionstate = COMPLETION_COMPLETE;
937 $current->timemodified = time();
938 $sink = $this->redirectEvents();
939 $c->internal_set_data($activities[$forum->cmid], $current);
940 $events = $sink->get_events();
941 $event = reset($events);
942 $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
943 $this->assertEquals($forum->cmid, $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
944 $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
945 $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
946 $this->assertEquals($USER->id, $event->userid);
947 $this->assertEquals($this->user->id, $event->relateduserid);
948 $this->assertInstanceOf('moodle_url', $event->get_url());
949 $this->assertEventLegacyData($current, $event);
953 * Test course completed event.
955 public function test_course_completed_event() {
956 global $USER;
958 $this->setup_data();
959 $this->setAdminUser();
961 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
962 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
964 // Mark course as complete and get triggered event.
965 $sink = $this->redirectEvents();
966 $ccompletion->mark_complete();
967 $events = $sink->get_events();
968 $event = reset($events);
970 $this->assertInstanceOf('\core\event\course_completed', $event);
971 $this->assertEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course);
972 $this->assertEquals($this->course->id, $event->courseid);
973 $this->assertEquals($USER->id, $event->userid);
974 $this->assertEquals($this->user->id, $event->relateduserid);
975 $this->assertEquals(context_course::instance($this->course->id), $event->get_context());
976 $this->assertInstanceOf('moodle_url', $event->get_url());
977 $data = $ccompletion->get_record_data();
978 $this->assertEventLegacyData($data, $event);
982 * Test course completed event.
984 public function test_course_completion_updated_event() {
985 $this->setup_data();
986 $coursecontext = context_course::instance($this->course->id);
987 $coursecompletionevent = \core\event\course_completion_updated::create(
988 array(
989 'courseid' => $this->course->id,
990 'context' => $coursecontext
994 // Mark course as complete and get triggered event.
995 $sink = $this->redirectEvents();
996 $coursecompletionevent->trigger();
997 $events = $sink->get_events();
998 $event = array_pop($events);
999 $sink->close();
1001 $this->assertInstanceOf('\core\event\course_completion_updated', $event);
1002 $this->assertEquals($this->course->id, $event->courseid);
1003 $this->assertEquals($coursecontext, $event->get_context());
1004 $this->assertInstanceOf('moodle_url', $event->get_url());
1005 $expectedlegacylog = array($this->course->id, 'course', 'completion updated', 'completion.php?id='.$this->course->id);
1006 $this->assertEventLegacyLogData($expectedlegacylog, $event);
1009 public function test_completion_can_view_data() {
1010 $this->setup_data();
1012 $student = $this->getDataGenerator()->create_user();
1013 $this->getDataGenerator()->enrol_user($student->id, $this->course->id);
1015 $this->setUser($student);
1016 $this->assertTrue(completion_can_view_data($student->id, $this->course->id));
1017 $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id));
1021 class core_completionlib_fake_recordset implements Iterator {
1022 protected $closed;
1023 protected $values, $index;
1025 public function __construct($values) {
1026 $this->values = $values;
1027 $this->index = 0;
1030 public function current() {
1031 return $this->values[$this->index];
1034 public function key() {
1035 return $this->values[$this->index];
1038 public function next() {
1039 $this->index++;
1042 public function rewind() {
1043 $this->index = 0;
1046 public function valid() {
1047 return count($this->values) > $this->index;
1050 public function close() {
1051 $this->closed = true;
1054 public function was_closed() {
1055 return $this->closed;