Merge branch 'MDL-30819_grader_col_highlight' of git://github.com/andyjdavis/moodle
[moodle.git] / lib / phpunit / generatorlib.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  * PHPUnit data generator class
19  *
20  * @package    core
21  * @category   phpunit
22  * @copyright  2012 Petr Skoda {@link http://skodak.org}
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
29 /**
30  * Data generator for unit tests
31  *
32  * @package    core
33  * @category   phpunit
34  * @copyright  2012 Petr Skoda {@link http://skodak.org}
35  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36  */
37 class phpunit_data_generator {
38     protected $usercounter = 0;
39     protected $categorycount = 0;
40     protected $coursecount = 0;
41     protected $scalecount = 0;
43     /** @var array list of plugin generators */
44     protected $generators = array();
46     /** @var array lis of common last names */
47     public $lastnames = array(
48         'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller', 'Davis', 'García', 'Rodríguez', 'Wilson',
49         'Müller', 'Schmidt', 'Schneider', 'Fischer', 'Meyer', 'Weber', 'Schulz', 'Wagner', 'Becker', 'Hoffmann',
50         'Novák', 'Svoboda', 'Novotný', 'Dvořák', 'Černý', 'Procházková', 'Kučerová', 'Veselá', 'Horáková', 'Němcová',
51         'Смирнов', 'Иванов', 'Кузнецов', 'Соколов', 'Попов', 'Лебедева', 'Козлова', 'Новикова', 'Морозова', 'Петрова',
52         '王', '李', '张', '刘', '陈', '楊', '黃', '趙', '吳', '周',
53         '佐藤', '鈴木', '高橋', '田中', '渡辺', '伊藤', '山本', '中村', '小林', '斎藤',
54     );
56     /** @var array lis of common first names */
57     public $firstnames = array(
58         'Jacob', 'Ethan', 'Michael', 'Jayden', 'William', 'Isabella', 'Sophia', 'Emma', 'Olivia', 'Ava',
59         'Lukas', 'Leon', 'Luca', 'Timm', 'Paul', 'Leonie', 'Leah', 'Lena', 'Hanna', 'Laura',
60         'Jakub', 'Jan', 'Tomáš', 'Lukáš', 'Matěj', 'Tereza', 'Eliška', 'Anna', 'Adéla', 'Karolína',
61         'Даниил', 'Максим', 'Артем', 'Иван', 'Александр', 'София', 'Анастасия', 'Дарья', 'Мария', 'Полина',
62         '伟', '伟', '芳', '伟', '秀英', '秀英', '娜', '秀英', '伟', '敏',
63         '翔', '大翔', '拓海', '翔太', '颯太', '陽菜', 'さくら', '美咲', '葵', '美羽',
64     );
66     public $loremipsum = <<<EOD
67 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla non arcu lacinia neque faucibus fringilla. Vivamus porttitor turpis ac leo. Integer in sapien. Nullam eget nisl. Aliquam erat volutpat. Cras elementum. Mauris suscipit, ligula sit amet pharetra semper, nibh ante cursus purus, vel sagittis velit mauris vel metus. Integer malesuada. Nullam lectus justo, vulputate eget mollis sed, tempor sed magna. Mauris elementum mauris vitae tortor. Aliquam erat volutpat.
68 Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Pellentesque ipsum. Cras pede libero, dapibus nec, pretium sit amet, tempor quis. Aliquam ante. Proin in tellus sit amet nibh dignissim sagittis. Vivamus porttitor turpis ac leo. Duis bibendum, lectus ut viverra rhoncus, dolor nunc faucibus libero, eget facilisis enim ipsum id lacus. In sem justo, commodo ut, suscipit at, pharetra vitae, orci. Aliquam erat volutpat. Nulla est.
69 Vivamus luctus egestas leo. Aenean fermentum risus id tortor. Mauris dictum facilisis augue. Aliquam erat volutpat. Aliquam ornare wisi eu metus. Aliquam id dolor. Duis condimentum augue id magna semper rutrum. Donec iaculis gravida nulla. Pellentesque ipsum. Etiam dictum tincidunt diam. Quisque tincidunt scelerisque libero. Etiam egestas wisi a erat.
70 Integer lacinia. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris tincidunt sem sed arcu. Nullam feugiat, turpis at pulvinar vulputate, erat libero tristique tellus, nec bibendum odio risus sit amet ante. Aliquam id dolor. Maecenas sollicitudin. Et harum quidem rerum facilis est et expedita distinctio. Mauris suscipit, ligula sit amet pharetra semper, nibh ante cursus purus, vel sagittis velit mauris vel metus. Nullam dapibus fermentum ipsum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Pellentesque sapien. Duis risus. Mauris elementum mauris vitae tortor. Suspendisse nisl. Integer rutrum, orci vestibulum ullamcorper ultricies, lacus quam ultricies odio, vitae placerat pede sem sit amet enim.
71 In laoreet, magna id viverra tincidunt, sem odio bibendum justo, vel imperdiet sapien wisi sed libero. Proin pede metus, vulputate nec, fermentum fringilla, vehicula vitae, justo. Nullam justo enim, consectetuer nec, ullamcorper ac, vestibulum in, elit. Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? Maecenas lorem. Etiam posuere lacus quis dolor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Curabitur ligula sapien, pulvinar a vestibulum quis, facilisis vel sapien. Nam sed tellus id magna elementum tincidunt. Suspendisse nisl. Vivamus luctus egestas leo. Nulla non arcu lacinia neque faucibus fringilla. Etiam dui sem, fermentum vitae, sagittis id, malesuada in, quam. Etiam dictum tincidunt diam. Etiam commodo dui eget wisi. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Proin pede metus, vulputate nec, fermentum fringilla, vehicula vitae, justo. Duis ante orci, molestie vitae vehicula venenatis, tincidunt ac pede. Pellentesque sapien.
72 EOD;
74     /**
75      * To be called from data reset code only,
76      * do not use in tests.
77      * @return void
78      */
79     public function reset() {
80         $this->usercounter = 0;
81         $this->categorycount = 0;
82         $this->coursecount = 0;
83         $this->scalecount = 0;
85         foreach($this->generators as $generator) {
86             $generator->reset();
87         }
88     }
90     /**
91      * Return generator for given plugin
92      * @param string $component
93      * @return mixed plugin data generator
94      */
95     public function get_plugin_generator($component) {
96         list($type, $plugin) = normalize_component($component);
98         if ($type !== 'mod' and $type !== 'block') {
99             throw new coding_exception("Plugin type $type does not support generators yet");
100         }
102         $dir = get_plugin_directory($type, $plugin);
104         if (!isset($this->generators[$type.'_'.$plugin])) {
105             $lib = "$dir/tests/generator/lib.php";
106             if (!include_once($lib)) {
107                 throw new coding_exception("Plugin $component does not support data generator, missing tests/generator/lib");
108             }
109             $classname = $type.'_'.$plugin.'_generator';
110             $this->generators[$type.'_'.$plugin] = new $classname($this);
111         }
113         return $this->generators[$type.'_'.$plugin];
114     }
116     /**
117      * Create a test user
118      * @param array|stdClass $record
119      * @param array $options
120      * @return stdClass user record
121      */
122     public function create_user($record=null, array $options=null) {
123         global $DB, $CFG;
125         $this->usercounter++;
126         $i = $this->usercounter;
128         $record = (array)$record;
130         if (!isset($record['auth'])) {
131             $record['auth'] = 'manual';
132         }
134         if (!isset($record['firstname']) and !isset($record['lastname'])) {
135             $country = rand(0, 5);
136             $firstname = rand(0, 4);
137             $lastname = rand(0, 4);
138             $female = rand(0, 1);
139             $record['firstname'] = $this->firstnames[($country*10) + $firstname + ($female*5)];
140             $record['lastname'] = $this->lastnames[($country*10) + $lastname + ($female*5)];
142         } else if (!isset($record['firstname'])) {
143             $record['firstname'] = 'Firstname'.$i;
145         } else if (!isset($record['lastname'])) {
146             $record['lastname'] = 'Lastname'.$i;
147         }
149         if (!isset($record['idnumber'])) {
150             $record['idnumber'] = '';
151         }
153         if (!isset($record['mnethostid'])) {
154             $record['mnethostid'] = $CFG->mnet_localhost_id;
155         }
157         if (!isset($record['username'])) {
158             $record['username'] = textlib::strtolower($record['firstname']).textlib::strtolower($record['lastname']);
159             while ($DB->record_exists('user', array('username'=>$record['username'], 'mnethostid'=>$record['mnethostid']))) {
160                 $record['username'] = $record['username'].'_'.$i;
161             }
162         }
164         if (!isset($record['password'])) {
165             $record['password'] = 'lala';
166         }
168         if (!isset($record['email'])) {
169             $record['email'] = $record['username'].'@example.com';
170         }
172         if (!isset($record['confirmed'])) {
173             $record['confirmed'] = 1;
174         }
176         if (!isset($record['lang'])) {
177             $record['lang'] = 'en';
178         }
180         if (!isset($record['maildisplay'])) {
181             $record['maildisplay'] = 1;
182         }
184         if (!isset($record['deleted'])) {
185             $record['deleted'] = 0;
186         }
188         $record['timecreated'] = time();
189         $record['timemodified'] = $record['timecreated'];
190         $record['lastip'] = '0.0.0.0';
192         $record['password'] = hash_internal_user_password($record['password']);
194         if ($record['deleted']) {
195             $delname = $record['email'].'.'.time();
196             while ($DB->record_exists('user', array('username'=>$delname))) {
197                 $delname++;
198             }
199             $record['idnumber'] = '';
200             $record['email']    = md5($record['username']);
201             $record['username'] = $delname;
202         }
204         $userid = $DB->insert_record('user', $record);
206         if (!$record['deleted']) {
207             context_user::instance($userid);
208         }
210         return $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
211     }
213     /**
214      * Create a test course category
215      * @param array|stdClass $record
216      * @param array $options
217      * @return stdClass course category record
218      */
219     function create_category($record=null, array $options=null) {
220         global $DB, $CFG;
221         require_once("$CFG->dirroot/course/lib.php");
223         $this->categorycount++;
224         $i = $this->categorycount;
226         $record = (array)$record;
228         if (!isset($record['name'])) {
229             $record['name'] = 'Course category '.$i;
230         }
232         if (!isset($record['idnumber'])) {
233             $record['idnumber'] = '';
234         }
236         if (!isset($record['description'])) {
237             $record['description'] = "Test course category $i\n$this->loremipsum";
238         }
240         if (!isset($record['descriptionformat'])) {
241             $record['description'] = FORMAT_MOODLE;
242         }
244         if (!isset($record['parent'])) {
245             $record['descriptionformat'] = 0;
246         }
248         if (empty($record['parent'])) {
249             $parent = new stdClass();
250             $parent->path = '';
251             $parent->depth = 0;
252         } else {
253             $parent = $DB->get_record('course_categories', array('id'=>$record['parent']), '*', MUST_EXIST);
254         }
255         $record['depth'] = $parent->depth+1;
257         $record['sortorder'] = 0;
258         $record['timemodified'] = time();
259         $record['timecreated'] = $record['timemodified'];
261         $catid = $DB->insert_record('course_categories', $record);
262         $path = $parent->path . '/' . $catid;
263         $DB->set_field('course_categories', 'path', $path, array('id'=>$catid));
264         context_coursecat::instance($catid);
266         fix_course_sortorder();
268         return $DB->get_record('course_categories', array('id'=>$catid), '*', MUST_EXIST);
269     }
271     /**
272      * Create a test course
273      * @param array|stdClass $record
274      * @param array $options with keys:
275      *      'createsections'=>bool precreate all sections
276      * @return stdClass course record
277      */
278     function create_course($record=null, array $options=null) {
279         global $DB, $CFG;
280         require_once("$CFG->dirroot/course/lib.php");
282         $this->coursecount++;
283         $i = $this->coursecount;
285         $record = (array)$record;
287         if (!isset($record['fullname'])) {
288             $record['fullname'] = 'Test course '.$i;
289         }
291         if (!isset($record['shortname'])) {
292             $record['shortname'] = 'tc_'.$i;
293         }
295         if (!isset($record['idnumber'])) {
296             $record['idnumber'] = '';
297         }
299         if (!isset($record['format'])) {
300             $record['format'] = 'topics';
301         }
303         if (!isset($record['newsitems'])) {
304             $record['newsitems'] = 0;
305         }
307         if (!isset($record['numsections'])) {
308             $record['numsections'] = 5;
309         }
311         if (!isset($record['description'])) {
312             $record['description'] = "Test course $i\n$this->loremipsum";
313         }
315         if (!isset($record['descriptionformat'])) {
316             $record['description'] = FORMAT_MOODLE;
317         }
319         if (!isset($record['category'])) {
320             $record['category'] = $DB->get_field_select('course_categories', "MIN(id)", "parent=0");
321         }
323         $course = create_course((object)$record);
324         context_course::instance($course->id);
326         if (!empty($options['createsections'])) {
327             for($i=1; $i<$record['numsections']; $i++) {
328                 self::create_course_section(array('course'=>$course->id, 'section'=>$i));
329             }
330         }
332         return $course;
333     }
335     /**
336      * Create course section if does not exist yet
337      * @param mixed $record
338      * @param array|null $options
339      * @return stdClass
340      * @throws coding_exception
341      */
342     public function create_course_section($record = null, array $options = null) {
343         global $DB;
345         $record = (array)$record;
347         if (empty($record['course'])) {
348             throw new coding_exception('course must be present in phpunit_util::create_course_section() $record');
349         }
351         if (!isset($record['section'])) {
352             throw new coding_exception('section must be present in phpunit_util::create_course_section() $record');
353         }
355         if (!isset($record['name'])) {
356             $record['name'] = '';
357         }
359         if (!isset($record['summary'])) {
360             $record['summary'] = '';
361         }
363         if (!isset($record['summaryformat'])) {
364             $record['summaryformat'] = FORMAT_MOODLE;
365         }
367         if ($section = $DB->get_record('course_sections', array('course'=>$record['course'], 'section'=>$record['section']))) {
368             return $section;
369         }
371         $section = new stdClass();
372         $section->course        = $record['course'];
373         $section->section       = $record['section'];
374         $section->name          = $record['name'];
375         $section->summary       = $record['summary'];
376         $section->summaryformat = $record['summaryformat'];
377         $id = $DB->insert_record('course_sections', $section);
379         return $DB->get_record('course_sections', array('id'=>$id));
380     }
382     /**
383      * Create a test block
384      * @param string $blockname
385      * @param array|stdClass $record
386      * @param array $options
387      * @return stdClass block instance record
388      */
389     public function create_block($blockname, $record=null, array $options=null) {
390         $generator = $this->get_plugin_generator('block_'.$blockname);
391         return $generator->create_instance($record, $options);
392     }
394     /**
395      * Create a test module
396      * @param string $modulename
397      * @param array|stdClass $record
398      * @param array $options
399      * @return stdClass activity record
400      */
401     public function create_module($modulename, $record=null, array $options=null) {
402         $generator = $this->get_plugin_generator('mod_'.$modulename);
403         return $generator->create_instance($record, $options);
404     }
406     /**
407      * Create a test scale
408      * @param array|stdClass $record
409      * @param array $options
410      * @return stdClass block instance record
411      */
412     public function create_scale($record=null, array $options=null) {
413         global $DB;
415         $this->scalecount++;
416         $i = $this->scalecount;
418         $record = (array)$record;
420         if (!isset($record['name'])) {
421             $record['name'] = 'Test scale '.$i;
422         }
424         if (!isset($record['scale'])) {
425             $record['scale'] = 'A,B,C,D,F';
426         }
428         if (!isset($record['courseid'])) {
429             $record['courseid'] = 0;
430         }
432         if (!isset($record['userid'])) {
433             $record['userid'] = 0;
434         }
436         if (!isset($record['description'])) {
437             $record['description'] = 'Test scale description '.$i;
438         }
440         if (!isset($record['descriptionformat'])) {
441             $record['descriptionformat'] = FORMAT_MOODLE;
442         }
444         $record['timemodified'] = time();
446         if (isset($record['id'])) {
447             $DB->import_record('scale', $record);
448             $DB->get_manager()->reset_sequence('scale');
449             $id = $record['id'];
450         } else {
451             $id = $DB->insert_record('scale', $record);
452         }
454         return $DB->get_record('scale', array('id'=>$id), '*', MUST_EXIST);
455     }
459 /**
460  * Module generator base class.
461  *
462  * Extend in mod/xxxx/tests/generator/lib.php as class mod_xxxx_generator.
463  *
464  * @package    core
465  * @category   phpunit
466  * @copyright  2012 Petr Skoda {@link http://skodak.org}
467  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
468  */
469 abstract class phpunit_module_generator {
470     /** @var phpunit_data_generator@var  */
471     protected $datagenerator;
473     /** @var number of created instances */
474     protected $instancecount = 0;
476     public function __construct(phpunit_data_generator $datagenerator) {
477         $this->datagenerator = $datagenerator;
478     }
480     /**
481      * To be called from data reset code only,
482      * do not use in tests.
483      * @return void
484      */
485     public function reset() {
486         $this->instancecount = 0;
487     }
489     /**
490      * Returns module name
491      * @return string name of module that this class describes
492      * @throws coding_exception if class invalid
493      */
494     public function get_modulename() {
495         $matches = null;
496         if (!preg_match('/^mod_([a-z0-9]+)_generator$/', get_class($this), $matches)) {
497             throw new coding_exception('Invalid module generator class name: '.get_class($this));
498         }
500         if (empty($matches[1])) {
501             throw new coding_exception('Invalid module generator class name: '.get_class($this));
502         }
503         return $matches[1];
504     }
506     /**
507      * Create course module and link it to course
508      * @param stdClass $instance
509      * @param array $options: section, visible
510      * @return stdClass $cm instance
511      */
512     protected function create_course_module(stdClass $instance, array $options) {
513         global $DB, $CFG;
514         require_once("$CFG->dirroot/course/lib.php");
516         $modulename = $this->get_modulename();
518         $cm = new stdClass();
519         $cm->course             = $instance->course;
520         $cm->module             = $DB->get_field('modules', 'id', array('name'=>$modulename));
521         $cm->instance           = $instance->id;
522         $cm->section            = isset($options['section']) ? $options['section'] : 0;
523         $cm->idnumber           = isset($options['idnumber']) ? $options['idnumber'] : 0;
524         $cm->added              = time();
526         $columns = $DB->get_columns('course_modules');
527         foreach ($options as $key=>$value) {
528             if ($key === 'id' or !isset($columns[$key])) {
529                 continue;
530             }
531             if (property_exists($cm, $key)) {
532                 continue;
533             }
534             $cm->$key = $value;
535         }
537         $cm->id = $DB->insert_record('course_modules', $cm);
538         $cm->coursemodule = $cm->id;
540         add_mod_to_section($cm);
542         $cm = get_coursemodule_from_id($modulename, $cm->id, $cm->course, true, MUST_EXIST);
544         context_module::instance($cm->id);
546         return $cm;
547     }
549     /**
550      * Create a test module
551      * @param array|stdClass $record
552      * @param array $options
553      * @return stdClass activity record
554      */
555     abstract public function create_instance($record = null, array $options = null);
559 /**
560  * Block generator base class.
561  *
562  * Extend in blocks/xxxx/tests/generator/lib.php as class block_xxxx_generator.
563  *
564  * @package    core
565  * @category   phpunit
566  * @copyright  2012 Petr Skoda {@link http://skodak.org}
567  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
568  */
569 abstract class phpunit_block_generator {
570     /** @var phpunit_data_generator@var  */
571     protected $datagenerator;
573     /** @var number of created instances */
574     protected $instancecount = 0;
576     public function __construct(phpunit_data_generator $datagenerator) {
577         $this->datagenerator = $datagenerator;
578     }
580     /**
581      * To be called from data reset code only,
582      * do not use in tests.
583      * @return void
584      */
585     public function reset() {
586         $this->instancecount = 0;
587     }
589     /**
590      * Returns block name
591      * @return string name of block that this class describes
592      * @throws coding_exception if class invalid
593      */
594     public function get_blockname() {
595         $matches = null;
596         if (!preg_match('/^block_([a-z0-9_]+)_generator$/', get_class($this), $matches)) {
597             throw new coding_exception('Invalid block generator class name: '.get_class($this));
598         }
600         if (empty($matches[1])) {
601             throw new coding_exception('Invalid block generator class name: '.get_class($this));
602         }
603         return $matches[1];
604     }
606     /**
607      * Fill in record defaults
608      * @param stdClass $record
609      * @return stdClass
610      */
611     protected function prepare_record(stdClass $record) {
612         $record->blockname = $this->get_blockname();
613         if (!isset($record->parentcontextid)) {
614             $record->parentcontextid = context_system::instance()->id;
615         }
616         if (!isset($record->showinsubcontexts)) {
617             $record->showinsubcontexts = 1;
618         }
619         if (!isset($record->pagetypepattern)) {
620             $record->pagetypepattern = '';
621         }
622         if (!isset($record->subpagepattern)) {
623             $record->subpagepattern = null;
624         }
625         if (!isset($record->defaultregion)) {
626             $record->defaultregion = '';
627         }
628         if (!isset($record->defaultweight)) {
629             $record->defaultweight = '';
630         }
631         if (!isset($record->configdata)) {
632             $record->configdata = null;
633         }
634         return $record;
635     }
637     /**
638      * Create a test block
639      * @param array|stdClass $record
640      * @param array $options
641      * @return stdClass activity record
642      */
643     abstract public function create_instance($record = null, array $options = null);