MDL-52471 tests: simplify setup code
[moodle.git] / lib / tests / completionlib_test.php
CommitLineData
4059b645
PS
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
82ff8e29 18 * Completion tests.
4059b645
PS
19 *
20 * @package core_completion
21 * @category phpunit
22 * @copyright 2008 Sam Marshall
82ff8e29 23 * @copyright 2013 Frédéric Massart
4059b645
PS
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 */
26
27defined('MOODLE_INTERNAL') || die();
28
29global $CFG;
30require_once($CFG->libdir.'/completionlib.php');
31
82ff8e29 32class core_completionlib_testcase extends advanced_testcase {
cdc54199
RT
33 protected $course;
34 protected $user;
35 protected $module1;
36 protected $module2;
37
82ff8e29
PS
38 protected function mock_setup() {
39 global $DB, $CFG, $USER;
4059b645 40
82ff8e29 41 $this->resetAfterTest();
4059b645 42
82ff8e29 43 $DB = $this->getMock(get_class($DB));
4059b645 44 $CFG->enablecompletion = COMPLETION_ENABLED;
4059b645
PS
45 $USER = (object)array('id' =>314159);
46 }
47
cdc54199
RT
48 /**
49 * Create course with user and activities.
50 */
51 protected function setup_data() {
52 global $DB, $CFG;
53
54 $this->resetAfterTest();
55
7fbe33fc
MG
56 // Enable completion before creating modules, otherwise the completion data is not written in DB.
57 $CFG->enablecompletion = true;
58
cdc54199 59 // Create a course with activities.
7fbe33fc 60 $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
cdc54199 61 $this->user = $this->getDataGenerator()->create_user();
ef5df7b7 62 $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id);
cdc54199
RT
63
64 $this->module1 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id));
65 $this->module2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id));
66 }
67
2a67e105
PS
68 /**
69 * Asserts that two variables are equal.
70 *
71 * @param mixed $expected
72 * @param mixed $actual
73 * @param string $message
74 * @param float $delta
75 * @param integer $maxDepth
76 * @param boolean $canonicalize
77 * @param boolean $ignoreCase
78 */
79 public static function assertEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) {
80 // Nasty cheating hack: prevent random failures on timemodified field.
81 if (is_object($expected) and is_object($actual)) {
82 if (property_exists($expected, 'timemodified') and property_exists($actual, 'timemodified')) {
83 if ($expected->timemodified + 1 == $actual->timemodified) {
84 $expected = clone($expected);
85 $expected->timemodified = $actual->timemodified;
86 }
87 }
88 }
89 parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase);
90 }
91
82ff8e29 92 public function test_is_enabled() {
4059b645 93 global $CFG;
82ff8e29 94 $this->mock_setup();
4059b645 95
82ff8e29 96 // Config alone.
4059b645
PS
97 $CFG->enablecompletion = COMPLETION_DISABLED;
98 $this->assertEquals(COMPLETION_DISABLED, completion_info::is_enabled_for_site());
99 $CFG->enablecompletion = COMPLETION_ENABLED;
100 $this->assertEquals(COMPLETION_ENABLED, completion_info::is_enabled_for_site());
101
82ff8e29 102 // Course.
4059b645
PS
103 $course = (object)array('id' =>13);
104 $c = new completion_info($course);
105 $course->enablecompletion = COMPLETION_DISABLED;
106 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
107 $course->enablecompletion = COMPLETION_ENABLED;
108 $this->assertEquals(COMPLETION_ENABLED, $c->is_enabled());
109 $CFG->enablecompletion = COMPLETION_DISABLED;
110 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled());
111
82ff8e29 112 // Course and CM.
4059b645
PS
113 $cm = new stdClass();
114 $cm->completion = COMPLETION_TRACKING_MANUAL;
115 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
116 $CFG->enablecompletion = COMPLETION_ENABLED;
117 $course->enablecompletion = COMPLETION_DISABLED;
118 $this->assertEquals(COMPLETION_DISABLED, $c->is_enabled($cm));
119 $course->enablecompletion = COMPLETION_ENABLED;
120 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $c->is_enabled($cm));
121 $cm->completion = COMPLETION_TRACKING_NONE;
122 $this->assertEquals(COMPLETION_TRACKING_NONE, $c->is_enabled($cm));
123 $cm->completion = COMPLETION_TRACKING_AUTOMATIC;
124 $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $c->is_enabled($cm));
125 }
126
82ff8e29
PS
127 public function test_update_state() {
128 $this->mock_setup();
4059b645 129
82ff8e29 130 $c = $this->getMock('completion_info', array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data'), array((object)array('id'=>42)));
4059b645
PS
131 $cm = (object)array('id'=>13, 'course'=>42);
132
82ff8e29 133 // Not enabled, should do nothing.
4059b645
PS
134 $c->expects($this->at(0))
135 ->method('is_enabled')
136 ->with($cm)
137 ->will($this->returnValue(false));
138 $c->update_state($cm);
139
82ff8e29 140 // Enabled, but current state is same as possible result, do nothing.
4059b645
PS
141 $current = (object)array('completionstate'=>COMPLETION_COMPLETE);
142 $c->expects($this->at(0))
143 ->method('is_enabled')
144 ->with($cm)
145 ->will($this->returnValue(true));
146 $c->expects($this->at(1))
147 ->method('get_data')
148 ->with($cm, false, 0)
149 ->will($this->returnValue($current));
150 $c->update_state($cm, COMPLETION_COMPLETE);
151
152 // Enabled, but current state is a specific one and new state is just
82ff8e29 153 // complete, so do nothing.
4059b645
PS
154 $current->completionstate = COMPLETION_COMPLETE_PASS;
155 $c->expects($this->at(0))
156 ->method('is_enabled')
157 ->with($cm)
158 ->will($this->returnValue(true));
159 $c->expects($this->at(1))
160 ->method('get_data')
161 ->with($cm, false, 0)
162 ->will($this->returnValue($current));
163 $c->update_state($cm, COMPLETION_COMPLETE);
164
82ff8e29
PS
165 // Manual, change state (no change).
166 $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_MANUAL);
4059b645
PS
167 $current->completionstate=COMPLETION_COMPLETE;
168 $c->expects($this->at(0))
169 ->method('is_enabled')
170 ->with($cm)
171 ->will($this->returnValue(true));
172 $c->expects($this->at(1))
173 ->method('get_data')
174 ->with($cm, false, 0)
175 ->will($this->returnValue($current));
176 $c->update_state($cm, COMPLETION_COMPLETE);
177
82ff8e29 178 // Manual, change state (change).
4059b645
PS
179 $c->expects($this->at(0))
180 ->method('is_enabled')
181 ->with($cm)
182 ->will($this->returnValue(true));
183 $c->expects($this->at(1))
184 ->method('get_data')
185 ->with($cm, false, 0)
186 ->will($this->returnValue($current));
187 $changed = clone($current);
188 $changed->timemodified = time();
189 $changed->completionstate = COMPLETION_INCOMPLETE;
97b1a482
AN
190 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
191 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
4059b645
PS
192 $c->expects($this->at(2))
193 ->method('internal_set_data')
97b1a482 194 ->with($cm, $comparewith);
4059b645
PS
195 $c->update_state($cm, COMPLETION_INCOMPLETE);
196
82ff8e29
PS
197 // Auto, change state.
198 $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
4059b645
PS
199 $current = (object)array('completionstate'=>COMPLETION_COMPLETE);
200 $c->expects($this->at(0))
201 ->method('is_enabled')
202 ->with($cm)
203 ->will($this->returnValue(true));
204 $c->expects($this->at(1))
205 ->method('get_data')
206 ->with($cm, false, 0)
207 ->will($this->returnValue($current));
208 $c->expects($this->at(2))
209 ->method('internal_get_state')
210 ->will($this->returnValue(COMPLETION_COMPLETE_PASS));
211 $changed = clone($current);
212 $changed->timemodified = time();
213 $changed->completionstate = COMPLETION_COMPLETE_PASS;
97b1a482
AN
214 $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
215 $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
4059b645
PS
216 $c->expects($this->at(3))
217 ->method('internal_set_data')
97b1a482 218 ->with($cm, $comparewith);
4059b645
PS
219 $c->update_state($cm, COMPLETION_COMPLETE_PASS);
220 }
221
82ff8e29 222 public function test_internal_get_state() {
4059b645 223 global $DB;
82ff8e29 224 $this->mock_setup();
4059b645
PS
225
226 $c = $this->getMock('completion_info', array('internal_get_grade_state'), array((object)array('id'=>42)));
227 $cm = (object)array('id'=>13, 'course'=>42, 'completiongradeitemnumber'=>null);
228
82ff8e29 229 // If view is required, but they haven't viewed it yet.
4059b645
PS
230 $cm->completionview = COMPLETION_VIEW_REQUIRED;
231 $current = (object)array('viewed'=>COMPLETION_NOT_VIEWED);
232 $this->assertEquals(COMPLETION_INCOMPLETE, $c->internal_get_state($cm, 123, $current));
233
82ff8e29 234 // OK set view not required.
4059b645
PS
235 $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
236
82ff8e29 237 // Test not getting module name.
4059b645
PS
238 $cm->modname='label';
239 $this->assertEquals(COMPLETION_COMPLETE, $c->internal_get_state($cm, 123, $current));
240
82ff8e29 241 // Test getting module name.
4059b645
PS
242 $cm->module = 13;
243 unset($cm->modname);
244 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
245 $DB->expects($this->once())
246 ->method('get_field')
247 ->with('modules', 'name', array('id'=>13))
248 ->will($this->returnValue('lable'));
249 $this->assertEquals(COMPLETION_COMPLETE, $c->internal_get_state($cm, 123, $current));
250
82ff8e29
PS
251 // Note: This function is not fully tested (including kind of the main part) because:
252 // * the grade_item/grade_grade calls are static and can't be mocked,
253 // * the plugin_supports call is static and can't be mocked.
4059b645
PS
254 }
255
82ff8e29
PS
256 public function test_set_module_viewed() {
257 $this->mock_setup();
4059b645
PS
258
259 $c = $this->getMock('completion_info',
260 array('delete_all_state', 'get_tracked_users', 'update_state', 'internal_get_grade_state', 'is_enabled', 'get_data', 'internal_get_state', 'internal_set_data'),
261 array((object)array('id'=>42)));
262 $cm = (object)array('id'=>13, 'course'=>42);
263
82ff8e29 264 // Not tracking completion, should do nothing.
4059b645
PS
265 $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
266 $c->set_module_viewed($cm);
267
82ff8e29 268 // Tracking completion but completion is disabled, should do nothing.
4059b645
PS
269 $cm->completionview = COMPLETION_VIEW_REQUIRED;
270 $c->expects($this->at(0))
271 ->method('is_enabled')
272 ->with($cm)
273 ->will($this->returnValue(false));
274 $c->set_module_viewed($cm);
275
276 // Now it's enabled, we expect it to get data. If data already has
82ff8e29 277 // viewed, still do nothing.
4059b645
PS
278 $c->expects($this->at(0))
279 ->method('is_enabled')
280 ->with($cm)
281 ->will($this->returnValue(true));
282 $c->expects($this->at(1))
283 ->method('get_data')
284 ->with($cm, 0)
285 ->will($this->returnValue((object)array('viewed'=>COMPLETION_VIEWED)));
286 $c->set_module_viewed($cm);
287
288 // OK finally one that hasn't been viewed, now it should set it viewed
82ff8e29 289 // and update state.
4059b645
PS
290 $c->expects($this->at(0))
291 ->method('is_enabled')
292 ->with($cm)
293 ->will($this->returnValue(true));
294 $c->expects($this->at(1))
295 ->method('get_data')
1caeb4b4 296 ->with($cm, false, 1337)
4059b645
PS
297 ->will($this->returnValue((object)array('viewed'=>COMPLETION_NOT_VIEWED)));
298 $c->expects($this->at(2))
299 ->method('internal_set_data')
300 ->with($cm, (object)array('viewed'=>COMPLETION_VIEWED));
301 $c->expects($this->at(3))
302 ->method('update_state')
303 ->with($cm, COMPLETION_COMPLETE, 1337);
304 $c->set_module_viewed($cm, 1337);
305 }
306
82ff8e29 307 public function test_count_user_data() {
4059b645 308 global $DB;
82ff8e29 309 $this->mock_setup();
4059b645
PS
310
311 $course = (object)array('id'=>13);
312 $cm = (object)array('id'=>42);
313
314 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
315 $DB->expects($this->at(0))
316 ->method('get_field_sql')
317 ->will($this->returnValue(666));
318
4059b645
PS
319 $c = new completion_info($course);
320 $this->assertEquals(666, $c->count_user_data($cm));
321 }
322
82ff8e29 323 public function test_delete_all_state() {
3871db0a 324 global $DB;
82ff8e29 325 $this->mock_setup();
4059b645
PS
326
327 $course = (object)array('id'=>13);
82ff8e29 328 $cm = (object)array('id'=>42, 'course'=>13);
4059b645
PS
329 $c = new completion_info($course);
330
82ff8e29 331 // Check it works ok without data in session.
4059b645
PS
332 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
333 $DB->expects($this->at(0))
334 ->method('delete_records')
335 ->with('course_modules_completion', array('coursemoduleid'=>42))
336 ->will($this->returnValue(true));
337 $c->delete_all_state($cm);
4059b645
PS
338 }
339
82ff8e29 340 public function test_reset_all_state() {
4059b645 341 global $DB;
82ff8e29 342 $this->mock_setup();
4059b645
PS
343
344 $c = $this->getMock('completion_info',
82ff8e29 345 array('delete_all_state', 'get_tracked_users', 'update_state', 'internal_get_grade_state', 'is_enabled', 'get_data', 'internal_get_state', 'internal_set_data'),
4059b645
PS
346 array((object)array('id'=>42)));
347
348 $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
349
350 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
351 $DB->expects($this->at(0))
352 ->method('get_recordset')
353 ->will($this->returnValue(
82ff8e29 354 new core_completionlib_fake_recordset(array((object)array('id'=>1, 'userid'=>100), (object)array('id'=>2, 'userid'=>101)))));
4059b645
PS
355
356 $c->expects($this->at(0))
357 ->method('delete_all_state')
358 ->with($cm);
359
360 $c->expects($this->at(1))
361 ->method('get_tracked_users')
362 ->will($this->returnValue(array(
82ff8e29
PS
363 (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'),
364 (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy'))));
4059b645
PS
365
366 $c->expects($this->at(2))
367 ->method('update_state')
82ff8e29 368 ->with($cm, COMPLETION_UNKNOWN, 100);
4059b645
PS
369 $c->expects($this->at(3))
370 ->method('update_state')
82ff8e29 371 ->with($cm, COMPLETION_UNKNOWN, 101);
4059b645
PS
372 $c->expects($this->at(4))
373 ->method('update_state')
82ff8e29 374 ->with($cm, COMPLETION_UNKNOWN, 201);
4059b645
PS
375
376 $c->reset_all_state($cm);
377 }
378
82ff8e29 379 public function test_get_data() {
3871db0a 380 global $DB;
82ff8e29 381 $this->mock_setup();
4059b645 382
3871db0a
MW
383 $cache = cache::make('core', 'completion');
384
0cc9d709 385 $c = new completion_info((object)array('id'=>42, 'cacherev'=>1));
4059b645
PS
386 $cm = (object)array('id'=>13, 'course'=>42);
387
82ff8e29 388 // 1. Not current user, record exists.
4059b645
PS
389 $sillyrecord = (object)array('frog'=>'kermit');
390
391 /** @var $DB PHPUnit_Framework_MockObject_MockObject */
392 $DB->expects($this->at(0))
393 ->method('get_record')
82ff8e29 394 ->with('course_modules_completion', array('coursemoduleid'=>13, 'userid'=>123))
4059b645 395 ->will($this->returnValue($sillyrecord));
82ff8e29 396 $result = $c->get_data($cm, false, 123);
4059b645 397 $this->assertEquals($sillyrecord, $result);
0cc9d709 398 $this->assertEquals(false, $cache->get('123_42')); // Not current user is not cached.
4059b645 399
3871db0a
MW
400 // 2. Not current user, default record, whole course.
401 $cache->purge();
4059b645 402 $DB->expects($this->at(0))
3871db0a
MW
403 ->method('get_records_sql')
404 ->will($this->returnValue(array()));
405 $modinfo = new stdClass();
406 $modinfo->cms = array((object)array('id'=>13));
407 $result=$c->get_data($cm, true, 123, $modinfo);
4059b645 408 $this->assertEquals((object)array(
82ff8e29
PS
409 'id'=>'0', 'coursemoduleid'=>13, 'userid'=>123, 'completionstate'=>0,
410 'viewed'=>0, 'timemodified'=>0), $result);
0cc9d709 411 $this->assertEquals(false, $cache->get('123_42')); // Not current user is not cached.
4059b645 412
82ff8e29 413 // 3. Current user, single record, not from cache.
4059b645
PS
414 $DB->expects($this->at(0))
415 ->method('get_record')
82ff8e29 416 ->with('course_modules_completion', array('coursemoduleid'=>13, 'userid'=>314159))
4059b645
PS
417 ->will($this->returnValue($sillyrecord));
418 $result = $c->get_data($cm);
419 $this->assertEquals($sillyrecord, $result);
0cc9d709
MG
420 $cachevalue = $cache->get('314159_42');
421 $this->assertEquals($sillyrecord, $cachevalue[13]);
4059b645 422
82ff8e29 423 // 4. Current user, 'whole course', but from cache.
4059b645
PS
424 $result = $c->get_data($cm, true);
425 $this->assertEquals($sillyrecord, $result);
426
3871db0a
MW
427 // 5. Current user, 'whole course' and record not in cache.
428 $cache->purge();
4059b645 429
82ff8e29 430 // Scenario: Completion data exists for one CMid.
4059b645
PS
431 $basicrecord = (object)array('coursemoduleid'=>13);
432 $DB->expects($this->at(0))
433 ->method('get_records_sql')
434 ->will($this->returnValue(array('1'=>$basicrecord)));
435
82ff8e29 436 // There are two CMids in total, the one we had data for and another one.
4059b645
PS
437 $modinfo = new stdClass();
438 $modinfo->cms = array((object)array('id'=>13), (object)array('id'=>14));
439 $result = $c->get_data($cm, true, 0, $modinfo);
440
82ff8e29 441 // Check result.
4059b645
PS
442 $this->assertEquals($basicrecord, $result);
443
82ff8e29 444 // Check the cache contents.
0cc9d709
MG
445 $cachevalue = $cache->get('314159_42');
446 $this->assertEquals($basicrecord, $cachevalue[13]);
447 $this->assertEquals((object)array('id'=>'0', 'coursemoduleid'=>14,
3871db0a 448 'userid'=>314159, 'completionstate'=>0, 'viewed'=>0, 'timemodified'=>0),
0cc9d709 449 $cachevalue[14]);
4059b645
PS
450 }
451
82ff8e29 452 public function test_internal_set_data() {
3871db0a 453 global $DB;
cdc54199 454 $this->setup_data();
4059b645 455
cdc54199
RT
456 $this->setUser($this->user);
457 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
458 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
459 $cm = get_coursemodule_from_instance('forum', $forum->id);
460 $c = new completion_info($this->course);
4059b645 461
82ff8e29 462 // 1) Test with new data.
cdc54199
RT
463 $data = new stdClass();
464 $data->id = 0;
465 $data->userid = $this->user->id;
466 $data->coursemoduleid = $cm->id;
467 $data->completionstate = COMPLETION_COMPLETE;
468 $data->timemodified = time();
cd5be9a5 469 $data->viewed = COMPLETION_NOT_VIEWED;
4059b645
PS
470
471 $c->internal_set_data($cm, $data);
cdc54199
RT
472 $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id));
473 $this->assertEquals($d1, $data->id);
3871db0a 474 $cache = cache::make('core', 'completion');
0cc9d709
MG
475 // Cache was not set for another user.
476 $this->assertEquals(array('cacherev' => $this->course->cacherev, $cm->id => $data),
477 $cache->get($data->userid . '_' . $cm->course));
4059b645 478
3871db0a 479 // 2) Test with existing data and for different user.
cdc54199
RT
480 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
481 $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
482 $newuser = $this->getDataGenerator()->create_user();
483
484 $d2 = new stdClass();
485 $d2->id = 7;
486 $d2->userid = $newuser->id;
487 $d2->coursemoduleid = $cm2->id;
488 $d2->completionstate = COMPLETION_COMPLETE;
489 $d2->timemodified = time();
cd5be9a5 490 $d2->viewed = COMPLETION_NOT_VIEWED;
cdc54199 491 $c->internal_set_data($cm2, $d2);
0cc9d709
MG
492 // Cache for current user returns the data.
493 $cachevalue = $cache->get($data->userid . '_' . $cm->course);
494 $this->assertEquals($data, $cachevalue[$cm->id]);
495 // Cache for another user is not filled.
496 $this->assertEquals(false, $cache->get($d2->userid . '_' . $cm2->course));
4059b645
PS
497
498 // 3) Test where it THINKS the data is new (from cache) but actually
82ff8e29
PS
499 // in the database it has been set since.
500 // 1) Test with new data.
cdc54199
RT
501 $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
502 $cm3 = get_coursemodule_from_instance('forum', $forum3->id);
503 $newuser2 = $this->getDataGenerator()->create_user();
504 $d3 = new stdClass();
505 $d3->id = 13;
506 $d3->userid = $newuser2->id;
507 $d3->coursemoduleid = $cm3->id;
508 $d3->completionstate = COMPLETION_COMPLETE;
509 $d3->timemodified = time();
cd5be9a5 510 $d3->viewed = COMPLETION_NOT_VIEWED;
cdc54199 511 $DB->insert_record('course_modules_completion', $d3);
4059b645
PS
512 $c->internal_set_data($cm, $data);
513 }
514
82ff8e29 515 public function test_get_progress_all() {
4059b645 516 global $DB;
82ff8e29 517 $this->mock_setup();
4059b645
PS
518
519 $c = $this->getMock('completion_info',
520 array('delete_all_state', 'get_tracked_users', 'update_state', 'internal_get_grade_state', 'is_enabled', 'get_data', 'internal_get_state', 'internal_set_data'),
521 array((object)array('id'=>42)));
522
82ff8e29 523 // 1) Basic usage.
4059b645
PS
524 $c->expects($this->at(0))
525 ->method('get_tracked_users')
526 ->with(false, array(), 0, '', '', '', null)
527 ->will($this->returnValue(array(
528 (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'),
529 (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy'))));
530 $DB->expects($this->at(0))
531 ->method('get_in_or_equal')
532 ->with(array(100, 201))
533 ->will($this->returnValue(array(' IN (100, 201)', array())));
534 $progress1 = (object)array('userid'=>100, 'coursemoduleid'=>13);
535 $progress2 = (object)array('userid'=>201, 'coursemoduleid'=>14);
536 $DB->expects($this->at(1))
537 ->method('get_recordset_sql')
82ff8e29 538 ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
4059b645
PS
539
540 $this->assertEquals(array(
541 100 => (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh',
542 'progress'=>array(13=>$progress1)),
543 201 => (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy',
544 'progress'=>array(14=>$progress2)),
545 ), $c->get_progress_all(false));
546
82ff8e29 547 // 2) With more than 1, 000 results.
4059b645
PS
548 $tracked = array();
549 $ids = array();
550 $progress = array();
82ff8e29 551 for ($i = 100; $i<2000; $i++) {
4059b645
PS
552 $tracked[] = (object)array('id'=>$i, 'firstname'=>'frog', 'lastname'=>$i);
553 $ids[] = $i;
554 $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>13);
555 $progress[] = (object)array('userid'=>$i, 'coursemoduleid'=>14);
556 }
557 $c->expects($this->at(0))
558 ->method('get_tracked_users')
559 ->with(true, 3, 0, '', '', '', null)
560 ->will($this->returnValue($tracked));
561 $DB->expects($this->at(0))
562 ->method('get_in_or_equal')
563 ->with(array_slice($ids, 0, 1000))
564 ->will($this->returnValue(array(' IN whatever', array())));
565 $DB->expects($this->at(1))
566 ->method('get_recordset_sql')
82ff8e29 567 ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 0, 1000))));
4059b645
PS
568
569 $DB->expects($this->at(2))
570 ->method('get_in_or_equal')
571 ->with(array_slice($ids, 1000))
572 ->will($this->returnValue(array(' IN whatever2', array())));
573 $DB->expects($this->at(3))
574 ->method('get_recordset_sql')
82ff8e29 575 ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 1000))));
4059b645
PS
576
577 $result = $c->get_progress_all(true, 3);
578 $resultok = true;
579 $resultok = $resultok && ($ids == array_keys($result));
580
82ff8e29 581 foreach ($result as $userid => $data) {
4059b645
PS
582 $resultok = $resultok && $data->firstname == 'frog';
583 $resultok = $resultok && $data->lastname == $userid;
584 $resultok = $resultok && $data->id == $userid;
585 $cms = $data->progress;
586 $resultok = $resultok && (array(13, 14) == array_keys($cms));
587 $resultok = $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>13) == $cms[13]);
588 $resultok = $resultok && ((object)array('userid'=>$userid, 'coursemoduleid'=>14) == $cms[14]);
589 }
590 $this->assertTrue($resultok);
591 }
592
82ff8e29
PS
593 public function test_inform_grade_changed() {
594 $this->mock_setup();
595
4059b645
PS
596 $c = $this->getMock('completion_info',
597 array('delete_all_state', 'get_tracked_users', 'update_state', 'internal_get_grade_state', 'is_enabled', 'get_data', 'internal_get_state', 'internal_set_data'),
598 array((object)array('id'=>42)));
599
600 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>null);
601 $item = (object)array('itemnumber'=>3, 'gradepass'=>1, 'hidden'=>0);
602 $grade = (object)array('userid'=>31337, 'finalgrade'=>0, 'rawgrade'=>0);
603
82ff8e29 604 // Not enabled (should do nothing).
4059b645
PS
605 $c->expects($this->at(0))
606 ->method('is_enabled')
607 ->with($cm)
608 ->will($this->returnValue(false));
609 $c->inform_grade_changed($cm, $item, $grade, false);
610
82ff8e29 611 // Enabled but still no grade completion required, should still do nothing.
4059b645
PS
612 $c->expects($this->at(0))
613 ->method('is_enabled')
614 ->with($cm)
615 ->will($this->returnValue(true));
616 $c->inform_grade_changed($cm, $item, $grade, false);
617
82ff8e29 618 // Enabled and completion required but item number is wrong, does nothing.
4059b645
PS
619 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>7);
620 $c->expects($this->at(0))
621 ->method('is_enabled')
622 ->with($cm)
623 ->will($this->returnValue(true));
624 $c->inform_grade_changed($cm, $item, $grade, false);
625
626 // Enabled and completion required and item number right. It is supposed
627 // to call update_state with the new potential state being obtained from
628 // internal_get_grade_state.
629 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3);
630 $grade = (object)array('userid'=>31337, 'finalgrade'=>1, 'rawgrade'=>0);
631 $c->expects($this->at(0))
632 ->method('is_enabled')
633 ->with($cm)
634 ->will($this->returnValue(true));
635 $c->expects($this->at(1))
636 ->method('update_state')
637 ->with($cm, COMPLETION_COMPLETE_PASS, 31337)
638 ->will($this->returnValue(true));
639 $c->inform_grade_changed($cm, $item, $grade, false);
640
641 // Same as above but marked deleted. It is supposed to call update_state
82ff8e29 642 // with new potential state being COMPLETION_INCOMPLETE.
4059b645
PS
643 $cm = (object)array('course'=>42, 'id'=>13, 'completion'=>0, 'completiongradeitemnumber'=>3);
644 $grade = (object)array('userid'=>31337, 'finalgrade'=>1, 'rawgrade'=>0);
645 $c->expects($this->at(0))
646 ->method('is_enabled')
647 ->with($cm)
648 ->will($this->returnValue(true));
649 $c->expects($this->at(1))
650 ->method('update_state')
651 ->with($cm, COMPLETION_INCOMPLETE, 31337)
652 ->will($this->returnValue(true));
653 $c->inform_grade_changed($cm, $item, $grade, true);
654 }
655
82ff8e29
PS
656 public function test_internal_get_grade_state() {
657 $this->mock_setup();
658
4059b645
PS
659 $item = new stdClass;
660 $grade = new stdClass;
661
662 $item->gradepass = 4;
663 $item->hidden = 0;
664 $grade->rawgrade = 4.0;
665 $grade->finalgrade = null;
666
82ff8e29 667 // Grade has pass mark and is not hidden, user passes.
4059b645
PS
668 $this->assertEquals(
669 COMPLETION_COMPLETE_PASS,
670 completion_info::internal_get_grade_state($item, $grade));
671
82ff8e29 672 // Same but user fails.
4059b645
PS
673 $grade->rawgrade = 3.9;
674 $this->assertEquals(
675 COMPLETION_COMPLETE_FAIL,
676 completion_info::internal_get_grade_state($item, $grade));
677
82ff8e29 678 // User fails on raw grade but passes on final.
4059b645
PS
679 $grade->finalgrade = 4.0;
680 $this->assertEquals(
681 COMPLETION_COMPLETE_PASS,
682 completion_info::internal_get_grade_state($item, $grade));
683
82ff8e29 684 // Item is hidden.
4059b645
PS
685 $item->hidden = 1;
686 $this->assertEquals(
687 COMPLETION_COMPLETE,
688 completion_info::internal_get_grade_state($item, $grade));
689
82ff8e29 690 // Item isn't hidden but has no pass mark.
4059b645
PS
691 $item->hidden = 0;
692 $item->gradepass = 0;
693 $this->assertEquals(
694 COMPLETION_COMPLETE,
695 completion_info::internal_get_grade_state($item, $grade));
696 }
82ff8e29
PS
697
698 public function test_get_activities() {
7fbe33fc 699 global $CFG;
82ff8e29
PS
700 $this->resetAfterTest();
701
7fbe33fc
MG
702 // Enable completion before creating modules, otherwise the completion data is not written in DB.
703 $CFG->enablecompletion = true;
704
82ff8e29 705 // Create a course with mixed auto completion data.
7fbe33fc 706 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
82ff8e29
PS
707 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
708 $completionmanual = array('completion' => COMPLETION_TRACKING_MANUAL);
709 $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
710 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
711 $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionauto);
712 $data = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionmanual);
713
714 $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionnone);
715 $page2 = $this->getDataGenerator()->create_module('page', array('course' => $course->id), $completionnone);
716 $data2 = $this->getDataGenerator()->create_module('data', array('course' => $course->id), $completionnone);
717
718 // Create data in another course to make sure it's not considered.
7fbe33fc 719 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
82ff8e29
PS
720 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionauto);
721 $c2page = $this->getDataGenerator()->create_module('page', array('course' => $course2->id), $completionmanual);
722 $c2data = $this->getDataGenerator()->create_module('data', array('course' => $course2->id), $completionnone);
723
724 $c = new completion_info($course);
725 $activities = $c->get_activities();
726 $this->assertCount(3, $activities);
727 $this->assertTrue(isset($activities[$forum->cmid]));
728 $this->assertSame($forum->name, $activities[$forum->cmid]->name);
729 $this->assertTrue(isset($activities[$page->cmid]));
730 $this->assertSame($page->name, $activities[$page->cmid]->name);
731 $this->assertTrue(isset($activities[$data->cmid]));
732 $this->assertSame($data->name, $activities[$data->cmid]->name);
733
734 $this->assertFalse(isset($activities[$forum2->cmid]));
735 $this->assertFalse(isset($activities[$page2->cmid]));
736 $this->assertFalse(isset($activities[$data2->cmid]));
737 }
738
739 public function test_has_activities() {
7fbe33fc 740 global $CFG;
82ff8e29
PS
741 $this->resetAfterTest();
742
7fbe33fc
MG
743 // Enable completion before creating modules, otherwise the completion data is not written in DB.
744 $CFG->enablecompletion = true;
745
82ff8e29 746 // Create a course with mixed auto completion data.
7fbe33fc
MG
747 $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
748 $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
82ff8e29
PS
749 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
750 $completionnone = array('completion' => COMPLETION_TRACKING_NONE);
751 $c1forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id), $completionauto);
752 $c2forum = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id), $completionnone);
753
754 $c1 = new completion_info($course);
755 $c2 = new completion_info($course2);
756
757 $this->assertTrue($c1->has_activities());
758 $this->assertFalse($c2->has_activities());
759 }
cdc54199
RT
760
761 /**
762 * Test course module completion update event.
763 */
764 public function test_course_module_completion_updated_event() {
7fbe33fc 765 global $USER, $CFG;
cdc54199
RT
766
767 $this->setup_data();
7fbe33fc 768
cdc54199
RT
769 $this->setAdminUser();
770
771 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
772 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
773
774 $c = new completion_info($this->course);
775 $activities = $c->get_activities();
776 $this->assertEquals(1, count($activities));
777 $this->assertTrue(isset($activities[$forum->cmid]));
778 $this->assertEquals($activities[$forum->cmid]->name, $forum->name);
779
780 $current = $c->get_data($activities[$forum->cmid], false, $this->user->id);
781 $current->completionstate = COMPLETION_COMPLETE;
782 $current->timemodified = time();
783 $sink = $this->redirectEvents();
784 $c->internal_set_data($activities[$forum->cmid], $current);
785 $events = $sink->get_events();
786 $event = reset($events);
787 $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
788 $this->assertEquals($forum->cmid, $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
789 $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
74b63eae 790 $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
cdc54199 791 $this->assertEquals($USER->id, $event->userid);
02a5a4b2 792 $this->assertEquals($this->user->id, $event->relateduserid);
fc4365d0 793 $this->assertInstanceOf('moodle_url', $event->get_url());
cdc54199
RT
794 $this->assertEventLegacyData($current, $event);
795 }
796
1cb1e5fe
RT
797 /**
798 * Test course completed event.
799 */
800 public function test_course_completed_event() {
801 global $USER;
802
803 $this->setup_data();
804 $this->setAdminUser();
805
806 $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
807 $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
808
809 // Mark course as complete and get triggered event.
810 $sink = $this->redirectEvents();
811 $ccompletion->mark_complete();
812 $events = $sink->get_events();
813 $event = reset($events);
814
815 $this->assertInstanceOf('\core\event\course_completed', $event);
816 $this->assertEquals($this->course->id, $event->get_record_snapshot('course_completions', $event->objectid)->course);
817 $this->assertEquals($this->course->id, $event->courseid);
818 $this->assertEquals($USER->id, $event->userid);
02a5a4b2 819 $this->assertEquals($this->user->id, $event->relateduserid);
1cb1e5fe 820 $this->assertEquals(context_course::instance($this->course->id), $event->get_context());
fc4365d0 821 $this->assertInstanceOf('moodle_url', $event->get_url());
1cb1e5fe
RT
822 $data = $ccompletion->get_record_data();
823 $this->assertEventLegacyData($data, $event);
824 }
4059b645 825
135dde74
RT
826 /**
827 * Test course completed event.
828 */
829 public function test_course_completion_updated_event() {
830 $this->setup_data();
831 $coursecontext = context_course::instance($this->course->id);
832 $coursecompletionevent = \core\event\course_completion_updated::create(
833 array(
834 'courseid' => $this->course->id,
835 'context' => $coursecontext
836 )
837 );
838
839 // Mark course as complete and get triggered event.
840 $sink = $this->redirectEvents();
841 $coursecompletionevent->trigger();
842 $events = $sink->get_events();
843 $event = array_pop($events);
844 $sink->close();
845
846 $this->assertInstanceOf('\core\event\course_completion_updated', $event);
847 $this->assertEquals($this->course->id, $event->courseid);
848 $this->assertEquals($coursecontext, $event->get_context());
fc4365d0 849 $this->assertInstanceOf('moodle_url', $event->get_url());
135dde74
RT
850 $expectedlegacylog = array($this->course->id, 'course', 'completion updated', 'completion.php?id='.$this->course->id);
851 $this->assertEventLegacyLogData($expectedlegacylog, $event);
852 }
194a02e4
DP
853
854 public function test_completion_can_view_data() {
855 $this->setup_data();
856
857 $student = $this->getDataGenerator()->create_user();
858 $this->getDataGenerator()->enrol_user($student->id, $this->course->id);
859
860 $this->setUser($student);
861 $this->assertTrue(completion_can_view_data($student->id, $this->course->id));
862 $this->assertFalse(completion_can_view_data($this->user->id, $this->course->id));
863 }
135dde74 864}
4059b645 865
82ff8e29
PS
866class core_completionlib_fake_recordset implements Iterator {
867 protected $closed;
868 protected $values, $index;
4059b645 869
82ff8e29 870 public function __construct($values) {
4059b645
PS
871 $this->values = $values;
872 $this->index = 0;
873 }
874
82ff8e29 875 public function current() {
4059b645
PS
876 return $this->values[$this->index];
877 }
878
82ff8e29 879 public function key() {
4059b645
PS
880 return $this->values[$this->index];
881 }
882
82ff8e29 883 public function next() {
4059b645
PS
884 $this->index++;
885 }
886
82ff8e29 887 public function rewind() {
4059b645
PS
888 $this->index = 0;
889 }
890
82ff8e29 891 public function valid() {
4059b645
PS
892 return count($this->values) > $this->index;
893 }
894
82ff8e29 895 public function close() {
4059b645
PS
896 $this->closed = true;
897 }
898
82ff8e29 899 public function was_closed() {
4059b645
PS
900 return $this->closed;
901 }
902}