Merge branch 'MDL-10971_cloze_shuffle_fix' of git://github.com/timhunt/moodle
[moodle.git] / backup / moodle2 / tests / moodle2_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  * Tests for Moodle 2 format backup operation.
19  *
20  * @package core_backup
21  * @copyright 2014 The Open University
22  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 global $CFG;
28 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
29 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
30 require_once($CFG->libdir . '/completionlib.php');
32 /**
33  * Tests for Moodle 2 format backup operation.
34  *
35  * @package core_backup
36  * @copyright 2014 The Open University
37  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class core_backup_moodle2_testcase extends advanced_testcase {
41     /**
42      * Tests the availability field on modules and sections is correctly
43      * backed up and restored.
44      */
45     public function test_backup_availability() {
46         global $DB, $CFG;
48         $this->resetAfterTest(true);
49         $this->setAdminUser();
50         $CFG->enableavailability = true;
51         $CFG->enablecompletion = true;
53         // Create a course with some availability data set.
54         $generator = $this->getDataGenerator();
55         $course = $generator->create_course(
56                 array('format' => 'topics', 'numsections' => 3,
57                     'enablecompletion' => COMPLETION_ENABLED),
58                 array('createsections' => true));
59         $forum = $generator->create_module('forum', array(
60                 'course' => $course->id));
61         $forum2 = $generator->create_module('forum', array(
62                 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
64         // We need a grade, easiest is to add an assignment.
65         $assignrow = $generator->create_module('assign', array(
66                 'course' => $course->id));
67         $assign = new assign(context_module::instance($assignrow->cmid), false, false);
68         $item = $assign->get_grade_item();
70         // Make a test grouping as well.
71         $grouping = $generator->create_grouping(array('courseid' => $course->id,
72                 'name' => 'Grouping!'));
74         $availability = '{"op":"|","show":false,"c":[' .
75                 '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' .
76                 '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' .
77                 '{"type":"grouping","id":' . $grouping->id . '}' .
78                 ']}';
79         $DB->set_field('course_modules', 'availability', $availability, array(
80                 'id' => $forum->cmid));
81         $DB->set_field('course_sections', 'availability', $availability, array(
82                 'course' => $course->id, 'section' => 1));
84         // Backup and restore it.
85         $newcourseid = $this->backup_and_restore($course);
87         // Check settings in new course.
88         $modinfo = get_fast_modinfo($newcourseid);
89         $forums = array_values($modinfo->get_instances_of('forum'));
90         $assigns = array_values($modinfo->get_instances_of('assign'));
91         $newassign = new assign(context_module::instance($assigns[0]->id), false, false);
92         $newitem = $newassign->get_grade_item();
93         $newgroupingid = $DB->get_field('groupings', 'id', array('courseid' => $newcourseid));
95         // Expected availability should have new ID for the forum, grade, and grouping.
96         $newavailability = str_replace(
97                 '"grouping","id":' . $grouping->id,
98                 '"grouping","id":' . $newgroupingid,
99                 str_replace(
100                     '"grade","id":' . $item->id,
101                     '"grade","id":' . $newitem->id,
102                     str_replace(
103                         '"cm":' . $forum2->cmid,
104                         '"cm":' . $forums[1]->id,
105                         $availability)));
107         $this->assertEquals($newavailability, $forums[0]->availability);
108         $this->assertNull($forums[1]->availability);
109         $this->assertEquals($newavailability, $modinfo->get_section_info(1, MUST_EXIST)->availability);
110         $this->assertNull($modinfo->get_section_info(2, MUST_EXIST)->availability);
111     }
113     /**
114      * The availability data format was changed in Moodle 2.7. This test
115      * ensures that a Moodle 2.6 backup with this data can still be correctly
116      * restored.
117      */
118     public function test_restore_legacy_availability() {
119         global $DB, $USER, $CFG;
120         require_once($CFG->dirroot . '/grade/querylib.php');
121         require_once($CFG->libdir . '/completionlib.php');
123         $this->resetAfterTest(true);
124         $this->setAdminUser();
125         $CFG->enableavailability = true;
126         $CFG->enablecompletion = true;
128         // Extract backup file.
129         $backupid = 'abc';
130         $backuppath = $CFG->tempdir . '/backup/' . $backupid;
131         check_dir_exists($backuppath);
132         get_file_packer('application/vnd.moodle.backup')->extract_to_pathname(
133                 __DIR__ . '/fixtures/availability_26_format.mbz', $backuppath);
135         // Do restore to new course with default settings.
136         $generator = $this->getDataGenerator();
137         $categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
138         $newcourseid = restore_dbops::create_new_course(
139                 'Test fullname', 'Test shortname', $categoryid);
140         $rc = new restore_controller($backupid, $newcourseid,
141                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
142                 backup::TARGET_NEW_COURSE);
143         $thrown = null;
144         try {
145             $this->assertTrue($rc->execute_precheck());
146             $rc->execute_plan();
147             $rc->destroy();
148         } catch (Exception $e) {
149             $thrown = $e;
150             // Because of the PHPUnit exception behaviour in this situation, we
151             // will not see this message unless it is explicitly echoed (just
152             // using it in a fail() call or similar will not work).
153             echo "\n\nEXCEPTION: " . $thrown->getMessage() . '[' .
154                     $thrown->getFile() . ':' . $thrown->getLine(). "]\n\n";
155         }
157         // Must set restore_controller variable to null so that php
158         // garbage-collects it; otherwise the file will be left open and
159         // attempts to delete it will cause a permission error on Windows
160         // systems, breaking unit tests.
161         $rc = null;
162         $this->assertNull($thrown);
164         // Get information about the resulting course and check that it is set
165         // up correctly.
166         $modinfo = get_fast_modinfo($newcourseid);
167         $pages = array_values($modinfo->get_instances_of('page'));
168         $forums = array_values($modinfo->get_instances_of('forum'));
169         $quizzes = array_values($modinfo->get_instances_of('quiz'));
170         $grouping = $DB->get_record('groupings', array('courseid' => $newcourseid));
172         // FROM date.
173         $this->assertEquals(
174                 '{"op":"&","showc":[true],"c":[{"type":"date","d":">=","t":1893456000}]}',
175                 $pages[1]->availability);
176         // UNTIL date.
177         $this->assertEquals(
178                 '{"op":"&","showc":[false],"c":[{"type":"date","d":"<","t":1393977600}]}',
179                 $pages[2]->availability);
180         // FROM and UNTIL.
181         $this->assertEquals(
182                 '{"op":"&","showc":[true,false],"c":[' .
183                 '{"type":"date","d":">=","t":1449705600},' .
184                 '{"type":"date","d":"<","t":1893456000}' .
185                 ']}',
186                 $pages[3]->availability);
187         // Grade >= 75%.
188         $grades = array_values(grade_get_grade_items_for_activity($quizzes[0], true));
189         $gradeid = $grades[0]->id;
190         $coursegrade = grade_item::fetch_course_item($newcourseid);
191         $this->assertEquals(
192                 '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":75}]}',
193                 $pages[4]->availability);
194         // Grade < 25%.
195         $this->assertEquals(
196                 '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"max":25}]}',
197                 $pages[5]->availability);
198         // Grade 90-100%.
199         $this->assertEquals(
200                 '{"op":"&","showc":[true],"c":[{"type":"grade","id":' . $gradeid . ',"min":90,"max":100}]}',
201                 $pages[6]->availability);
202         // Email contains frog.
203         $this->assertEquals(
204                 '{"op":"&","showc":[true],"c":[{"type":"profile","op":"contains","sf":"email","v":"frog"}]}',
205                 $pages[7]->availability);
206         // Page marked complete..
207         $this->assertEquals(
208                 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $pages[0]->id .
209                 ',"e":' . COMPLETION_COMPLETE . '}]}',
210                 $pages[8]->availability);
211         // Quiz complete but failed.
212         $this->assertEquals(
213                 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id .
214                 ',"e":' . COMPLETION_COMPLETE_FAIL . '}]}',
215                 $pages[9]->availability);
216         // Quiz complete and succeeded.
217         $this->assertEquals(
218                 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id .
219                 ',"e":' . COMPLETION_COMPLETE_PASS. '}]}',
220                 $pages[10]->availability);
221         // Quiz not complete.
222         $this->assertEquals(
223                 '{"op":"&","showc":[true],"c":[{"type":"completion","cm":' . $quizzes[0]->id .
224                 ',"e":' . COMPLETION_INCOMPLETE . '}]}',
225                 $pages[11]->availability);
226         // Grouping.
227         $this->assertEquals(
228                 '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}',
229                 $pages[12]->availability);
231         // All the options.
232         $this->assertEquals('{"op":"&",' .
233                 '"showc":[false,true,false,true,true,true,true,true,true],' .
234                 '"c":[' .
235                 '{"type":"grouping","id":' . $grouping->id . '},' .
236                 '{"type":"date","d":">=","t":1488585600},' .
237                 '{"type":"date","d":"<","t":1709510400},' .
238                 '{"type":"profile","op":"contains","sf":"email","v":"@"},' .
239                 '{"type":"profile","op":"contains","sf":"city","v":"Frogtown"},' .
240                 '{"type":"grade","id":' . $gradeid . ',"min":30,"max":35},' .
241                 '{"type":"grade","id":' . $coursegrade->id . ',"min":5,"max":10},' .
242                 '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '},' .
243                 '{"type":"completion","cm":' . $quizzes[0]->id .',"e":' . COMPLETION_INCOMPLETE . '}' .
244                 ']}', $pages[13]->availability);
246         // Group members only forum.
247         $this->assertEquals(
248                 '{"op":"&","showc":[false],"c":[{"type":"group"}]}',
249                 $forums[0]->availability);
251         // Section with lots of conditions.
252         $this->assertEquals(
253                 '{"op":"&","showc":[false,false,false,false],"c":[' .
254                 '{"type":"date","d":">=","t":1417737600},' .
255                 '{"type":"profile","op":"contains","sf":"email","v":"@"},' .
256                 '{"type":"grade","id":' . $gradeid . ',"min":20},' .
257                 '{"type":"completion","cm":' . $pages[0]->id . ',"e":' . COMPLETION_COMPLETE . '}]}',
258                 $modinfo->get_section_info(3)->availability);
260         // Section with grouping.
261         $this->assertEquals(
262                 '{"op":"&","showc":[false],"c":[{"type":"grouping","id":' . $grouping->id . '}]}',
263                 $modinfo->get_section_info(4)->availability);
264     }
266     /**
267      * Tests the backup and restore of single activity to same course (duplicate)
268      * when it contains availability conditions that depend on other items in
269      * course.
270      */
271     public function test_duplicate_availability() {
272         global $DB, $CFG;
274         $this->resetAfterTest(true);
275         $this->setAdminUser();
276         $CFG->enableavailability = true;
277         $CFG->enablecompletion = true;
279         // Create a course with completion enabled and 2 forums.
280         $generator = $this->getDataGenerator();
281         $course = $generator->create_course(
282                 array('format' => 'topics', 'enablecompletion' => COMPLETION_ENABLED));
283         $forum = $generator->create_module('forum', array(
284                 'course' => $course->id));
285         $forum2 = $generator->create_module('forum', array(
286                 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL));
288         // We need a grade, easiest is to add an assignment.
289         $assignrow = $generator->create_module('assign', array(
290                 'course' => $course->id));
291         $assign = new assign(context_module::instance($assignrow->cmid), false, false);
292         $item = $assign->get_grade_item();
294         // Make a test group and grouping as well.
295         $group = $generator->create_group(array('courseid' => $course->id,
296                 'name' => 'Group!'));
297         $grouping = $generator->create_grouping(array('courseid' => $course->id,
298                 'name' => 'Grouping!'));
300         // Set the forum to have availability conditions on all those things,
301         // plus some that don't exist or are special values.
302         $availability = '{"op":"|","show":false,"c":[' .
303                 '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' .
304                 '{"type":"completion","cm":99999999,"e":1},' .
305                 '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' .
306                 '{"type":"grade","id":99999998,"min":4,"max":94},' .
307                 '{"type":"grouping","id":' . $grouping->id . '},' .
308                 '{"type":"grouping","id":99999997},' .
309                 '{"type":"group","id":' . $group->id . '},' .
310                 '{"type":"group"},' .
311                 '{"type":"group","id":99999996}' .
312                 ']}';
313         $DB->set_field('course_modules', 'availability', $availability, array(
314                 'id' => $forum->cmid));
316         // Duplicate it.
317         $newcmid = $this->duplicate($course, $forum->cmid);
319         // For those which still exist on the course we expect it to keep using
320         // the real ID. For those which do not exist on the course any more
321         // (e.g. simulating backup/restore of single activity between 2 courses)
322         // we expect the IDs to be replaced with marker value: 0 for cmid
323         // and grade, -1 for group/grouping.
324         $expected = str_replace(
325                 array('99999999', '99999998', '99999997', '99999996'),
326                 array(0, 0, -1, -1),
327                 $availability);
329         // Check settings in new activity.
330         $actual = $DB->get_field('course_modules', 'availability', array('id' => $newcmid));
331         $this->assertEquals($expected, $actual);
332     }
334     /**
335      * When restoring a course, you can change the start date, which shifts other
336      * dates. This test checks that certain dates are correctly modified.
337      */
338     public function test_restore_dates() {
339         global $DB, $CFG;
341         $this->resetAfterTest(true);
342         $this->setAdminUser();
343         $CFG->enableavailability = true;
345         // Create a course with specific start date.
346         $generator = $this->getDataGenerator();
347         $course = $generator->create_course(array(
348                 'startdate' => strtotime('1 Jan 2014 00:00 GMT')));
350         // Add a forum with conditional availability date restriction, including
351         // one of them nested inside a tree.
352         $availability = '{"op":"&","showc":[true,true],"c":[' .
353                 '{"op":"&","c":[{"type":"date","d":">=","t":DATE1}]},' .
354                 '{"type":"date","d":"<","t":DATE2}]}';
355         $before = str_replace(
356                 array('DATE1', 'DATE2'),
357                 array(strtotime('1 Feb 2014 00:00 GMT'), strtotime('10 Feb 2014 00:00 GMT')),
358                 $availability);
359         $forum = $generator->create_module('forum', array('course' => $course->id,
360                 'availability' => $before));
362         // Add an assign with defined start date.
363         $assign = $generator->create_module('assign', array('course' => $course->id,
364                 'allowsubmissionsfromdate' => strtotime('7 Jan 2014 16:00 GMT')));
366         // Do backup and restore.
367         $newcourseid = $this->backup_and_restore($course, strtotime('3 Jan 2015 00:00 GMT'));
369         $modinfo = get_fast_modinfo($newcourseid);
371         // Check forum dates are modified by the same amount as the course start.
372         $newforums = $modinfo->get_instances_of('forum');
373         $newforum = reset($newforums);
374         $after = str_replace(
375             array('DATE1', 'DATE2'),
376             array(strtotime('3 Feb 2015 00:00 GMT'), strtotime('12 Feb 2015 00:00 GMT')),
377             $availability);
378         $this->assertEquals($after, $newforum->availability);
380         // Check assign date.
381         $newassigns = $modinfo->get_instances_of('assign');
382         $newassign = reset($newassigns);
383         $this->assertEquals(strtotime('9 Jan 2015 16:00 GMT'), $DB->get_field(
384                 'assign', 'allowsubmissionsfromdate', array('id' => $newassign->instance)));
385     }
387     /**
388      * Backs a course up and restores it.
389      *
390      * @param stdClass $course Course object to backup
391      * @param int $newdate If non-zero, specifies custom date for new course
392      * @return int ID of newly restored course
393      */
394     protected function backup_and_restore($course, $newdate = 0) {
395         global $USER, $CFG;
397         // Turn off file logging, otherwise it can't delete the file (Windows).
398         $CFG->backup_file_logger_level = backup::LOG_NONE;
400         // Do backup with default settings. MODE_IMPORT means it will just
401         // create the directory and not zip it.
402         $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
403                 backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
404                 $USER->id);
405         $backupid = $bc->get_backupid();
406         $bc->execute_plan();
407         $bc->destroy();
409         // Do restore to new course with default settings.
410         $newcourseid = restore_dbops::create_new_course(
411                 $course->fullname, $course->shortname . '_2', $course->category);
412         $rc = new restore_controller($backupid, $newcourseid,
413                 backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
414                 backup::TARGET_NEW_COURSE);
415         if ($newdate) {
416             $rc->get_plan()->get_setting('course_startdate')->set_value($newdate);
417         }
418         $this->assertTrue($rc->execute_precheck());
419         $rc->execute_plan();
420         $rc->destroy();
422         return $newcourseid;
423     }
425     /**
426      * Duplicates a single activity within a course.
427      *
428      * This is based on the code from course/modduplicate.php, but reduced for
429      * simplicity.
430      *
431      * @param stdClass $course Course object
432      * @param int $cmid Activity to duplicate
433      * @return int ID of new activity
434      */
435     protected function duplicate($course, $cmid) {
436         global $USER;
438         // Do backup.
439         $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cmid, backup::FORMAT_MOODLE,
440                 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
441         $backupid = $bc->get_backupid();
442         $bc->execute_plan();
443         $bc->destroy();
445         // Do restore.
446         $rc = new restore_controller($backupid, $course->id,
447                 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
448         $this->assertTrue($rc->execute_precheck());
449         $rc->execute_plan();
451         // Find cmid.
452         $tasks = $rc->get_plan()->get_tasks();
453         $cmcontext = context_module::instance($cmid);
454         $newcmid = 0;
455         foreach ($tasks as $task) {
456             if (is_subclass_of($task, 'restore_activity_task')) {
457                 if ($task->get_old_contextid() == $cmcontext->id) {
458                     $newcmid = $task->get_moduleid();
459                     break;
460                 }
461             }
462         }
463         $rc->destroy();
464         if (!$newcmid) {
465             throw new coding_exception('Unexpected: failure to find restored cmid');
466         }
467         return $newcmid;
468     }