MDL-52471 tests: simplify setup code
[moodle.git] / lib / tests / completionlib_test.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Completion tests.
19  *
20  * @package    core_completion
21  * @category   phpunit
22  * @copyright  2008 Sam Marshall
23  * @copyright  2013 Frédéric Massart
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 defined('MOODLE_INTERNAL') || die();
29 global $CFG;
30 require_once($CFG->libdir.'/completionlib.php');
32 class core_completionlib_testcase extends advanced_testcase {
33     protected $course;
34     protected $user;
35     protected $module1;
36     protected $module2;
38     protected function mock_setup() {
39         global $DB, $CFG, $USER;
41         $this->resetAfterTest();
43         $DB = $this->getMock(get_class($DB));
44         $CFG->enablecompletion = COMPLETION_ENABLED;
45         $USER = (object)array('id' =>314159);
46     }
48     /**
49      * Create course with user and activities.
50      */
51     protected function setup_data() {
52         global $DB, $CFG;
54         $this->resetAfterTest();
56         // Enable completion before creating modules, otherwise the completion data is not written in DB.
57         $CFG->enablecompletion = true;
59         // Create a course with activities.
60         $this->course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
61         $this->user = $this->getDataGenerator()->create_user();
62         $this->getDataGenerator()->enrol_user($this->user->id, $this->course->id);
64         $this->module1 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id));
65         $this->module2 = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id));
66     }
68     /**
69      * Asserts that two variables are equal.
70      *
71      * @param  mixed   $expected
72      * @param  mixed   $actual
73      * @param  string  $message
74      * @param  float   $delta
75      * @param  integer $maxDepth
76      * @param  boolean $canonicalize
77      * @param  boolean $ignoreCase
78      */
79     public static function assertEquals($expected, $actual, $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     }
92     public function test_is_enabled() {
93         global $CFG;
94         $this->mock_setup();
96         // Config alone.
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());
102         // Course.
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());
112         // Course and CM.
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     }
127     public function test_update_state() {
128         $this->mock_setup();
130         $c = $this->getMock('completion_info', array('is_enabled', 'get_data', 'internal_get_state', 'internal_set_data'), array((object)array('id'=>42)));
131         $cm = (object)array('id'=>13, 'course'=>42);
133         // Not enabled, should do nothing.
134         $c->expects($this->at(0))
135             ->method('is_enabled')
136             ->with($cm)
137             ->will($this->returnValue(false));
138         $c->update_state($cm);
140         // Enabled, but current state is same as possible result, do nothing.
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);
152         // Enabled, but current state is a specific one and new state is just
153         // complete, so do nothing.
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);
165         // Manual, change state (no change).
166         $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_MANUAL);
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);
178         // Manual, change state (change).
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;
190         $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
191         $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
192         $c->expects($this->at(2))
193             ->method('internal_set_data')
194             ->with($cm, $comparewith);
195         $c->update_state($cm, COMPLETION_INCOMPLETE);
197         // Auto, change state.
198         $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
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;
214         $comparewith = new phpunit_constraint_object_is_equal_with_exceptions($changed);
215         $comparewith->add_exception('timemodified', 'assertGreaterThanOrEqual');
216         $c->expects($this->at(3))
217             ->method('internal_set_data')
218             ->with($cm, $comparewith);
219         $c->update_state($cm, COMPLETION_COMPLETE_PASS);
220     }
222     public function test_internal_get_state() {
223         global $DB;
224         $this->mock_setup();
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);
229         // If view is required, but they haven't viewed it yet.
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));
234         // OK set view not required.
235         $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
237         // Test not getting module name.
238         $cm->modname='label';
239         $this->assertEquals(COMPLETION_COMPLETE, $c->internal_get_state($cm, 123, $current));
241         // Test getting module name.
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));
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.
254     }
256     public function test_set_module_viewed() {
257         $this->mock_setup();
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);
264         // Not tracking completion, should do nothing.
265         $cm->completionview = COMPLETION_VIEW_NOT_REQUIRED;
266         $c->set_module_viewed($cm);
268         // Tracking completion but completion is disabled, should do nothing.
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);
276         // Now it's enabled, we expect it to get data. If data already has
277         // viewed, still do nothing.
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);
288         // OK finally one that hasn't been viewed, now it should set it viewed
289         // and update state.
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')
296             ->with($cm, false, 1337)
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     }
307     public function test_count_user_data() {
308         global $DB;
309         $this->mock_setup();
311         $course = (object)array('id'=>13);
312         $cm = (object)array('id'=>42);
314         /** @var $DB PHPUnit_Framework_MockObject_MockObject */
315         $DB->expects($this->at(0))
316             ->method('get_field_sql')
317             ->will($this->returnValue(666));
319         $c = new completion_info($course);
320         $this->assertEquals(666, $c->count_user_data($cm));
321     }
323     public function test_delete_all_state() {
324         global $DB;
325         $this->mock_setup();
327         $course = (object)array('id'=>13);
328         $cm = (object)array('id'=>42, 'course'=>13);
329         $c = new completion_info($course);
331         // Check it works ok without data in session.
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);
338     }
340     public function test_reset_all_state() {
341         global $DB;
342         $this->mock_setup();
344         $c = $this->getMock('completion_info',
345             array('delete_all_state', 'get_tracked_users', 'update_state', 'internal_get_grade_state', 'is_enabled', 'get_data', 'internal_get_state', 'internal_set_data'),
346             array((object)array('id'=>42)));
348         $cm = (object)array('id'=>13, 'course'=>42, 'completion'=>COMPLETION_TRACKING_AUTOMATIC);
350         /** @var $DB PHPUnit_Framework_MockObject_MockObject */
351         $DB->expects($this->at(0))
352             ->method('get_recordset')
353             ->will($this->returnValue(
354                 new core_completionlib_fake_recordset(array((object)array('id'=>1, 'userid'=>100), (object)array('id'=>2, 'userid'=>101)))));
356         $c->expects($this->at(0))
357             ->method('delete_all_state')
358             ->with($cm);
360         $c->expects($this->at(1))
361             ->method('get_tracked_users')
362             ->will($this->returnValue(array(
363             (object)array('id'=>100, 'firstname'=>'Woot', 'lastname'=>'Plugh'),
364             (object)array('id'=>201, 'firstname'=>'Vroom', 'lastname'=>'Xyzzy'))));
366         $c->expects($this->at(2))
367             ->method('update_state')
368             ->with($cm, COMPLETION_UNKNOWN, 100);
369         $c->expects($this->at(3))
370             ->method('update_state')
371             ->with($cm, COMPLETION_UNKNOWN, 101);
372         $c->expects($this->at(4))
373             ->method('update_state')
374             ->with($cm, COMPLETION_UNKNOWN, 201);
376         $c->reset_all_state($cm);
377     }
379     public function test_get_data() {
380         global $DB;
381         $this->mock_setup();
383         $cache = cache::make('core', 'completion');
385         $c = new completion_info((object)array('id'=>42, 'cacherev'=>1));
386         $cm = (object)array('id'=>13, 'course'=>42);
388         // 1. Not current user, record exists.
389         $sillyrecord = (object)array('frog'=>'kermit');
391         /** @var $DB PHPUnit_Framework_MockObject_MockObject */
392         $DB->expects($this->at(0))
393             ->method('get_record')
394             ->with('course_modules_completion', array('coursemoduleid'=>13, 'userid'=>123))
395             ->will($this->returnValue($sillyrecord));
396         $result = $c->get_data($cm, false, 123);
397         $this->assertEquals($sillyrecord, $result);
398         $this->assertEquals(false, $cache->get('123_42')); // Not current user is not cached.
400         // 2. Not current user, default record, whole course.
401         $cache->purge();
402         $DB->expects($this->at(0))
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);
408         $this->assertEquals((object)array(
409             'id'=>'0', 'coursemoduleid'=>13, 'userid'=>123, 'completionstate'=>0,
410             'viewed'=>0, 'timemodified'=>0), $result);
411         $this->assertEquals(false, $cache->get('123_42')); // Not current user is not cached.
413         // 3. Current user, single record, not from cache.
414         $DB->expects($this->at(0))
415             ->method('get_record')
416             ->with('course_modules_completion', array('coursemoduleid'=>13, 'userid'=>314159))
417             ->will($this->returnValue($sillyrecord));
418         $result = $c->get_data($cm);
419         $this->assertEquals($sillyrecord, $result);
420         $cachevalue = $cache->get('314159_42');
421         $this->assertEquals($sillyrecord, $cachevalue[13]);
423         // 4. Current user, 'whole course', but from cache.
424         $result = $c->get_data($cm, true);
425         $this->assertEquals($sillyrecord, $result);
427         // 5. Current user, 'whole course' and record not in cache.
428         $cache->purge();
430         // Scenario: Completion data exists for one CMid.
431         $basicrecord = (object)array('coursemoduleid'=>13);
432         $DB->expects($this->at(0))
433             ->method('get_records_sql')
434             ->will($this->returnValue(array('1'=>$basicrecord)));
436         // There are two CMids in total, the one we had data for and another one.
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);
441         // Check result.
442         $this->assertEquals($basicrecord, $result);
444         // Check the cache contents.
445         $cachevalue = $cache->get('314159_42');
446         $this->assertEquals($basicrecord, $cachevalue[13]);
447         $this->assertEquals((object)array('id'=>'0', 'coursemoduleid'=>14,
448             'userid'=>314159, 'completionstate'=>0, 'viewed'=>0, 'timemodified'=>0),
449             $cachevalue[14]);
450     }
452     public function test_internal_set_data() {
453         global $DB;
454         $this->setup_data();
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);
462         // 1) Test with new data.
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();
469         $data->viewed = COMPLETION_NOT_VIEWED;
471         $c->internal_set_data($cm, $data);
472         $d1 = $DB->get_field('course_modules_completion', 'id', array('coursemoduleid' => $cm->id));
473         $this->assertEquals($d1, $data->id);
474         $cache = cache::make('core', 'completion');
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));
479         // 2) Test with existing data and for different user.
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();
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();
490         $d2->viewed = COMPLETION_NOT_VIEWED;
491         $c->internal_set_data($cm2, $d2);
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));
498         // 3) Test where it THINKS the data is new (from cache) but actually
499         //    in the database it has been set since.
500         // 1) Test with new data.
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();
510         $d3->viewed = COMPLETION_NOT_VIEWED;
511         $DB->insert_record('course_modules_completion', $d3);
512         $c->internal_set_data($cm, $data);
513     }
515     public function test_get_progress_all() {
516         global $DB;
517         $this->mock_setup();
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)));
523         // 1) Basic usage.
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')
538             ->will($this->returnValue(new core_completionlib_fake_recordset(array($progress1, $progress2))));
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));
547         // 2) With more than 1, 000 results.
548         $tracked = array();
549         $ids = array();
550         $progress = array();
551         for ($i = 100; $i<2000; $i++) {
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')
567             ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 0, 1000))));
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')
575             ->will($this->returnValue(new core_completionlib_fake_recordset(array_slice($progress, 1000))));
577         $result = $c->get_progress_all(true, 3);
578         $resultok = true;
579         $resultok  =  $resultok && ($ids == array_keys($result));
581         foreach ($result as $userid => $data) {
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     }
593     public function test_inform_grade_changed() {
594         $this->mock_setup();
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)));
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);
604         // Not enabled (should do nothing).
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);
611         // Enabled but still no grade completion required,  should still do nothing.
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);
618         // Enabled and completion required but item number is wrong,  does nothing.
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);
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);
641         // Same as above but marked deleted. It is supposed to call update_state
642         // with new potential state being COMPLETION_INCOMPLETE.
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     }
656     public function test_internal_get_grade_state() {
657         $this->mock_setup();
659         $item = new stdClass;
660         $grade = new stdClass;
662         $item->gradepass = 4;
663         $item->hidden = 0;
664         $grade->rawgrade = 4.0;
665         $grade->finalgrade = null;
667         // Grade has pass mark and is not hidden,  user passes.
668         $this->assertEquals(
669             COMPLETION_COMPLETE_PASS,
670             completion_info::internal_get_grade_state($item, $grade));
672         // Same but user fails.
673         $grade->rawgrade = 3.9;
674         $this->assertEquals(
675             COMPLETION_COMPLETE_FAIL,
676             completion_info::internal_get_grade_state($item, $grade));
678         // User fails on raw grade but passes on final.
679         $grade->finalgrade = 4.0;
680         $this->assertEquals(
681             COMPLETION_COMPLETE_PASS,
682             completion_info::internal_get_grade_state($item, $grade));
684         // Item is hidden.
685         $item->hidden = 1;
686         $this->assertEquals(
687             COMPLETION_COMPLETE,
688             completion_info::internal_get_grade_state($item, $grade));
690         // Item isn't hidden but has no pass mark.
691         $item->hidden = 0;
692         $item->gradepass = 0;
693         $this->assertEquals(
694             COMPLETION_COMPLETE,
695             completion_info::internal_get_grade_state($item, $grade));
696     }
698     public function test_get_activities() {
699         global $CFG;
700         $this->resetAfterTest();
702         // Enable completion before creating modules, otherwise the completion data is not written in DB.
703         $CFG->enablecompletion = true;
705         // Create a course with mixed auto completion data.
706         $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
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);
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);
718         // Create data in another course to make sure it's not considered.
719         $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
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);
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);
734         $this->assertFalse(isset($activities[$forum2->cmid]));
735         $this->assertFalse(isset($activities[$page2->cmid]));
736         $this->assertFalse(isset($activities[$data2->cmid]));
737     }
739     public function test_has_activities() {
740         global $CFG;
741         $this->resetAfterTest();
743         // Enable completion before creating modules, otherwise the completion data is not written in DB.
744         $CFG->enablecompletion = true;
746         // Create a course with mixed auto completion data.
747         $course = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
748         $course2 = $this->getDataGenerator()->create_course(array('enablecompletion' => true));
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);
754         $c1 = new completion_info($course);
755         $c2 = new completion_info($course2);
757         $this->assertTrue($c1->has_activities());
758         $this->assertFalse($c2->has_activities());
759     }
761     /**
762      * Test course module completion update event.
763      */
764     public function test_course_module_completion_updated_event() {
765         global $USER, $CFG;
767         $this->setup_data();
769         $this->setAdminUser();
771         $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
772         $forum = $this->getDataGenerator()->create_module('forum', array('course' => $this->course->id), $completionauto);
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);
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));
790         $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
791         $this->assertEquals($USER->id, $event->userid);
792         $this->assertEquals($this->user->id, $event->relateduserid);
793         $this->assertInstanceOf('moodle_url', $event->get_url());
794         $this->assertEventLegacyData($current, $event);
795     }
797     /**
798      * Test course completed event.
799      */
800     public function test_course_completed_event() {
801         global $USER;
803         $this->setup_data();
804         $this->setAdminUser();
806         $completionauto = array('completion' => COMPLETION_TRACKING_AUTOMATIC);
807         $ccompletion = new completion_completion(array('course' => $this->course->id, 'userid' => $this->user->id));
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);
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);
819         $this->assertEquals($this->user->id, $event->relateduserid);
820         $this->assertEquals(context_course::instance($this->course->id), $event->get_context());
821         $this->assertInstanceOf('moodle_url', $event->get_url());
822         $data = $ccompletion->get_record_data();
823         $this->assertEventLegacyData($data, $event);
824     }
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                 );
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();
846         $this->assertInstanceOf('\core\event\course_completion_updated', $event);
847         $this->assertEquals($this->course->id, $event->courseid);
848         $this->assertEquals($coursecontext, $event->get_context());
849         $this->assertInstanceOf('moodle_url', $event->get_url());
850         $expectedlegacylog = array($this->course->id, 'course', 'completion updated', 'completion.php?id='.$this->course->id);
851         $this->assertEventLegacyLogData($expectedlegacylog, $event);
852     }
854     public function test_completion_can_view_data() {
855         $this->setup_data();
857         $student = $this->getDataGenerator()->create_user();
858         $this->getDataGenerator()->enrol_user($student->id, $this->course->id);
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     }
866 class core_completionlib_fake_recordset implements Iterator {
867     protected $closed;
868     protected $values, $index;
870     public function __construct($values) {
871         $this->values = $values;
872         $this->index = 0;
873     }
875     public function current() {
876         return $this->values[$this->index];
877     }
879     public function key() {
880         return $this->values[$this->index];
881     }
883     public function next() {
884         $this->index++;
885     }
887     public function rewind() {
888         $this->index = 0;
889     }
891     public function valid() {
892         return count($this->values) > $this->index;
893     }
895     public function close() {
896         $this->closed = true;
897     }
899     public function was_closed() {
900         return $this->closed;
901     }