MDL-28021 Completion system can create inconsistent database rows
[moodle.git] / lib / simpletest / testcompletionlib.php
1 <?php
2 if (!defined('MOODLE_INTERNAL')) {
3     die('Direct access to this script is forbidden.');
4 }
5 require_once($CFG->libdir.'/completionlib.php');
7 global $DB;
8 Mock::generate(get_class($DB), 'mock_database');
9 Mock::generate('moodle_transaction', 'mock_transaction');
11 Mock::generatePartial('completion_info','completion_cutdown',
12     array('delete_all_state','get_tracked_users','update_state',
13         'internal_get_grade_state','is_enabled','get_data','internal_get_state','internal_set_data'));
14 Mock::generatePartial('completion_info','completion_cutdown2',
15     array('is_enabled','get_data','internal_get_state','internal_set_data'));
16 Mock::generatePartial('completion_info','completion_cutdown3',
17     array('internal_get_grade_state'));
19 class fake_recordset implements Iterator {
20     var $closed;
21     var $values,$index;
23     function fake_recordset($values) {
24         $this->values=$values;
25         $this->index=0;
26     }
28     function current() {
29         return $this->values[$this->index];
30     }
32     function key() {
33         return $this->values[$this->index];
34     }
36     function next() {
37         $this->index++;
38     }
40     function rewind() {
41         $this->index=0;
42     }
44     function valid() {
45         return count($this->values) > $this->index;
46     }
48     function close() {
49         $closed=true;
50     }
52     function was_closed() {
53         return $closed;
54     }
55 }
57 /**
58  * Expectation that checks an object for given values (normal equality test)
59  * plus a 'timemodified' field that is current (last second or two).
60  */
61 class TimeModifiedExpectation extends SimpleExpectation {
62     private $otherfields;
64     /**
65      * @param array $otherfields Array key=>value of required object fields
66      */
67     function TimeModifiedExpectation($otherfields) {
68         $this->otherfields=$otherfields;
69     }
71     function test($thing) {
72         $thingfields=(array)$thing;
73         foreach($this->otherfields as $key=>$value) {
74             if(!array_key_exists($key,$thingfields)) {
75                 return false;
76             }
77             if($thingfields[$key]!=$value) {
78                 return false;
79             }
80         }
82         $timedifference=time()-$thing->timemodified;
83         return ($timedifference < 2 && $timedifference>=0);
84     }
86     function testMessage($thing) {
87         return "Object does not match fields/time requirement";
88     }
89 }
91 class completionlib_test extends UnitTestCaseUsingDatabase {
93     public static $includecoverage = array('lib/completionlib.php');
94     var $realdb,$realcfg,$realsession,$realuser;
96     function setUp() {
97         global $DB,$CFG,$SESSION,$USER;
98         $this->realdb=$DB;
99         $this->realcfg=$CFG;
100         $this->realsession=$SESSION;
101         $this->prevuser=$USER;
102         $DB=new mock_database();
103         $CFG=clone($this->realcfg);
104         $CFG->prefix='test_';
105         $CFG->enablecompletion=COMPLETION_ENABLED;
106         $SESSION=new stdClass();
107         $USER=(object)array('id'=>314159);
108     }
110     function tearDown() {
111         global $DB,$CFG,$SESSION,$USER;
112         $DB=$this->realdb;
113         $CFG=$this->realcfg;
114         $SESSION=$this->realsession;
115         $USER=$this->prevuser;
116     }
118     function test_is_enabled() {
119         global $CFG;
121         // Config alone
122         $CFG->enablecompletion=COMPLETION_DISABLED;
123         $this->assertEqual(COMPLETION_DISABLED,completion_info::is_enabled_for_site());
124         $CFG->enablecompletion=COMPLETION_ENABLED;
125         $this->assertEqual(COMPLETION_ENABLED,completion_info::is_enabled_for_site());
127         // Course
128         //$course=new stdClass;
129         $course=(object)array('id'=>13);
130         $c=new completion_info($course);
131         $course->enablecompletion=COMPLETION_DISABLED;
132         $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled());
133         $course->enablecompletion=COMPLETION_ENABLED;
134         $this->assertEqual(COMPLETION_ENABLED,$c->is_enabled());
135         $CFG->enablecompletion=COMPLETION_DISABLED;
136         $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled());
138         // Course and CM
139         $cm=new stdClass;
140         $cm->completion=COMPLETION_TRACKING_MANUAL;
141         $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled($cm));
142         $CFG->enablecompletion=COMPLETION_ENABLED;
143         $course->enablecompletion=COMPLETION_DISABLED;
144         $this->assertEqual(COMPLETION_DISABLED,$c->is_enabled($cm));
145         $course->enablecompletion=COMPLETION_ENABLED;
146         $this->assertEqual(COMPLETION_TRACKING_MANUAL,$c->is_enabled($cm));
147         $cm->completion=COMPLETION_TRACKING_NONE;
148         $this->assertEqual(COMPLETION_TRACKING_NONE,$c->is_enabled($cm));
149         $cm->completion=COMPLETION_TRACKING_AUTOMATIC;
150         $this->assertEqual(COMPLETION_TRACKING_AUTOMATIC,$c->is_enabled($cm));
151     }
153     function test_update_state() {
154         $c=new completion_cutdown2();
155         $c->__construct((object)array('id'=>42));
156         $cm=(object)array('id'=>13,'course'=>42);
158         // Not enabled, should do nothing
159         $c->expectAt(0,'is_enabled',array($cm));
160         $c->setReturnValueAt(0,'is_enabled',false);
161         $c->update_state($cm);
163         // Enabled, but current state is same as possible result, do nothing
164         $current=(object)array('completionstate'=>COMPLETION_COMPLETE);
165         $c->expectAt(1,'is_enabled',array($cm));
166         $c->setReturnValueAt(1,'is_enabled',true);
168         $c->expectAt(0,'get_data',array($cm,false,0));
169         $c->setReturnValueAt(0,'get_data',$current);
170         $c->update_state($cm,COMPLETION_COMPLETE);
172         // Enabled, but current state is a specific one and new state is just
173         // omplete, so do nothing
174         $current->completionstate=COMPLETION_COMPLETE_PASS;
175         $c->expectAt(2,'is_enabled',array($cm));
176         $c->setReturnValueAt(2,'is_enabled',true);
177         $c->expectAt(1,'get_data',array($cm,false,0));
178         $c->setReturnValueAt(1,'get_data',$current);
179         $c->update_state($cm,COMPLETION_COMPLETE);
181         // Manual, change state (no change)
182         $cm->completion=COMPLETION_TRACKING_MANUAL;
183         $current->completionstate=COMPLETION_COMPLETE;
184         $c->expectAt(3,'is_enabled',array($cm));
185         $c->setReturnValueAt(3,'is_enabled',true);
186         $c->expectAt(2,'get_data',array($cm,false,0));
187         $c->setReturnValueAt(2,'get_data',$current);
188         $c->update_state($cm,COMPLETION_COMPLETE);
190         // Manual, change state (change)
191         $c->expectAt(4,'is_enabled',array($cm));
192         $c->setReturnValueAt(4,'is_enabled',true);
193         $c->expectAt(3,'get_data',array($cm,false,0));
194         $c->setReturnValueAt(3,'get_data',$current);
195         $c->expectAt(0,'internal_set_data',array($cm,
196             new TimeModifiedExpectation(array('completionstate'=>COMPLETION_INCOMPLETE))));
197         $c->update_state($cm,COMPLETION_INCOMPLETE);
199         // Auto, change state
200         $cm->completion=COMPLETION_TRACKING_AUTOMATIC;
201         $c->expectAt(5,'is_enabled',array($cm));
202         $c->setReturnValueAt(5,'is_enabled',true);
203         $c->expectAt(4,'get_data',array($cm,false,0));
204         $c->setReturnValueAt(4,'get_data',$current);
205         $c->expectAt(0,'internal_get_state',array($cm,0,$current));
206         $c->setReturnValueAt(0,'internal_get_state',COMPLETION_COMPLETE_PASS);
207         $c->expectAt(1,'internal_set_data',array($cm,
208             new TimeModifiedExpectation(array('completionstate'=>COMPLETION_COMPLETE_PASS))));
209         $c->update_state($cm,COMPLETION_COMPLETE_PASS);
211         $c->tally();
212     }
214     function test_internal_get_state() {
215         global $DB;
217         $c=new completion_cutdown3();
218         $c->__construct((object)array('id'=>42));
219         $cm=(object)array('id'=>13,'course'=>42,'completiongradeitemnumber'=>null);
221         // If view is required, but they haven't viewed it yet
222         $cm->completionview=COMPLETION_VIEW_REQUIRED;
223         $current=(object)array('viewed'=>COMPLETION_NOT_VIEWED);
224         $this->assertEqual(COMPLETION_INCOMPLETE,$c->internal_get_state($cm,123,$current));
226         // OK set view not required
227         $cm->completionview=COMPLETION_VIEW_NOT_REQUIRED;
229         // Test not getting module name
230         $cm->modname='label';
231         $this->assertEqual(COMPLETION_COMPLETE,$c->internal_get_state($cm,123,$current));
233         // Test getting module name
234         $cm->module=13;
235         unset($cm->modname);
236         $DB->expectOnce('get_field',array('modules','name',array('id'=>13)));
237         $DB->setReturnValue('get_field','label');
238         $this->assertEqual(COMPLETION_COMPLETE,$c->internal_get_state($cm,123,$current));
240         // Note: This function is not fully tested (including kind of the main
241         // part) because:
242         // * the grade_item/grade_grade calls are static and can't be mocked
243         // * the plugin_supports call is static and can't be mocked
245         $DB->tally();
246         $c->tally();
247     }
249     function test_set_module_viewed() {
250         $c=new completion_cutdown();
251         $c->__construct((object)array('id'=>42));
252         $cm=(object)array('id'=>13,'course'=>42);
254         // Not tracking completion, should do nothing
255         $cm->completionview=COMPLETION_VIEW_NOT_REQUIRED;
256         $c->set_module_viewed($cm);
258         // Tracking completion but completion is disabled, should do nothing
259         $cm->completionview=COMPLETION_VIEW_REQUIRED;
260         $c->expectAt(0,'is_enabled',array($cm));
261         $c->setReturnValueAt(0,'is_enabled',false);
262         $c->set_module_viewed($cm);
264         // Now it's enabled, we expect it to get data. If data already has
265         // viewed, still do nothing
266         $c->expectAt(1,'is_enabled',array($cm));
267         $c->setReturnValueAt(1,'is_enabled',true);
268         $c->expectAt(0,'get_data',array($cm,0));
269         $hasviewed=(object)array('viewed'=>COMPLETION_VIEWED);
270         $c->setReturnValueAt(0,'get_data',$hasviewed);
271         $c->set_module_viewed($cm);
273         // OK finally one that hasn't been viewed, now it should set it viewed
274         // and update state
275         $c->expectAt(2,'is_enabled',array($cm));
276         $c->setReturnValueAt(2,'is_enabled',true);
277         $notviewed=(object)array('viewed'=>COMPLETION_NOT_VIEWED);
278         $c->expectAt(1,'get_data',array($cm,1337));
279         $c->setReturnValueAt(1,'get_data',$notviewed);
280         $c->expectOnce('internal_set_data',array($cm,$hasviewed));
281         $c->expectOnce('update_state',array($cm,COMPLETION_COMPLETE,1337));
282         $c->set_module_viewed($cm,1337);
284         $c->tally();
285     }
287     function test_count_user_data() {
288         global $DB;
289         $course=(object)array('id'=>13);
290         $cm=(object)array('id'=>42);
291         $DB->setReturnValue('get_field_sql',666);
292         $DB->expectOnce('get_field_sql',array(new IgnoreWhitespaceExpectation("SELECT
293     COUNT(1)
294 FROM
295     {course_modules_completion}
296 WHERE
297     coursemoduleid=? AND completionstate<>0"),array(42)));
298         $c=new completion_info($course);
299         $this->assertEqual(666,$c->count_user_data($cm));
301         $DB->tally();
302     }
304     function test_delete_all_state() {
305         global $DB,$SESSION;
306         $course=(object)array('id'=>13);
307         $cm=(object)array('id'=>42,'course'=>13);
308         $c=new completion_info($course);
309         // Check it works ok without data in session
310         $DB->expectAt(0,'delete_records',
311             array('course_modules_completion',array('coursemoduleid'=>42)));
312         $c->delete_all_state($cm);
314         // Build up a session to check it deletes the right bits from it
315         // (and not other bits)
316         $SESSION->completioncache=array();
317         $SESSION->completioncache[13]=array();
318         $SESSION->completioncache[13][42]='foo';
319         $SESSION->completioncache[13][43]='foo';
320         $SESSION->completioncache[14]=array();
321         $SESSION->completioncache[14][42]='foo';
322         $DB->expectAt(1,'delete_records',
323             array('course_modules_completion',array('coursemoduleid'=>42)));
324         $c->delete_all_state($cm);
325         $this->assertEqual(array(13=>array(43=>'foo'),14=>array(42=>'foo')),
326             $SESSION->completioncache);
328         $DB->tally();
329     }
331     function test_reset_all_state() {
332         global $DB;
333         $c=new completion_cutdown();
334         $c->__construct((object)array('id'=>42));
336         $cm=(object)array('id'=>13,'course'=>42,
337             'completion'=>COMPLETION_TRACKING_AUTOMATIC);
339         $DB->setReturnValue('get_recordset',new fake_recordset(array(
340             (object)array('id'=>1,'userid'=>100),
341             (object)array('id'=>2,'userid'=>101),
342         )));
343         $DB->expectOnce('get_recordset',array('course_modules_completion',
344             array('coursemoduleid'=>13),'','userid'));
345         $c->expectOnce('delete_all_state',array($cm));
346         $c->expectOnce('get_tracked_users',array());
347         $c->setReturnValue('get_tracked_users',array(
348             (object)array('id'=>100,'firstname'=>'Woot','lastname'=>'Plugh'),
349             (object)array('id'=>201,'firstname'=>'Vroom','lastname'=>'Xyzzy'),
350             ));
352         $c->expectAt(0,'update_state',array($cm,COMPLETION_UNKNOWN,100));
353         $c->expectAt(1,'update_state',array($cm,COMPLETION_UNKNOWN,101));
354         $c->expectAt(2,'update_state',array($cm,COMPLETION_UNKNOWN,201));
356         $c->reset_all_state($cm);
358         $DB->tally();
359         $c->tally();
360     }
362     function test_get_data() {
363         global $DB,$SESSION;
365         $c=new completion_info((object)array('id'=>42));
366         $cm=(object)array('id'=>13,'course'=>42);
368         // 1. Not current user, record exists
369         $sillyrecord=(object)array('frog'=>'kermit');
370         $DB->expectAt(0,'get_record',array('course_modules_completion',
371             array('coursemoduleid'=>13,'userid'=>123)));
372         $DB->setReturnValueAt(0,'get_record',$sillyrecord);
373         $result=$c->get_data($cm,false,123);
374         $this->assertEqual($sillyrecord,$result);
375         $this->assertTrue(empty($SESSION->completioncache));
377         // 2. Not current user, default record, wholecourse (ignored)
378         $DB->expectAt(1,'get_record',array('course_modules_completion',
379             array('coursemoduleid'=>13,'userid'=>123)));
380         $DB->setReturnValueAt(1,'get_record',false);
381         $result=$c->get_data($cm,true,123);
382         $this->assertEqual((object)array(
383             'id'=>'0','coursemoduleid'=>13,'userid'=>123,'completionstate'=>0,
384             'viewed'=>0,'timemodified'=>0),$result);
385         $this->assertTrue(empty($SESSION->completioncache));
387         // 3. Current user, single record, not from cache
388         $DB->expectAt(2,'get_record',array('course_modules_completion',
389             array('coursemoduleid'=>13,'userid'=>314159)));
390         $DB->setReturnValueAt(2,'get_record',$sillyrecord);
391         $result=$c->get_data($cm);
392         $this->assertEqual($sillyrecord,$result);
393         $this->assertEqual($sillyrecord,$SESSION->completioncache[42][13]);
394         // When checking time(), allow for second overlaps
395         $this->assertTrue(time()-$SESSION->completioncache[42]['updated']<2);
397         // 4. Current user, 'whole course', but from cache
398         $result=$c->get_data($cm,true);
399         $this->assertEqual($sillyrecord,$result);
401         // 5. Current user, single record, cache expired
402         $SESSION->completioncache[42]['updated']=37; // Quite a long time ago
403         $now=time();
404         $SESSION->completioncache[17]['updated']=$now;
405         $SESSION->completioncache[39]['updated']=72; // Also a long time ago
406         $DB->expectAt(3,'get_record',array('course_modules_completion',
407             array('coursemoduleid'=>13,'userid'=>314159)));
408         $DB->setReturnValueAt(3,'get_record',$sillyrecord);
409         $result=$c->get_data($cm,false);
410         $this->assertEqual($sillyrecord,$result);
411         // Check that updated value is right, then fudge it to make next compare
412         // work
413         $this->assertTrue(time()-$SESSION->completioncache[42]['updated']<2);
414         $SESSION->completioncache[42]['updated']=$now;
415         // Check things got expired from cache
416         $this->assertEqual(array(42=>array(13=>$sillyrecord,'updated'=>$now),
417             17=>array('updated'=>$now)),$SESSION->completioncache);
419         // 6. Current user, 'whole course' and record not in cache
420         unset($SESSION->completioncache);
422         // Scenario: Completion data exists for one CMid
423         $basicrecord=(object)array('coursemoduleid'=>13);
424         $DB->setReturnValueAt(0,'get_records_sql',array(
425             1=>$basicrecord
426         ));
427         $DB->expectAt(0,'get_records_sql',array(new IgnoreWhitespaceExpectation("
428 SELECT
429     cmc.*
430 FROM
431     {course_modules} cm
432     INNER JOIN {course_modules_completion} cmc ON cmc.coursemoduleid=cm.id
433 WHERE
434     cm.course=? AND cmc.userid=?"),array(42,314159)));
436         // There are two CMids in total, the one we had data for and another one
437         $modinfo->cms=array((object)array('id'=>13),(object)array('id'=>14));
438         $result=$c->get_data($cm,true,0,$modinfo);
440         // Check result
441         $this->assertEqual($basicrecord,$result);
443         // Check the cache contents
444         $this->assertTrue(time()-$SESSION->completioncache[42]['updated']<2);
445         $SESSION->completioncache[42]['updated']=$now;
446         $this->assertEqual(array(42=>array(13=>$basicrecord,14=>(object)array(
447             'id'=>'0','coursemoduleid'=>14,'userid'=>314159,'completionstate'=>0,
448             'viewed'=>0,'timemodified'=>0),'updated'=>$now)),$SESSION->completioncache);
450         $DB->tally();
451     }
453     function test_internal_set_data() {
454         global $DB,$SESSION;
456         $cm = (object)array('course' => 42,'id' => 13);
457         $c = new completion_info((object)array('id' => 42));
459         // 1) Test with new data
460         $data = (object)array('id'=>0, 'userid' => 314159, 'coursemoduleid' => 99);
461         $DB->setReturnValueAt(0, 'start_delegated_transaction', new mock_transaction());
462         $DB->setReturnValueAt(0, 'insert_record', 4);
463         $DB->expectAt(0, 'get_field', array('course_modules_completion', 'id',
464                 array('coursemoduleid' => 99, 'userid' => 314159)));
465         $DB->expectAt(0, 'insert_record', array('course_modules_completion', $data));
466         $c->internal_set_data($cm, $data);
467         $this->assertEqual(4, $data->id);
468         $this->assertEqual(array(42 => array(13 => $data)), $SESSION->completioncache);
470         // 2) Test with existing data and for different user (not cached)
471         unset($SESSION->completioncache);
472         $d2 = (object)array('id' => 7, 'userid' => 17, 'coursemoduleid' => 66);
473         $DB->setReturnValueAt(1, 'start_delegated_transaction', new mock_transaction());
474         $DB->expectAt(0,'update_record', array('course_modules_completion', $d2));
475         $c->internal_set_data($cm, $d2);
476         $this->assertFalse(isset($SESSION->completioncache));
478         // 3) Test where it THINKS the data is new (from cache) but actually
479         // in the database it has been set since
480         // 1) Test with new data
481         $data = (object)array('id'=>0, 'userid' => 314159, 'coursemoduleid' => 99);
482         $DB->setReturnValueAt(2, 'start_delegated_transaction', new mock_transaction());
483         $DB->setReturnValueAt(1, 'get_field', 13);
484         $DB->expectAt(1, 'get_field', array('course_modules_completion', 'id',
485                 array('coursemoduleid' => 99, 'userid' => 314159)));
486         $d3 = (object)array('id' => 13, 'userid' => 314159, 'coursemoduleid' => 99);
487         $DB->expectAt(1,'update_record', array('course_modules_completion', $d3));
488         $c->internal_set_data($cm, $data);
490         $DB->tally();
491     }
493     function test_get_activities() {
494         global $DB;
496         $c=new completion_info((object)array('id'=>42));
498         // Try with no activities
499         $DB->expectAt(0,'get_records_select',array('course_modules',
500               'course=42 AND completion<>'.COMPLETION_TRACKING_NONE));
501         $DB->setReturnValueAt(0,'get_records_select',array());
502         $result=$c->get_activities();
503         $this->assertEqual(array(),$result);
505         // Try with an activity (need to fake up modinfo for it as well)
506         $DB->expectAt(1,'get_records_select',array('course_modules',
507               'course=42 AND completion<>'.COMPLETION_TRACKING_NONE));
508         $DB->setReturnValueAt(1,'get_records_select',array(
509             13=>(object)array('id'=>13)
510         ));
511         $modinfo=new stdClass;
512         $modinfo->sections=array(array(1,2,3),array(12,13,14));
513         $modinfo->cms[13]=(object)array('modname'=>'frog','name'=>'kermit');
514         $result=$c->get_activities($modinfo);
515         $this->assertEqual(array(13=>(object)array('id'=>13,'modname'=>'frog','name'=>'kermit')),$result);
517         $DB->tally();
518     }
520     // get_tracked_users() cannot easily be tested because it uses
521     // get_role_users, so skipping that
523     function test_get_progress_all() {
524         global $DB;
526         $c=new completion_cutdown();
527         $c->__construct((object)array('id'=>42));
529         // 1) Basic usage
530         $c->expectAt(0,'get_tracked_users',array(false, array(), 0, '', '', ''));
531         $c->setReturnValueAt(0,'get_tracked_users',array(
532             (object)array('id'=>100,'firstname'=>'Woot','lastname'=>'Plugh'),
533             (object)array('id'=>201,'firstname'=>'Vroom','lastname'=>'Xyzzy'),
534             ));
535         $DB->expectAt(0,'get_in_or_equal',array(array(100,201)));
536         $DB->setReturnValueAt(0,'get_in_or_equal',array(' IN (100,201)',array()));
537         $DB->expectAt(0,'get_recordset_sql',array(new IgnoreWhitespaceExpectation("
538 SELECT
539     cmc.*
540 FROM
541     {course_modules} cm
542     INNER JOIN {course_modules_completion} cmc ON cm.id=cmc.coursemoduleid
543 WHERE
544     cm.course=? AND cmc.userid IN (100,201)"),array(42)));
545         $progress1=(object)array('userid'=>100,'coursemoduleid'=>13);
546         $progress2=(object)array('userid'=>201,'coursemoduleid'=>14);
547         $DB->setReturnValueAt(0,'get_recordset_sql',new fake_recordset(array(
548             $progress1,$progress2
549         )));
550         $this->assertEqual(array(
552                 100 => (object)array('id'=>100,'firstname'=>'Woot','lastname'=>'Plugh',
553                     'progress'=>array(13=>$progress1)),
554                 201 => (object)array('id'=>201,'firstname'=>'Vroom','lastname'=>'Xyzzy',
555                     'progress'=>array(14=>$progress2)),
556             ),$c->get_progress_all(false));
558         // 2) With more than 1,000 results
559         $c->expectAt(1,'get_tracked_users',array(true, 3, 0, '', '', ''));
561         $tracked=array();
562         $ids=array();
563         $progress=array();
564         for($i=100;$i<2000;$i++) {
565             $tracked[]=(object)array('id'=>$i,'firstname'=>'frog','lastname'=>$i);
566             $ids[]=$i;
567             $progress[]=(object)array('userid'=>$i,'coursemoduleid'=>13);
568             $progress[]=(object)array('userid'=>$i,'coursemoduleid'=>14);
569         }
570         $c->setReturnValueAt(1,'get_tracked_users',$tracked);
572         $DB->expectAt(1,'get_in_or_equal',array(array_slice($ids,0,1000)));
573         $DB->setReturnValueAt(1,'get_in_or_equal',array(' IN whatever',array()));
574         $DB->expectAt(1,'get_recordset_sql',array(new IgnoreWhitespaceExpectation("
575 SELECT
576     cmc.*
577 FROM
578     {course_modules} cm
579     INNER JOIN {course_modules_completion} cmc ON cm.id=cmc.coursemoduleid
580 WHERE
581     cm.course=? AND cmc.userid IN whatever"),array(42)));
582         $DB->setReturnValueAt(1,'get_recordset_sql',new fake_recordset(array_slice($progress,0,1000)));
583         $DB->expectAt(2,'get_in_or_equal',array(array_slice($ids,1000)));
584         $DB->setReturnValueAt(2,'get_in_or_equal',array(' IN whatever2',array()));
585         $DB->expectAt(2,'get_recordset_sql',array(new IgnoreWhitespaceExpectation("
586 SELECT
587     cmc.*
588 FROM
589     {course_modules} cm
590     INNER JOIN {course_modules_completion} cmc ON cm.id=cmc.coursemoduleid
591 WHERE
592     cm.course=? AND cmc.userid IN whatever2"),array(42)));
593         $DB->setReturnValueAt(2,'get_recordset_sql',new fake_recordset(array_slice($progress,1000)));
594         $result=$c->get_progress_all(true,3);
595         $resultok=true;
596         $resultok = $resultok && ($ids==array_keys($result));
598         foreach($result as $userid => $data) {
599             $resultok = $resultok && $data->firstname=='frog';
600             $resultok = $resultok && $data->lastname==$userid;
601             $resultok = $resultok && $data->id==$userid;
602             $cms=$data->progress;
603             $resultok= $resultok && (array(13,14)==array_keys($cms));
604             $resultok= $resultok && ((object)array('userid'=>$userid,'coursemoduleid'=>13)==$cms[13]);
605             $resultok= $resultok && ((object)array('userid'=>$userid,'coursemoduleid'=>14)==$cms[14]);
606         }
607         $this->assertTrue($resultok);
609         $DB->tally();
610         $c->tally();
611     }
613     function test_inform_grade_changed() {
614         $c=new completion_cutdown();
615         $c->__construct((object)array('id'=>42));
617         $cm=(object)array('course'=>42,'id'=>13,'completion'=>0,'completiongradeitemnumber'=>null);
618         $item=(object)array('itemnumber'=>3);
619         $grade=(object)array('userid'=>31337);
621         // Not enabled (should do nothing)
622         $c->setReturnValueAt(0,'is_enabled',false);
623         $c->expectAt(0,'is_enabled',array($cm));
624         $c->inform_grade_changed($cm,$item,$grade,false);
626         // Enabled but still no grade completion required, should still do nothing
627         $c->setReturnValueAt(1,'is_enabled',true);
628         $c->expectAt(1,'is_enabled',array($cm));
629         $c->inform_grade_changed($cm,$item,$grade,false);
631         // Enabled and completion required but item number is wrong, does nothing
632         $cm->completiongradeitemnumber=7;
633         $c->setReturnValueAt(2,'is_enabled',true);
634         $c->expectAt(2,'is_enabled',array($cm));
635         $c->inform_grade_changed($cm,$item,$grade,false);
637         // Enabled and completion required and item number right. It is supposed
638         // to call update_state with the new potential state being obtained from
639         // internal_get_grade_state.
640         $cm->completiongradeitemnumber=3;
641         $c->setReturnValueAt(3,'is_enabled',true);
642         $c->expectAt(3,'is_enabled',array($cm));
643         $c->expectAt(0,'internal_get_grade_state',array($item,$grade));
644         $c->setReturnValueAt(0,'internal_get_grade_state',COMPLETION_COMPLETE_PASS);
645         $c->expectAt(0,'update_state',array($cm,COMPLETION_COMPLETE_PASS,31337));
646         $c->inform_grade_changed($cm,$item,$grade,false);
648         // Same as above but marked deleted. It is supposed to call update_state
649         // with new potential state being COMPLETION_INCOMPLETE
650         $c->setReturnValueAt(4,'is_enabled',false);
651         $c->expectAt(4,'is_enabled',array($cm));
652         $c->expectAt(1,'update_state',array($cm,COMPLETION_INCOMPLETE,31337));
653         $c->inform_grade_changed($cm,$item,$grade,false);
655         $c->tally();
656     }
658     function test_internal_get_grade_state() {
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->assertEqual(
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->assertEqual(
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->assertEqual(
681             COMPLETION_COMPLETE_PASS,
682             completion_info::internal_get_grade_state($item,$grade));
684         // Item is hidden
685         $item->hidden=1;
686         $this->assertEqual(
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->assertEqual(
694             COMPLETION_COMPLETE,
695             completion_info::internal_get_grade_state($item,$grade));
696     }