ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç generator/component_generator_base.php000064400000013532152267206310014315 0ustar00. /** * Component generator base class. * * @package core * @category test * @copyright 2013 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Component generator base class. * * Extend in path/to/component/tests/generator/lib.php as * class type_plugin_generator extends component_generator_base * Note that there are more specific classes to extend for mods and blocks. * * @copyright 2013 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class component_generator_base { /** * @var testing_data_generator */ protected $datagenerator; /** * Constructor. * @param testing_data_generator $datagenerator */ public function __construct(testing_data_generator $datagenerator) { $this->datagenerator = $datagenerator; } /** * To be called from data reset code only, * do not use in tests. * @return void */ public function reset() { } /** * Set the current user during data generation. * * This should be avoided wherever possible, but in some situations underlying code will insert data as the current * user. * * @param stdClass $user */ protected function set_user(?stdClass $user = null): void { global $CFG, $DB; if ($user === null) { $user = (object) [ 'id' => 0, 'mnethostid' => $CFG->mnet_localhost_id, ]; } else { $user = clone($user); unset($user->description); unset($user->access); unset($user->preference); } // Ensure session is empty, as it may contain caches and user-specific info. \core\session\manager::init_empty_session(); \core\session\manager::set_user($user); } /** * Update the instance record, inserting any files that are referenced. * * @param stdClass $instance The instance record of the already-created record * @param stdClass $record The data passed in to create the instance * @param string $table The table that the data exists in * @param context $context The context of the instance * @param string $component The component of the owning plugin * @param string $filearea The name of the file area * @param int $targetitemid The itemid to use when saving the files * @return stdClass The updated instance record */ protected function insert_files( stdClass $instance, stdClass $record, string $table, context $context, string $component, string $filearea, int $targetitemid ): stdClass { global $CFG, $DB, $USER; $fieldname = "[[files::{$filearea}]]"; if (!isset($record->$fieldname)) { return $instance; } preg_match('/\[\[files::(.*)\]\]/', $fieldname, $matches); if (empty($matches[1])) { throw new coding_exception('Invalid file field name: ' . $fieldname); } $referencedfieldname = trim($matches[1]); if (!isset($record->$referencedfieldname)) { throw new coding_exception("File field '{$fieldname}' references non-existent field '{$referencedfieldname}'"); } $fs = get_file_storage(); $itemid = file_get_unused_draft_itemid(); $itemidfieldname = "{$referencedfieldname}[itemid]"; $record->$itemidfieldname = $itemid; $filenames = explode(',', $record->$fieldname); foreach ($filenames as $filename) { $filename = trim($filename); if (!$filename) { continue; } $explodedfilename = explode('::', $filename, 3); if (count($explodedfilename) === 2) { [$sourcefile, $targetfile] = $explodedfilename; $user = $USER; } else { [$sourcefile, $targetfile, $username] = $explodedfilename; $user = \core_user::get_user_by_username($username); } $filepath = "{$CFG->dirroot}/{$sourcefile}"; if (!file_exists($filepath)) { throw new coding_exception("File '{$filepath}' does not exist"); } $filerecord = [ 'userid' => $user->id, 'contextid' => context_user::instance($user->id)->id, 'component' => 'user', 'filearea' => 'draft', 'itemid' => $itemid, 'filepath' => '/' . dirname($targetfile), 'filename' => basename($targetfile), ]; $fs->create_file_from_pathname($filerecord, $filepath); } $olduser = $USER; $this->set_user($user); $instance->$referencedfieldname = file_save_draft_area_files( $itemid, $context->id, $component, $referencedfieldname, $targetitemid, null, $instance->$referencedfieldname ); $this->set_user($olduser); $DB->update_record($table, $instance); return $instance; } } generator/lib.php000064400000002406152267206310010017 0ustar00. /** * Adds data generator support * * @package core * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: MOODLE_INTERNAL is not verified here because we load this before setup.php! require_once(__DIR__.'/data_generator.php'); require_once(__DIR__.'/component_generator_base.php'); require_once(__DIR__.'/module_generator.php'); require_once(__DIR__.'/block_generator.php'); require_once(__DIR__.'/default_block_generator.php'); require_once(__DIR__.'/repository_generator.php'); generator/data_generator.php000064400000165440152267206310012240 0ustar00. defined('MOODLE_INTERNAL') || die(); /** * Data generator class for unit tests and other tools that need to create fake test sites. * * @package core * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class testing_data_generator { /** @var int The number of grade categories created */ protected $gradecategorycounter = 0; /** @var int The number of grade items created */ protected $gradeitemcounter = 0; /** @var int The number of grade outcomes created */ protected $gradeoutcomecounter = 0; protected $usercounter = 0; protected $categorycount = 0; protected $cohortcount = 0; protected $coursecount = 0; protected $scalecount = 0; protected $groupcount = 0; protected $groupingcount = 0; protected $rolecount = 0; protected $tagcount = 0; /** @var array list of plugin generators */ protected $generators = array(); /** @var array lis of common last names */ public $lastnames = array( 'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller', 'Davis', 'García', 'Rodríguez', 'Wilson', 'Müller', 'Schmidt', 'Schneider', 'Fischer', 'Meyer', 'Weber', 'Schulz', 'Wagner', 'Becker', 'Hoffmann', 'Novák', 'Svoboda', 'Novotný', 'Dvořák', 'Černý', 'Procházková', 'Kučerová', 'Veselá', 'Horáková', 'Němcová', 'Смирнов', 'Иванов', 'Кузнецов', 'Соколов', 'Попов', 'Лебедева', 'Козлова', 'Новикова', 'Морозова', 'Петрова', '王', '李', '张', '刘', '陈', '楊', '黃', '趙', '吳', '周', '佐藤', '鈴木', '高橋', '田中', '渡辺', '伊藤', '山本', '中村', '小林', '斎藤', ); /** @var array lis of common first names */ public $firstnames = array( 'Jacob', 'Ethan', 'Michael', 'Jayden', 'William', 'Isabella', 'Sophia', 'Emma', 'Olivia', 'Ava', 'Lukas', 'Leon', 'Luca', 'Timm', 'Paul', 'Leonie', 'Leah', 'Lena', 'Hanna', 'Laura', 'Jakub', 'Jan', 'Tomáš', 'Lukáš', 'Matěj', 'Tereza', 'Eliška', 'Anna', 'Adéla', 'Karolína', 'Даниил', 'Максим', 'Артем', 'Иван', 'Александр', 'София', 'Анастасия', 'Дарья', 'Мария', 'Полина', '伟', '伟', '芳', '伟', '秀英', '秀英', '娜', '秀英', '伟', '敏', '翔', '大翔', '拓海', '翔太', '颯太', '陽菜', 'さくら', '美咲', '葵', '美羽', ); public $loremipsum = <<gradecategorycounter = 0; $this->gradeitemcounter = 0; $this->gradeoutcomecounter = 0; $this->usercounter = 0; $this->categorycount = 0; $this->cohortcount = 0; $this->coursecount = 0; $this->scalecount = 0; $this->groupcount = 0; $this->groupingcount = 0; $this->rolecount = 0; $this->tagcount = 0; foreach ($this->generators as $generator) { $generator->reset(); } } /** * Return generator for given plugin or component. * @param string $component the component name, e.g. 'mod_forum' or 'core_question'. * @return component_generator_base or rather an instance of the appropriate subclass. */ public function get_plugin_generator($component) { // Note: This global is included so that generator have access to it. // CFG is widely used in require statements. global $CFG; list($type, $plugin) = core_component::normalize_component($component); $cleancomponent = $type . '_' . $plugin; if ($cleancomponent != $component) { debugging("Please specify the component you want a generator for as " . "{$cleancomponent}, not {$component}.", DEBUG_DEVELOPER); $component = $cleancomponent; } if (isset($this->generators[$component])) { return $this->generators[$component]; } $dir = core_component::get_component_directory($component); $lib = $dir . '/tests/generator/lib.php'; if (!$dir || !is_readable($lib)) { $this->generators[$component] = $this->get_default_plugin_generator($component); return $this->generators[$component]; } include_once($lib); $classname = $component . '_generator'; if (class_exists($classname)) { $this->generators[$component] = new $classname($this); } else { $this->generators[$component] = $this->get_default_plugin_generator($component, $classname); } return $this->generators[$component]; } /** * Create a test user * @param array|stdClass $record * @param array $options * @return stdClass user record */ public function create_user($record=null, ?array $options=null) { global $DB, $CFG; require_once($CFG->dirroot.'/user/lib.php'); $this->usercounter++; $i = $this->usercounter; $record = (array)$record; if (!isset($record['auth'])) { $record['auth'] = 'manual'; } if (!isset($record['firstname']) and !isset($record['lastname'])) { $country = rand(0, 5); $firstname = rand(0, 4); $lastname = rand(0, 4); $female = rand(0, 1); $record['firstname'] = $this->firstnames[($country*10) + $firstname + ($female*5)]; $record['lastname'] = $this->lastnames[($country*10) + $lastname + ($female*5)]; } else if (!isset($record['firstname'])) { $record['firstname'] = 'Firstname'.$i; } else if (!isset($record['lastname'])) { $record['lastname'] = 'Lastname'.$i; } if (!isset($record['firstnamephonetic'])) { $firstnamephonetic = rand(0, 59); $record['firstnamephonetic'] = $this->firstnames[$firstnamephonetic]; } if (!isset($record['lastnamephonetic'])) { $lastnamephonetic = rand(0, 59); $record['lastnamephonetic'] = $this->lastnames[$lastnamephonetic]; } if (!isset($record['middlename'])) { $middlename = rand(0, 59); $record['middlename'] = $this->firstnames[$middlename]; } if (!isset($record['alternatename'])) { $alternatename = rand(0, 59); $record['alternatename'] = $this->firstnames[$alternatename]; } if (!isset($record['idnumber'])) { $record['idnumber'] = ''; } if (!isset($record['mnethostid'])) { $record['mnethostid'] = $CFG->mnet_localhost_id; } if (!isset($record['username'])) { $record['username'] = 'username'.$i; $j = 2; while ($DB->record_exists('user', array('username'=>$record['username'], 'mnethostid'=>$record['mnethostid']))) { $record['username'] = 'username'.$i.'_'.$j; $j++; } } if (isset($record['password'])) { $record['password'] = hash_internal_user_password($record['password']); } if (!isset($record['email'])) { $record['email'] = $record['username'].'@example.com'; } if (!isset($record['confirmed'])) { $record['confirmed'] = 1; } if (!isset($record['lastip'])) { $record['lastip'] = '0.0.0.0'; } $tobedeleted = !empty($record['deleted']); unset($record['deleted']); $userid = user_create_user($record, false, false); if ($extrafields = array_intersect_key($record, ['password' => 1, 'timecreated' => 1])) { $DB->update_record('user', ['id' => $userid] + $extrafields); } if (!$tobedeleted) { // All new not deleted users must have a favourite self-conversation. $selfconversation = \core_message\api::create_conversation( \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, [$userid] ); \core_message\api::set_favourite_conversation($selfconversation->id, $userid); // Save custom profile fields data. $hasprofilefields = array_filter($record, function($key){ return strpos($key, 'profile_field_') === 0; }, ARRAY_FILTER_USE_KEY); if ($hasprofilefields) { require_once($CFG->dirroot.'/user/profile/lib.php'); $usernew = (object)(['id' => $userid] + $record); profile_save_data($usernew); } } $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST); if (!$tobedeleted && isset($record['interests'])) { require_once($CFG->dirroot . '/user/editlib.php'); if (!is_array($record['interests'])) { $record['interests'] = preg_split('/\s*,\s*/', trim($record['interests']), -1, PREG_SPLIT_NO_EMPTY); } useredit_update_interests($user, $record['interests']); } \core\event\user_created::create_from_userid($userid)->trigger(); if ($tobedeleted) { delete_user($user); $user = $DB->get_record('user', array('id' => $userid)); } return $user; } /** * Create a test course category * @param array|stdClass $record * @param array $options * @return core_course_category course category record */ public function create_category($record=null, ?array $options=null) { $this->categorycount++; $i = $this->categorycount; $record = (array)$record; if (!isset($record['name'])) { $record['name'] = 'Course category '.$i; } if (!isset($record['description'])) { $record['description'] = "Test course category $i\n$this->loremipsum"; } if (!isset($record['idnumber'])) { $record['idnumber'] = ''; } return core_course_category::create($record); } /** * Create test cohort. * @param array|stdClass $record * @param array $options * @return stdClass cohort record */ public function create_cohort($record=null, ?array $options=null) { global $DB, $CFG; require_once("$CFG->dirroot/cohort/lib.php"); $this->cohortcount++; $i = $this->cohortcount; $record = (array)$record; if (!isset($record['contextid'])) { $record['contextid'] = context_system::instance()->id; } if (!isset($record['name'])) { $record['name'] = 'Cohort '.$i; } if (!isset($record['idnumber'])) { $record['idnumber'] = ''; } if (!isset($record['description'])) { $record['description'] = "Description for '{$record['name']}' \n$this->loremipsum"; } if (!isset($record['descriptionformat'])) { $record['descriptionformat'] = FORMAT_MOODLE; } if (!isset($record['visible'])) { $record['visible'] = 1; } if (!isset($record['component'])) { $record['component'] = ''; } $id = cohort_add_cohort((object)$record); return $DB->get_record('cohort', array('id'=>$id), '*', MUST_EXIST); } /** * Create a test course * @param array|stdClass $record Apart from the course information, the following can be also set: * 'initsections' => bool for section name initialization, renaming them to "Section X". Default value is 0 (false). * @param array $options with keys: * 'createsections' => bool precreate all sections * @return stdClass course record */ public function create_course($record=null, ?array $options=null) { global $DB, $CFG; require_once("$CFG->dirroot/course/lib.php"); $this->coursecount++; $i = $this->coursecount; $record = (array)$record; if (!isset($record['fullname'])) { $record['fullname'] = 'Test course '.$i; } if (!isset($record['shortname'])) { $record['shortname'] = 'tc_'.$i; } if (!isset($record['idnumber'])) { $record['idnumber'] = ''; } if (!isset($record['format'])) { $record['format'] = 'topics'; } if (!isset($record['newsitems'])) { $record['newsitems'] = 0; } if (!isset($record['numsections'])) { $record['numsections'] = 5; } if (!isset($record['summary'])) { $record['summary'] = "Test course $i\n$this->loremipsum"; } if (!isset($record['summaryformat'])) { $record['summaryformat'] = FORMAT_MOODLE; } if (!isset($record['category'])) { $record['category'] = $DB->get_field_select('course_categories', "MIN(id)", "parent=0"); } if (!isset($record['startdate'])) { $record['startdate'] = usergetmidnight(time()); } if (isset($record['tags']) && !is_array($record['tags'])) { $record['tags'] = preg_split('/\s*,\s*/', trim($record['tags']), -1, PREG_SPLIT_NO_EMPTY); } if (!empty($options['createsections']) && empty($record['numsections'])) { // Since Moodle 3.3 function create_course() automatically creates sections if numsections is specified. // For BC if 'createsections' is given but 'numsections' is not, assume the default value from config. $record['numsections'] = get_config('moodlecourse', 'numsections'); } if (!empty($record['customfields'])) { foreach ($record['customfields'] as $field) { $record['customfield_'.$field['shortname']] = $field['value']; } } $initsections = !empty($record['initsections']); unset($record['initsections']); $course = create_course((object)$record); if ($initsections) { $this->init_sections($course); } context_course::instance($course->id); return $course; } /** * Initializes sections for a specified course, such as configuring section names for courses using 'Section X'. * * @param stdClass $course The course object. */ private function init_sections(stdClass $course): void { global $DB; $sections = $DB->get_records('course_sections', ['course' => $course->id], 'section'); foreach ($sections as $section) { if ($section->section != 0) { $DB->set_field( table: 'course_sections', newfield: 'name', newvalue: get_string('section', 'core') . ' ' . $section->section, conditions: [ 'id' => $section->id, ], ); } } } /** * Create course section if does not exist yet * @param array|stdClass $record must contain 'course' and 'section' attributes * @param array|null $options * @return section_info * @throws coding_exception */ public function create_course_section($record = null, ?array $options = null) { global $DB; $record = (array)$record; if (empty($record['course'])) { throw new coding_exception('course must be present in testing_data_generator::create_course_section() $record'); } if (!isset($record['section'])) { throw new coding_exception('section must be present in testing_data_generator::create_course_section() $record'); } course_create_sections_if_missing($record['course'], $record['section']); return get_fast_modinfo($record['course'])->get_section_info($record['section']); } /** * Create a test block. * * The $record passed in becomes the basis for the new row added to the * block_instances table. You only need to supply the values of interest. * Any missing values have sensible defaults filled in, and ->blockname will be set based on $blockname. * * The $options array provides additional data, not directly related to what * will be inserted in the block_instance table, which may affect the block * that is created. The meanings of any data passed here depends on the particular * type of block being created. * * @param string $blockname the type of block to create. E.g. 'html'. * @param array|stdClass $record forms the basis for the entry to be inserted in the block_instances table. * @param array $options further, block-specific options to control how the block is created. * @return stdClass new block_instance record. */ public function create_block($blockname, $record=null, array $options=array()) { $generator = $this->get_plugin_generator('block_'.$blockname); return $generator->create_instance($record, $options); } /** * Create a test activity module. * * The $record should contain the same data that you would call from * ->get_data() when the mod_[type]_mod_form is submitted, except that you * only need to supply values of interest. The only required value is * 'course'. Any missing values will have a sensible default supplied. * * The $options array provides additional data, not directly related to what * would come back from the module edit settings form, which may affect the activity * that is created. The meanings of any data passed here depends on the particular * type of activity being created. * * @param string $modulename the type of activity to create. E.g. 'forum' or 'quiz'. * @param array|stdClass $record data, as if from the module edit settings form. * @param array $options additional data that may affect how the module is created. * @return stdClass activity record new new record that was just inserted in the table * like 'forum' or 'quiz', with a ->cmid field added. */ public function create_module($modulename, $record=null, ?array $options=null) { $generator = $this->get_plugin_generator('mod_'.$modulename); return $generator->create_instance($record, $options); } /** * Create a test group for the specified course * * $record should be either an array or a stdClass containing infomation about the group to create. * At the very least it needs to contain courseid. * Default values are added for name, description, and descriptionformat if they are not present. * * This function calls groups_create_group() to create the group within the database. * @see groups_create_group * @param array|stdClass $record * @return stdClass group record */ public function create_group($record) { global $DB, $CFG; require_once($CFG->dirroot . '/group/lib.php'); $this->groupcount++; $i = str_pad($this->groupcount, 4, '0', STR_PAD_LEFT); $record = (array)$record; if (empty($record['courseid'])) { throw new coding_exception('courseid must be present in testing_data_generator::create_group() $record'); } if (!isset($record['name'])) { $record['name'] = 'group-' . $i; } if (!isset($record['description'])) { $record['description'] = "Test Group $i\n{$this->loremipsum}"; } if (!isset($record['descriptionformat'])) { $record['descriptionformat'] = FORMAT_MOODLE; } if (!isset($record['visibility'])) { $record['visibility'] = GROUPS_VISIBILITY_ALL; } if (!isset($record['participation'])) { $record['participation'] = true; } $id = groups_create_group((object)$record); // Allow tests to set group pictures. if (!empty($record['picturepath'])) { require_once($CFG->dirroot . '/lib/gdlib.php'); $grouppicture = process_new_icon(\context_course::instance($record['courseid']), 'group', 'icon', $id, $record['picturepath']); $DB->set_field('groups', 'picture', $grouppicture, ['id' => $id]); // Invalidate the group data as we've updated the group record. cache_helper::invalidate_by_definition('core', 'groupdata', array(), [$record['courseid']]); } return $DB->get_record('groups', array('id'=>$id)); } /** * Create a test group member * @param array|stdClass $record * @throws coding_exception * @return boolean */ public function create_group_member($record) { global $DB, $CFG; require_once($CFG->dirroot . '/group/lib.php'); $record = (array)$record; if (empty($record['userid'])) { throw new coding_exception('user must be present in testing_util::create_group_member() $record'); } if (!isset($record['groupid'])) { throw new coding_exception('group must be present in testing_util::create_group_member() $record'); } if (!isset($record['component'])) { $record['component'] = null; } if (!isset($record['itemid'])) { $record['itemid'] = 0; } return groups_add_member($record['groupid'], $record['userid'], $record['component'], $record['itemid']); } /** * Create a test grouping for the specified course * * $record should be either an array or a stdClass containing infomation about the grouping to create. * At the very least it needs to contain courseid. * Default values are added for name, description, and descriptionformat if they are not present. * * This function calls groups_create_grouping() to create the grouping within the database. * @see groups_create_grouping * @param array|stdClass $record * @return stdClass grouping record */ public function create_grouping($record) { global $DB, $CFG; require_once($CFG->dirroot . '/group/lib.php'); $this->groupingcount++; $i = $this->groupingcount; $record = (array)$record; if (empty($record['courseid'])) { throw new coding_exception('courseid must be present in testing_data_generator::create_grouping() $record'); } if (!isset($record['name'])) { $record['name'] = 'grouping-' . $i; } if (!isset($record['description'])) { $record['description'] = "Test Grouping $i\n{$this->loremipsum}"; } if (!isset($record['descriptionformat'])) { $record['descriptionformat'] = FORMAT_MOODLE; } $id = groups_create_grouping((object)$record); return $DB->get_record('groupings', array('id'=>$id)); } /** * Create a test grouping group * @param array|stdClass $record * @throws coding_exception * @return boolean */ public function create_grouping_group($record) { global $DB, $CFG; require_once($CFG->dirroot . '/group/lib.php'); $record = (array)$record; if (empty($record['groupingid'])) { throw new coding_exception('grouping must be present in testing::create_grouping_group() $record'); } if (!isset($record['groupid'])) { throw new coding_exception('group must be present in testing_util::create_grouping_group() $record'); } return groups_assign_grouping($record['groupingid'], $record['groupid']); } /** * Create an instance of a repository. * * @param string type of repository to create an instance for. * @param array|stdClass $record data to use to up set the instance. * @param array $options options * @return stdClass repository instance record * @since Moodle 2.5.1 */ public function create_repository($type, $record=null, ?array $options = null) { $generator = $this->get_plugin_generator('repository_'.$type); return $generator->create_instance($record, $options); } /** * Create an instance of a repository. * * @param string type of repository to create an instance for. * @param array|stdClass $record data to use to up set the instance. * @param array $options options * @return repository_type object * @since Moodle 2.5.1 */ public function create_repository_type($type, $record=null, ?array $options = null) { $generator = $this->get_plugin_generator('repository_'.$type); return $generator->create_type($record, $options); } /** * Create a test scale * @param array|stdClass $record * @param array $options * @return stdClass block instance record */ public function create_scale($record=null, ?array $options=null) { global $DB; $this->scalecount++; $i = $this->scalecount; $record = (array)$record; if (!isset($record['name'])) { $record['name'] = 'Test scale '.$i; } if (!isset($record['scale'])) { $record['scale'] = 'A,B,C,D,F'; } if (!isset($record['courseid'])) { $record['courseid'] = 0; } if (!isset($record['userid'])) { $record['userid'] = 0; } if (!isset($record['description'])) { $record['description'] = 'Test scale description '.$i; } if (!isset($record['descriptionformat'])) { $record['descriptionformat'] = FORMAT_MOODLE; } $record['timemodified'] = time(); if (isset($record['id'])) { $DB->import_record('scale', $record); $DB->get_manager()->reset_sequence('scale'); $id = $record['id']; } else { $id = $DB->insert_record('scale', $record); } return $DB->get_record('scale', array('id'=>$id), '*', MUST_EXIST); } /** * Creates a new role in the system. * * You can fill $record with the role 'name', * 'shortname', 'description' and 'archetype'. * * If an archetype is specified it's capabilities, * context where the role can be assigned and * all other properties are copied from the archetype; * if no archetype is specified it will create an * empty role. * * @param array|stdClass $record * @return int The new role id */ public function create_role($record=null) { global $DB; $this->rolecount++; $i = $this->rolecount; $record = (array)$record; if (empty($record['shortname'])) { $record['shortname'] = 'role-' . $i; } if (empty($record['name'])) { $record['name'] = 'Test role ' . $i; } if (empty($record['description'])) { $record['description'] = 'Test role ' . $i . ' description'; } if (empty($record['archetype'])) { $record['archetype'] = ''; } else { $archetypes = get_role_archetypes(); if (empty($archetypes[$record['archetype']])) { throw new coding_exception('\'role\' requires the field \'archetype\' to specify a ' . 'valid archetype shortname (editingteacher, student...)'); } } // Creates the role. if (!$newroleid = create_role($record['name'], $record['shortname'], $record['description'], $record['archetype'])) { throw new coding_exception('There was an error creating \'' . $record['shortname'] . '\' role'); } // If no archetype was specified we allow it to be added to all contexts, // otherwise we allow it in the archetype contexts. if (!$record['archetype']) { $contextlevels = []; $usefallback = true; foreach (context_helper::get_all_levels() as $level => $title) { if (array_key_exists($title, $record)) { $usefallback = false; if (!empty($record[$title])) { $contextlevels[] = $level; } } } if ($usefallback) { $contextlevels = array_keys(context_helper::get_all_levels()); } } else { // Copying from the archetype default rol. $archetyperoleid = $DB->get_field( 'role', 'id', array('shortname' => $record['archetype'], 'archetype' => $record['archetype']) ); $contextlevels = get_role_contextlevels($archetyperoleid); } set_role_contextlevels($newroleid, $contextlevels); if ($record['archetype']) { // We copy all the roles the archetype can assign, override, switch to and view. if ($record['archetype']) { $types = array('assign', 'override', 'switch', 'view'); foreach ($types as $type) { $rolestocopy = get_default_role_archetype_allows($type, $record['archetype']); foreach ($rolestocopy as $tocopy) { $functionname = "core_role_set_{$type}_allowed"; $functionname($newroleid, $tocopy); } } } // Copying the archetype capabilities. $sourcerole = $DB->get_record('role', array('id' => $archetyperoleid)); role_cap_duplicate($sourcerole, $newroleid); } $allcapabilities = get_all_capabilities(); $foundcapabilities = array_intersect(array_keys($allcapabilities), array_keys($record)); $systemcontext = \context_system::instance(); $allpermissions = [ 'inherit' => CAP_INHERIT, 'allow' => CAP_ALLOW, 'prevent' => CAP_PREVENT, 'prohibit' => CAP_PROHIBIT, ]; foreach ($foundcapabilities as $capability) { $permission = $record[$capability]; if (!array_key_exists($permission, $allpermissions)) { throw new \coding_exception("Unknown capability permissions '{$permission}'"); } assign_capability( $capability, $allpermissions[$permission], $newroleid, $systemcontext->id, true ); } return $newroleid; } /** * Set role capabilities for the specified role. * * @param int $roleid The Role to set capabilities for * @param array $rolecapabilities The list of capability =>permission to set for this role * @param null|context $context The context to apply this capability to */ public function create_role_capability(int $roleid, array $rolecapabilities, ?context $context = null): void { // Map the capabilities into human-readable names. $allpermissions = [ 'inherit' => CAP_INHERIT, 'allow' => CAP_ALLOW, 'prevent' => CAP_PREVENT, 'prohibit' => CAP_PROHIBIT, ]; // Fetch all capabilities to check that they exist. $allcapabilities = get_all_capabilities(); foreach ($rolecapabilities as $capability => $permission) { if ($permission === '') { // Allow items to be skipped. continue; } if (!array_key_exists($capability, $allcapabilities)) { throw new \coding_exception("Unknown capability '{$capability}'"); } if (!array_key_exists($permission, $allpermissions)) { throw new \coding_exception("Unknown capability permissions '{$permission}'"); } assign_capability( $capability, $allpermissions[$permission], $roleid, $context->id, true ); } } /** * Create a tag. * * @param array|stdClass $record * @return stdClass the tag record */ public function create_tag($record = null) { global $DB, $USER; $this->tagcount++; $i = $this->tagcount; $record = (array) $record; if (!isset($record['userid'])) { $record['userid'] = $USER->id; } if (!isset($record['rawname'])) { if (isset($record['name'])) { $record['rawname'] = $record['name']; } else { $record['rawname'] = 'Tag name ' . $i; } } // Attribute 'name' should be a lowercase version of 'rawname', if not set. if (!isset($record['name'])) { $record['name'] = core_text::strtolower($record['rawname']); } else { $record['name'] = core_text::strtolower($record['name']); } if (!isset($record['tagcollid'])) { $record['tagcollid'] = core_tag_collection::get_default(); } if (!isset($record['description'])) { $record['description'] = 'Tag description'; } if (!isset($record['descriptionformat'])) { $record['descriptionformat'] = FORMAT_MOODLE; } if (!isset($record['flag'])) { $record['flag'] = 0; } if (!isset($record['timemodified'])) { $record['timemodified'] = time(); } $id = $DB->insert_record('tag', $record); return $DB->get_record('tag', array('id' => $id), '*', MUST_EXIST); } /** * Helper method which combines $defaults with the values specified in $record. * If $record is an object, it is converted to an array. * Then, for each key that is in $defaults, but not in $record, the value * from $defaults is copied. * @param array $defaults the default value for each field with * @param array|stdClass $record * @return array updated $record. */ public function combine_defaults_and_record(array $defaults, $record) { $record = (array) $record; foreach ($defaults as $key => $default) { if (!array_key_exists($key, $record)) { $record[$key] = $default; } else if (is_array($record[$key]) && is_array($default)) { $record[$key] = $this->combine_defaults_and_record($default, $record[$key]); } } return $record; } /** * Simplified enrolment of user to course using default options. * * It is strongly recommended to use only this method for 'manual' and 'self' plugins only!!! * * @param int $userid * @param int $courseid * @param int|string $roleidorshortname optional role id or role shortname, use only with manual plugin * @param string $enrol name of enrol plugin, * there must be exactly one instance in course, * it must support enrol_user() method. * @param int $timestart (optional) 0 means unknown * @param int $timeend (optional) 0 means forever * @param int $status (optional) default to ENROL_USER_ACTIVE for new enrolments * @return bool success */ public function enrol_user($userid, $courseid, $roleidorshortname = null, $enrol = 'manual', $timestart = 0, $timeend = 0, $status = null) { global $DB; // If role is specified by shortname, convert it into an id. if (!is_numeric($roleidorshortname) && is_string($roleidorshortname)) { $roleid = $DB->get_field('role', 'id', array('shortname' => $roleidorshortname), MUST_EXIST); } else { $roleid = $roleidorshortname; } if (!$plugin = enrol_get_plugin($enrol)) { return false; } $instances = $DB->get_records('enrol', array('courseid'=>$courseid, 'enrol'=>$enrol)); if (count($instances) != 1) { return false; } $instance = reset($instances); if (is_null($roleid) and $instance->roleid) { $roleid = $instance->roleid; } $plugin->enrol_user($instance, $userid, $roleid, $timestart, $timeend, $status); return true; } /** * Assigns the specified role to a user in the context. * * @param int|string $role either an int role id or a string role shortname. * @param int $userid * @param int|context $contextid Defaults to the system context * @return int new/existing id of the assignment */ public function role_assign($role, $userid, $contextid = false) { global $DB; // Default to the system context. if (!$contextid) { $context = context_system::instance(); $contextid = $context->id; } if (empty($role)) { throw new coding_exception('roleid must be present in testing_data_generator::role_assign() arguments'); } if (!is_number($role)) { $role = $DB->get_field('role', 'id', ['shortname' => $role], MUST_EXIST); } if (empty($userid)) { throw new coding_exception('userid must be present in testing_data_generator::role_assign() arguments'); } return role_assign($role, $userid, $contextid); } /** * Create a grade_category. * * @param array|stdClass $record * @return stdClass the grade category record */ public function create_grade_category($record = null) { global $CFG; $this->gradecategorycounter++; $record = (array)$record; if (empty($record['courseid'])) { throw new coding_exception('courseid must be present in testing::create_grade_category() $record'); } if (!isset($record['fullname'])) { $record['fullname'] = 'Grade category ' . $this->gradecategorycounter; } // For gradelib classes. require_once($CFG->libdir . '/gradelib.php'); // Create new grading category in this course. $gradecategory = new grade_category(array('courseid' => $record['courseid']), false); $gradecategory->apply_default_settings(); grade_category::set_properties($gradecategory, $record); $gradecategory->apply_forced_settings(); $gradecategory->insert(); // This creates a default grade item for the category $gradeitem = $gradecategory->load_grade_item(); $gradecategory->update_from_db(); return $gradecategory->get_record_data(); } /** * Create a grade_grade. * * @param array $record * @return grade_grade the grade record */ public function create_grade_grade(?array $record = null): grade_grade { global $DB, $USER; $item = $DB->get_record('grade_items', ['id' => $record['itemid']]); $userid = $record['userid'] ?? $USER->id; unset($record['itemid']); unset($record['userid']); if ($item->itemtype === 'mod') { $cm = get_coursemodule_from_instance($item->itemmodule, $item->iteminstance); $module = new $item->itemmodule(context_module::instance($cm->id), $cm, false); $record['attemptnumber'] = $record['attemptnumber'] ?? 0; $module->save_grade($userid, (object) $record); $grade = grade_grade::fetch(['userid' => $userid, 'itemid' => $item->id]); } else { $grade = grade_grade::fetch(['userid' => $userid, 'itemid' => $item->id]); $record['rawgrade'] = $record['rawgrade'] ?? $record['grade'] ?? null; $record['finalgrade'] = $record['finalgrade'] ?? $record['grade'] ?? null; unset($record['grade']); if ($grade) { $fields = $grade->required_fields + array_keys($grade->optional_fields); foreach ($fields as $field) { $grade->{$field} = $record[$field] ?? $grade->{$field}; } $grade->update(); } else { $record['userid'] = $userid; $record['itemid'] = $item->id; $grade = new grade_grade($record, false); $grade->insert(); } } return $grade; } /** * Create a grade_item. * * @param array|stdClass $record * @return stdClass the grade item record */ public function create_grade_item($record = null) { global $CFG; require_once("$CFG->libdir/gradelib.php"); $this->gradeitemcounter++; if (!isset($record['itemtype'])) { $record['itemtype'] = 'manual'; } if (!isset($record['itemname'])) { $record['itemname'] = 'Grade item ' . $this->gradeitemcounter; } if (isset($record['outcomeid'])) { $outcome = new grade_outcome(array('id' => $record['outcomeid'])); $record['scaleid'] = $outcome->scaleid; } if (isset($record['scaleid'])) { $record['gradetype'] = GRADE_TYPE_SCALE; } else if (!isset($record['gradetype'])) { $record['gradetype'] = GRADE_TYPE_VALUE; } // Create new grade item in this course. $gradeitem = new grade_item($record, false); $gradeitem->insert(); $gradeitem->update_from_db(); return $gradeitem->get_record_data(); } /** * Create a grade_outcome. * * @param array|stdClass $record * @return stdClass the grade outcome record */ public function create_grade_outcome($record = null) { global $CFG; $this->gradeoutcomecounter++; $i = $this->gradeoutcomecounter; if (!isset($record['fullname'])) { $record['fullname'] = 'Grade outcome ' . $i; } // For gradelib classes. require_once($CFG->libdir . '/gradelib.php'); // Create new grading outcome in this course. $gradeoutcome = new grade_outcome($record, false); $gradeoutcome->insert(); $gradeoutcome->update_from_db(); return $gradeoutcome->get_record_data(); } /** * Helper function used to create an LTI tool. * * @param stdClass $data * @return stdClass the tool */ public function create_lti_tool($data = array()) { global $DB; $studentrole = $DB->get_record('role', array('shortname' => 'student')); $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); // Create a course if no course id was specified. if (empty($data->courseid)) { $course = $this->create_course(); $data->courseid = $course->id; } else { $course = get_course($data->courseid); } if (!empty($data->cmid)) { $data->contextid = context_module::instance($data->cmid)->id; } else { $data->contextid = context_course::instance($data->courseid)->id; } // Set it to enabled if no status was specified. if (!isset($data->status)) { $data->status = ENROL_INSTANCE_ENABLED; } // Default to legacy lti version. if (empty($data->ltiversion) || !in_array($data->ltiversion, ['LTI-1p0/LTI-2p0', 'LTI-1p3'])) { $data->ltiversion = 'LTI-1p0/LTI-2p0'; } // Add some extra necessary fields to the data. $data->name = $data->name ?? 'Test LTI'; $data->roleinstructor = $teacherrole->id; $data->rolelearner = $studentrole->id; // Get the enrol LTI plugin. $enrolplugin = enrol_get_plugin('lti'); $instanceid = $enrolplugin->add_instance($course, (array) $data); // Get the tool associated with this instance. return $DB->get_record('enrol_lti_tools', array('enrolid' => $instanceid)); } /** * Helper function used to create an event. * * @param array $data * @return stdClass */ public function create_event($data = []) { global $CFG; require_once($CFG->dirroot . '/calendar/lib.php'); $record = new \stdClass(); $record->name = 'event name'; $record->repeat = 0; $record->repeats = 0; $record->timestart = time(); $record->timeduration = 0; $record->timesort = 0; $record->eventtype = 'user'; $record->courseid = 0; $record->categoryid = 0; foreach ($data as $key => $value) { $record->$key = $value; } switch ($record->eventtype) { case 'user': unset($record->categoryid); unset($record->courseid); unset($record->groupid); break; case 'group': unset($record->categoryid); break; case 'course': unset($record->categoryid); unset($record->groupid); break; case 'category': unset($record->courseid); unset($record->groupid); break; case 'site': unset($record->categoryid); unset($record->courseid); unset($record->groupid); break; } $event = new calendar_event($record); $event->create($record); return $event->properties(); } /** * Create a new course custom field category with the given name. * * @param array $data Array with data['name'] of category * @return \core_customfield\category_controller The created category */ public function create_custom_field_category($data): \core_customfield\category_controller { return $this->get_plugin_generator('core_customfield')->create_category($data); } /** * Create a new custom field * * @param array $data Array with 'name', 'shortname' and 'type' of the field * @return \core_customfield\field_controller The created field */ public function create_custom_field($data): \core_customfield\field_controller { global $DB; if (empty($data['categoryid']) && !empty($data['category'])) { $data['categoryid'] = $DB->get_field('customfield_category', 'id', ['name' => $data['category']]); unset($data['category']); } return $this->get_plugin_generator('core_customfield')->create_field($data); } /** * Create a new category for custom profile fields. * * @param array $data Array with 'name' and optionally 'sortorder' * @return \stdClass New category object */ public function create_custom_profile_field_category(array $data): \stdClass { global $DB; // Pick next sortorder if not defined. if (!array_key_exists('sortorder', $data)) { $data['sortorder'] = (int)$DB->get_field_sql('SELECT MAX(sortorder) FROM {user_info_category}') + 1; } $category = (object)[ 'name' => $data['name'], 'sortorder' => $data['sortorder'] ]; $category->id = $DB->insert_record('user_info_category', $category); return $category; } /** * Creates a new custom profile field. * * Optional fields are: * * categoryid (or use 'category' to specify by name). If you don't specify * either, it will add the field to a 'Testing' category, which will be created for you if * necessary. * * sortorder (if you don't specify this, it will pick the next one in the category). * * all the other database fields (if you don't specify this, it will pick sensible defaults * based on the data type). * * @param array $data Array with 'datatype', 'shortname', and 'name' * @return \stdClass Database object from the user_info_field table */ public function create_custom_profile_field(array $data): \stdClass { global $DB, $CFG; require_once($CFG->dirroot . '/user/profile/lib.php'); // Set up category if necessary. if (!array_key_exists('categoryid', $data)) { if (array_key_exists('category', $data)) { $data['categoryid'] = $DB->get_field('user_info_category', 'id', ['name' => $data['category']], MUST_EXIST); } else { // Make up a 'Testing' category or use existing. $data['categoryid'] = $DB->get_field('user_info_category', 'id', ['name' => 'Testing']); if (!$data['categoryid']) { $created = $this->create_custom_profile_field_category(['name' => 'Testing']); $data['categoryid'] = $created->id; } } } // Pick sort order if necessary. if (!array_key_exists('sortorder', $data)) { $data['sortorder'] = (int)$DB->get_field_sql( 'SELECT MAX(sortorder) FROM {user_info_field} WHERE categoryid = ?', [$data['categoryid']]) + 1; } if ($data['datatype'] === 'menu' && isset($data['param1'])) { // Convert new lines to the proper character. $data['param1'] = str_replace('\n', "\n", $data['param1']); } // Defaults for other values. $defaults = [ 'description' => '', 'descriptionformat' => 0, 'required' => 0, 'locked' => 0, 'visible' => PROFILE_VISIBLE_ALL, 'forceunique' => 0, 'signup' => 0, 'defaultdata' => '', 'defaultdataformat' => 0, 'param1' => '', 'param2' => '', 'param3' => '', 'param4' => '', 'param5' => '' ]; // Type-specific defaults for other values. $typedefaults = [ 'text' => [ 'param1' => 30, 'param2' => 2048 ], 'menu' => [ 'param1' => "Yes\nNo", 'defaultdata' => 'No' ], 'datetime' => [ 'param1' => '2010', 'param2' => '2015', 'param3' => 1 ], 'checkbox' => [ 'defaultdata' => 0 ] ]; foreach ($typedefaults[$data['datatype']] ?? [] as $field => $value) { $defaults[$field] = $value; } foreach ($defaults as $field => $value) { if (!array_key_exists($field, $data)) { $data[$field] = $value; } } $data['id'] = $DB->insert_record('user_info_field', $data); return (object)$data; } /** * Create a new user, and enrol them in the specified course as the supplied role. * * @param \stdClass $course The course to enrol in * @param string $role The role to give within the course * @param \stdClass|array $userparams User parameters * @return \stdClass The created user */ public function create_and_enrol($course, $role = 'student', $userparams = null, $enrol = 'manual', $timestart = 0, $timeend = 0, $status = null) { global $DB; $user = $this->create_user($userparams); $roleid = $DB->get_field('role', 'id', ['shortname' => $role ]); $this->enrol_user($user->id, $course->id, $roleid, $enrol, $timestart, $timeend, $status); return $user; } /** * Create a new last access record for a given user in a course. * * @param \stdClass $user The user * @param \stdClass $course The course the user accessed * @param int $timestamp The timestamp for when the user last accessed the course * @return \stdClass The user_lastaccess record */ public function create_user_course_lastaccess(\stdClass $user, \stdClass $course, int $timestamp): \stdClass { global $DB; $record = [ 'userid' => $user->id, 'courseid' => $course->id, 'timeaccess' => $timestamp, ]; $recordid = $DB->insert_record('user_lastaccess', $record); return $DB->get_record('user_lastaccess', ['id' => $recordid], '*', MUST_EXIST); } /** * Generate a stored_progress record and return the ID. * * All fields are optional, required fields will be generated if not supplied. * * @param ?string $idnumber The unique ID Number for this stored progress. * @param ?int $timestart The time progress was started, defaults to now. * @param ?int $lastupdate The time the progress was last updated. * @param float $percent The percentage progress so far. * @param ?string $message An error message. * @param ?bool $haserrored Whether the process has encountered an error. * @return stdClass The record including the inserted id. * @throws dml_exception */ public function create_stored_progress( ?string $idnumber = null, ?int $timestart = null, ?int $lastupdate = null, float $percent = 0.00, ?string $message = null, ?bool $haserrored = false, ): stdClass { global $DB; $record = (object)[ 'idnumber' => $idnumber ?? random_string(), 'timestart' => $timestart ?? time(), 'lastupdate' => $lastupdate, 'percentcompleted' => $percent, 'message' => $message, 'haserrored' => $haserrored, ]; $record->id = $DB->insert_record('stored_progress', $record); return $record; } /** * Generate a stored progress record from an array of fields. * * For use as a behat createable entity. Use {@see self::create_stored_progress()} if calling directly. * * @param array $data * @return void */ public function create_stored_progress_bar(array $data): void { $this->create_stored_progress( $data['idnumber'] ?? null, $data['timestart'] ?? null, $data['lastupdate'] ?? null, $data['percent'] ?? 0.00, $data['message'] ?? null, $data['haserrored'] ?? false, ); } /** * Gets a default generator for a given component. * * @param string $component The component name, e.g. 'mod_forum' or 'core_question'. * @param string $classname The name of the class missing from the generators file. * @return component_generator_base The generator. */ protected function get_default_plugin_generator(string $component, ?string $classname = null) { [$type, $plugin] = core_component::normalize_component($component); switch ($type) { case 'block': return new default_block_generator($this, $plugin); } if (is_null($classname)) { throw new coding_exception("Component {$component} does not support " . "generators yet. Missing tests/generator/lib.php."); } throw new coding_exception("Component {$component} does not support " . "data generators yet. Class {$classname} not found."); } } generator/default_block_generator.php000064400000003257152267206310014122 0ustar00. /** * Default block generator class. * * @package core * @category test * @copyright 2021 Moodle Pty Ltd. (http://moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Default block generator class to be used when a specific one is not supported. * * @package core * @category test * @copyright 2021 Moodle Pty Ltd. (http://moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class default_block_generator extends testing_block_generator { /** * @var string */ private $blockname; /** * Constructor. * @param testing_data_generator $datagenerator * @param string $blockname */ public function __construct(testing_data_generator $datagenerator, string $blockname) { parent::__construct($datagenerator); $this->blockname = $blockname; } /** * {@inheritdoc} */ public function get_blockname() { return $this->blockname; } } generator/block_generator.php000064400000014225152267206310012413 0ustar00. /** * Block generator base class. * * @package core * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Block generator base class. * * Extend in blocks/xxxx/tests/generator/lib.php as class block_xxxx_generator. * * @package core * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class testing_block_generator extends component_generator_base { /** @var int number of created instances */ protected $instancecount = 0; /** * To be called from data reset code only, * do not use in tests. * @return void */ public function reset() { $this->instancecount = 0; } /** * Returns block name * @return string name of block that this class describes * @throws coding_exception if class invalid */ public function get_blockname() { $matches = null; if (!preg_match('/^block_([a-z0-9_]+)_generator$/', get_class($this), $matches)) { throw new coding_exception('Invalid block generator class name: '.get_class($this)); } if (empty($matches[1])) { throw new coding_exception('Invalid block generator class name: '.get_class($this)); } return $matches[1]; } /** * Fill in record defaults. * * @param stdClass $record * @return stdClass */ protected function prepare_record(stdClass $record) { $record->blockname = $this->get_blockname(); if (!isset($record->parentcontextid)) { $record->parentcontextid = context_system::instance()->id; } if (!isset($record->showinsubcontexts)) { $record->showinsubcontexts = 0; } if (!isset($record->pagetypepattern)) { $record->pagetypepattern = '*'; } if (!isset($record->subpagepattern)) { $record->subpagepattern = null; } if (!isset($record->defaultregion)) { $record->defaultregion = 'side-pre'; } if (!isset($record->defaultweight)) { $record->defaultweight = 5; } if (!isset($record->configdata)) { $record->configdata = null; } return $record; } /** * Create a test block instance. * * The $record passed in becomes the basis for the new row added to the * block_instances table. You only need to supply the values of interest. * Any missing values have sensible defaults filled in. * * The $options array provides additional data, not directly related to what * will be inserted in the block_instance table, which may affect the block * that is created. The meanings of any data passed here depends on the particular * type of block being created. * * @param array|stdClass $record forms the basis for the entry to be inserted in the block_instances table. * @param array $options further, block-specific options to control how the block is created. * @return stdClass the block_instance record that has just been created. */ public function create_instance($record = null, $options = array()) { global $DB, $PAGE; $this->instancecount++; // Creating a block is a back end operation, which should not cause any output to happen. // This will allow us to check that the theme was not initialised while creating the block instance. $outputstartedbefore = $PAGE->get_where_theme_was_initialised(); $record = (object)(array)$record; $this->preprocess_record($record, $options); $record = $this->prepare_record($record); if (empty($record->timecreated)) { $record->timecreated = time(); } if (empty($record->timemodified)) { $record->timemodified = time(); } $id = $DB->insert_record('block_instances', $record); context_block::instance($id); $instance = $DB->get_record('block_instances', array('id' => $id), '*', MUST_EXIST); // If the theme was initialised while creating the block instance, something somewhere called an output // function. Rather than leaving this as a hard-to-debug situation, let's make it fail with a clear error. $outputstartedafter = $PAGE->get_where_theme_was_initialised(); if ($outputstartedbefore === null && $outputstartedafter !== null) { throw new coding_exception('Creating a block_' . $this->get_blockname() . ' initialised the theme and output!', 'This should not happen. Creating a block should be a pure back-end operation. Unnecessarily initialising ' . 'the output mechanism at the wrong time can cause subtle bugs and is a significant performance hit. There is ' . 'likely a call to an output function that caused it:' . PHP_EOL . PHP_EOL . format_backtrace($outputstartedafter, true)); } return $instance; } /** * Can be overridden to do block-specific processing. $record can be modified * in-place. * * @param stdClass $record the data, before defaults are filled in. * @param array $options further, block-specific options, as passed to {@link create_instance()}. */ protected function preprocess_record(stdClass $record, array $options) { } } generator/module_generator.php000064400000032063152267206310012606 0ustar00. /** * Module generator base class. * * @package core * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Module generator base class. * * Extend in mod/xxxx/tests/generator/lib.php as class mod_xxxx_generator. * * @package core * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class testing_module_generator extends component_generator_base { /** * @var int number of created instances */ protected $instancecount = 0; /** * To be called from data reset code only, * do not use in tests. * @return void */ public function reset() { $this->instancecount = 0; } /** * Returns module name * @return string name of module that this class describes * @throws coding_exception if class invalid */ public function get_modulename() { $matches = null; if (!preg_match('/^mod_([a-z0-9]+)_generator$/', get_class($this), $matches)) { throw new coding_exception('Invalid module generator class name: '.get_class($this)); } if (empty($matches[1])) { throw new coding_exception('Invalid module generator class name: '.get_class($this)); } return $matches[1]; } /** * Create course module and link it to course * * Since 2.6 it is recommended to use function add_moduleinfo() to create a module. * * @deprecated since 2.6 * @see testing_module_generator::create_instance() * * @param integer $courseid * @param array $options section, visible * @return integer $cm instance id */ protected function precreate_course_module($courseid, array $options) { global $DB, $CFG; require_once("$CFG->dirroot/course/lib.php"); $modulename = $this->get_modulename(); $sectionnum = isset($options['section']) ? $options['section'] : 0; unset($options['section']); // Prevent confusion, it would be overridden later in course_add_cm_to_section() anyway. $cm = new stdClass(); $cm->course = $courseid; $cm->module = $DB->get_field('modules', 'id', array('name'=>$modulename)); $cm->instance = 0; $cm->section = 0; $cm->idnumber = isset($options['idnumber']) ? $options['idnumber'] : 0; $cm->added = time(); $columns = $DB->get_columns('course_modules'); foreach ($options as $key => $value) { if ($key === 'id' or !isset($columns[$key])) { continue; } if (property_exists($cm, $key)) { continue; } $cm->$key = $value; } $cm->id = $DB->insert_record('course_modules', $cm); course_add_cm_to_section($courseid, $cm->id, $sectionnum, null, $modulename); return $cm->id; } /** * Called after *_add_instance() * * Since 2.6 it is recommended to use function add_moduleinfo() to create a module. * * @deprecated since 2.6 * @see testing_module_generator::create_instance() * * @param int $id * @param int $cmid * @return stdClass module instance */ protected function post_add_instance($id, $cmid) { global $DB; $DB->set_field('course_modules', 'instance', $id, array('id'=>$cmid)); $instance = $DB->get_record($this->get_modulename(), array('id'=>$id), '*', MUST_EXIST); $cm = get_coursemodule_from_id($this->get_modulename(), $cmid, $instance->course, true, MUST_EXIST); context_module::instance($cm->id); $instance->cmid = $cm->id; return $instance; } /** * Merges together arguments $record and $options and fills default module * fields that are shared by all module types * * @param object|array $record fields (different from defaults) for this module * @param null|array $options for backward-compatibility this may include fields from course_modules * table. They are merged into $record * @throws coding_exception if $record->course is not specified */ protected function prepare_moduleinfo_record($record, $options) { global $DB; // Make sure we don't modify the original object. $moduleinfo = (object)(array)$record; if (empty($moduleinfo->course)) { throw new coding_exception('module generator requires $record->course'); } $moduleinfo->modulename = $this->get_modulename(); $moduleinfo->module = $DB->get_field('modules', 'id', array('name' => $moduleinfo->modulename)); // Allow idnumber to be set as either $options['idnumber'] or $moduleinfo->cmidnumber or $moduleinfo->idnumber. // The actual field name is 'idnumber' but add_moduleinfo() expects 'cmidnumber'. if (isset($options['idnumber'])) { $moduleinfo->cmidnumber = $options['idnumber']; } else if (!isset($moduleinfo->cmidnumber) && isset($moduleinfo->idnumber)) { $moduleinfo->cmidnumber = $moduleinfo->idnumber; } // These are the fields from table 'course_modules' in 2.6 when the second // argument $options is being deprecated. // List excludes fields: instance (does not exist yet), course, module and idnumber (set above) $easymergefields = array('section', 'added', 'score', 'indent', 'visible', 'visibleold', 'groupmode', 'groupingid', 'completion', 'completiongradeitemnumber', 'completionview', 'completionexpected', 'completionpassgrade', 'availability', 'showdescription'); foreach ($easymergefields as $key) { if (isset($options[$key])) { $moduleinfo->$key = $options[$key]; } } // Set default values. Note that visibleold and completiongradeitemnumber are not used when creating a module. $defaults = array( 'section' => 0, 'visible' => 1, 'visibleoncoursepage' => 1, 'cmidnumber' => '', 'groupmode' => 0, 'groupingid' => 0, 'availability' => null, 'completion' => 0, 'completionview' => 0, 'completionexpected' => 0, 'completionpassgrade' => 0, 'conditiongradegroup' => array(), 'conditionfieldgroup' => array(), 'conditioncompletiongroup' => array() ); foreach ($defaults as $key => $value) { if (!isset($moduleinfo->$key)) { $moduleinfo->$key = $value; } } return $moduleinfo; } /** * Creates an instance of the module for testing purposes. * * Module type will be taken from the class name. Each module type may overwrite * this function to add other default values used by it. * * @param array|stdClass $record data for module being generated. Requires 'course' key * (an id or the full object). Also can have any fields from add module form. * @param null|array $options general options for course module. Since 2.6 it is * possible to omit this argument by merging options into $record * @return stdClass record from module-defined table with additional field * cmid (corresponding id in course_modules table) */ public function create_instance($record = null, ?array $options = null) { global $CFG, $DB, $PAGE; require_once($CFG->dirroot.'/course/modlib.php'); $this->instancecount++; // Creating an activity is a back end operation, which should not cause any output to happen. // This will allow us to check that the theme was not initialised while creating the module instance. $outputstartedbefore = $PAGE->get_where_theme_was_initialised(); // Merge options into record and add default values. $record = $this->prepare_moduleinfo_record($record, $options); // Retrieve the course record. if (!empty($record->course->id)) { $course = $record->course; $record->course = $record->course->id; } else { $course = get_course($record->course); } // Fill the name and intro with default values (if missing). if (empty($record->name)) { $record->name = get_string('pluginname', $this->get_modulename()).' '.$this->instancecount; // Module label can be created without name specified. It will get its name from the intro's text. if ($this->get_modulename() === 'label') { $record->name = ''; } } if (empty($record->introeditor) && empty($record->intro)) { $record->intro = 'Test '.$this->get_modulename().' ' . $this->instancecount; } if (empty($record->introeditor) && empty($record->introformat)) { $record->introformat = FORMAT_MOODLE; } if (isset($record->tags) && !is_array($record->tags)) { $record->tags = preg_split('/\s*,\s*/', trim($record->tags), -1, PREG_SPLIT_NO_EMPTY); } // Before Moodle 2.6 it was possible to create a module with completion tracking when // it is not setup for course and/or site-wide. Display debugging message so it is // easier to trace an error in unittests. if ($record->completion && empty($CFG->enablecompletion)) { debugging('Did you forget to set $CFG->enablecompletion before generating module with completion tracking?', DEBUG_DEVELOPER); } if ($record->completion && empty($course->enablecompletion)) { debugging('Did you forget to enable completion tracking for the course before generating module with completion tracking?', DEBUG_DEVELOPER); } if (!empty($record->lang) && !has_capability('moodle/course:setforcedlanguage', context_course::instance($course->id))) { throw new coding_exception('Attempt to generate an activity when the current user does not have ' . 'permission moodle/course:setforcedlanguage. This does not work.'); } // Add the module to the course. $moduleinfo = add_moduleinfo($record, $course); // Prepare object to return with additional field cmid. $modulename = $this->get_modulename(); $instance = $DB->get_record($modulename, ['id' => $moduleinfo->instance], '*', MUST_EXIST); $instance->cmid = $moduleinfo->coursemodule; // Insert files for the 'intro' file area. $instance = $this->insert_files( $instance, $record, $modulename, \context_module::instance($instance->cmid), "mod_{$modulename}", 'intro', 0 ); // If the theme was initialised while creating the module instance, something somewhere called an output // function. Rather than leaving this as a hard-to-debug situation, let's make it fail with a clear error. $outputstartedafter = $PAGE->get_where_theme_was_initialised(); if ($outputstartedbefore === null && $outputstartedafter !== null) { throw new coding_exception('Creating a mod_' . $this->get_modulename() . ' activity initialised the theme and output!', 'This should not happen. Creating an activity should be a pure back-end operation. Unnecessarily initialising ' . 'the output mechanism at the wrong time can cause subtle bugs and is a significant performance hit. There is ' . 'likely a call to an output function that caused it:' . PHP_EOL . PHP_EOL . format_backtrace($outputstartedafter, true)); } return $instance; } /** * Generates a piece of content for the module. * User is usually taken from global $USER variable. * @param stdClass $instance object returned from create_instance() call * @param stdClass|array $record * @return stdClass generated object * @throws coding_exception if function is not implemented by module */ public function create_content($instance, $record = array()) { throw new coding_exception('Module generator for '.$this->get_modulename().' does not implement method create_content()'); } } generator/repository_generator.php000064400000017431152267206310013542 0ustar00. /** * Repository data generator * * @package repository * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Repository data generator class * * @package core * @category test * @copyright 2013 Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 2.5.1 */ class testing_repository_generator extends component_generator_base { /** * Number of instances created * @var int */ protected $instancecount = 0; /** * To be called from data reset code only, * do not use in tests. * @return void */ public function reset() { $this->instancecount = 0; } /** * Returns repository type name * * @return string name of the type of repository * @throws coding_exception if class invalid */ public function get_typename() { $matches = null; if (!preg_match('/^repository_([a-z0-9_]+)_generator$/', get_class($this), $matches)) { throw new coding_exception('Invalid repository generator class name: '.get_class($this)); } if (empty($matches[1])) { throw new coding_exception('Invalid repository generator class name: '.get_class($this)); } return $matches[1]; } /** * Fill in record defaults. * * @param array $record * @return array */ protected function prepare_record(array $record) { if (!isset($record['name'])) { $record['name'] = $this->get_typename() . ' ' . $this->instancecount; } if (!isset($record['contextid'])) { $record['contextid'] = context_system::instance()->id; } return $record; } /** * Fill in type record defaults. * * @param array $record * @return array */ protected function prepare_type_record(array $record) { if (!isset($record['pluginname'])) { $record['pluginname'] = ''; } if (!isset($record['enableuserinstances'])) { $record['enableuserinstances'] = 1; } if (!isset($record['enablecourseinstances'])) { $record['enablecourseinstances'] = 1; } return $record; } /** * Create a test repository instance. * * @param array|stdClass $record * @param array $options * @return stdClass repository instance record */ public function create_instance($record = null, ?array $options = null) { global $CFG, $DB, $PAGE; require_once($CFG->dirroot . '/repository/lib.php'); $this->instancecount++; $record = (array) $record; // Creating a repository is a back end operation, which should not cause any output to happen. // This will allow us to check that the theme was not initialised while creating the repository instance. $outputstartedbefore = $PAGE->get_where_theme_was_initialised(); $typeid = $DB->get_field('repository', 'id', array('type' => $this->get_typename()), MUST_EXIST); $instanceoptions = repository::static_function($this->get_typename(), 'get_instance_option_names'); if (empty($instanceoptions)) { // There can only be one instance of this repository, and it should have been created along with the type. $id = $DB->get_field('repository_instances', 'id', array('typeid' => $typeid), MUST_EXIST); } else { // Create the new instance, but first make sure all the required parameters are set. $record = $this->prepare_record($record); if (empty($record['contextid'])) { throw new coding_exception('contextid must be present in testing_repository_generator::create_instance() $record'); } foreach ($instanceoptions as $option) { if (!isset($record[$option])) { throw new coding_exception("$option must be present in testing_repository_generator::create_instance() \$record"); } } $context = context::instance_by_id($record['contextid']); unset($record['contextid']); if (!in_array($context->contextlevel, array(CONTEXT_SYSTEM, CONTEXT_COURSE, CONTEXT_USER))) { throw new coding_exception('Wrong contextid passed in testing_repository_generator::create_instance() $record'); } $id = repository::static_function($this->get_typename(), 'create', $this->get_typename(), 0, $context, $record); } // If the theme was initialised while creating the repository instance, something somewhere called an output // function. Rather than leaving this as a hard-to-debug situation, let's make it fail with a clear error. $outputstartedafter = $PAGE->get_where_theme_was_initialised(); if ($outputstartedbefore === null && $outputstartedafter !== null) { throw new coding_exception('Creating a repository_' . $this->get_typename() . ' initialised the theme and output!', 'This should not happen. Creating a repository should be a pure back-end operation. Unnecessarily initialising ' . 'the output mechanism at the wrong time can cause subtle bugs and is a significant performance hit. There is ' . 'likely a call to an output function that caused it:' . PHP_EOL . PHP_EOL . format_backtrace($outputstartedafter, true)); } return $DB->get_record('repository_instances', array('id' => $id), '*', MUST_EXIST); } /** * Create the type of repository. * * @param stdClass|array $record data to use to set up the type * @param array $options options for the set up of the type * * @return stdClass repository type record */ public function create_type($record = null, ?array $options = null) { global $CFG, $DB; require_once($CFG->dirroot . '/repository/lib.php'); $record = (array) $record; $type = $this->get_typename(); $typeoptions = repository::static_function($type, 'get_type_option_names'); $instanceoptions = repository::static_function($type, 'get_instance_option_names'); // The type allow for user and course instances. if (!empty($instanceoptions)) { $typeoptions[] = 'enableuserinstances'; $typeoptions[] = 'enablecourseinstances'; } // Make sure all the parameters are set. $record = $this->prepare_type_record($record); foreach ($typeoptions as $option) { if (!isset($record[$option])) { throw new coding_exception("$option must be present in testing::create_repository_type() for $type"); } } // Limit to allowed options. $record = array_intersect_key($record, array_flip($typeoptions)); // Create the type. $plugintype = new repository_type($type, $record); $plugintype->create(false); return $DB->get_record('repository', array('type' => $type)); } } lib.php000064400000022341152267206310006031 0ustar00. /** * Testing general functions * * Note: these functions must be self contained and must not rely on any library or include * * @package core * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Composer error exit status. * * @var int */ define('TESTING_EXITCODE_COMPOSER', 255); /** * Returns relative path against current working directory, * to be used for shell execution hints. * @param string $moodlepath starting with "/", ex: "/admin/tool/cli/init.php" * @return string path relative to current directory or absolute path */ function testing_cli_argument_path($moodlepath) { global $CFG; if (isset($CFG->admin) and $CFG->admin !== 'admin') { $moodlepath = preg_replace('|^/admin/|', "/$CFG->admin/", $moodlepath); } if (isset($_SERVER['REMOTE_ADDR'])) { // Web access, this should not happen often. $cwd = dirname(dirname(__DIR__)); } else { // This is the real CLI script, work with relative paths. $cwd = getcwd(); } // Remove last directory separator as $path will not contain one. if ((substr($cwd, -1) === '/') || (substr($cwd, -1) === '\\')) { $cwd = substr($cwd, -1); } $path = realpath($CFG->dirroot.$moodlepath); // We need standrad directory seperator for path and cwd, so it can be compared. $cwd = testing_cli_fix_directory_separator($cwd); $path = testing_cli_fix_directory_separator($path); if (strpos($path, $cwd) === 0) { // Remove current working directory and directory separator. $path = substr($path, strlen($cwd) + 1); } return $path; } /** * Try to change permissions to $CFG->dirroot or $CFG->dataroot if possible * @param string $file * @return bool success */ function testing_fix_file_permissions($file) { global $CFG; $permissions = fileperms($file); if ($permissions & $CFG->filepermissions != $CFG->filepermissions) { $permissions = $permissions | $CFG->filepermissions; return chmod($file, $permissions); } return true; } /** * Find out if running under Cygwin on Windows. * @return bool */ function testing_is_cygwin() { if (empty($_SERVER['OS']) or $_SERVER['OS'] !== 'Windows_NT') { return false; } else if (!empty($_SERVER['SHELL']) and $_SERVER['SHELL'] === '/bin/bash') { return true; } else if (!empty($_SERVER['TERM']) and $_SERVER['TERM'] === 'cygwin') { return true; } else { return false; } } /** * Returns whether a mingw CLI is running. * * MinGW sets $_SERVER['TERM'] to cygwin, but it * can not run .bat files; this function may be useful * when we need to output proposed commands to users * using Windows CLI interfaces. * * @link http://sourceforge.net/p/mingw/bugs/1902 * @return bool */ function testing_is_mingw() { if (!testing_is_cygwin()) { return false; } if (!empty($_SERVER['MSYSTEM'])) { return true; } return false; } /** * Mark empty dataroot to be used for testing. * @param string $dataroot The dataroot directory * @param string $framework The test framework * @return void */ function testing_initdataroot($dataroot, $framework) { global $CFG; $filename = $dataroot . '/' . $framework . 'testdir.txt'; umask(0); if (!file_exists($filename)) { file_put_contents($filename, 'Contents of this directory are used during tests only, do not delete this file!'); } testing_fix_file_permissions($filename); $varname = $framework . '_dataroot'; $datarootdir = $CFG->{$varname} . '/' . $framework; if (!file_exists($datarootdir)) { mkdir($datarootdir, $CFG->directorypermissions); } } /** * Prints an error and stops execution * * @param integer $errorcode * @param string $text * @return void exits */ function testing_error($errorcode, $text = '') { // do not write to error stream because we need the error message in PHP exec result from web ui echo($text."\n"); if (isset($_SERVER['REMOTE_ADDR'])) { header('HTTP/1.1 500 Internal Server Error'); } exit($errorcode); } /** * Perform necessary steps to install and/or upgrade composer and its dependencies. * * Installation is mandatory, but upgrade is optional. * * Note: This function does not return, but will call `exit()` on error. * * @param bool $selfupdate Perform a composer self-update to update the composer.phar utility * @param bool $updatedependencies Upgrade dependencies */ function testing_update_composer_dependencies(bool $selfupdate = true, bool $updatedependencies = true): void { // To restore the value after finishing. $cwd = getcwd(); // Set some paths. $dirroot = dirname(dirname(__DIR__)); // Switch to Moodle's dirroot for easier path handling. chdir($dirroot); // Download or update composer.phar. Unfortunately we can't use the curl // class in filelib.php as we're running within one of the test platforms. $composerpath = $dirroot . DIRECTORY_SEPARATOR . 'composer.phar'; if (!file_exists($composerpath)) { $composerurl = 'https://getcomposer.org/composer.phar'; $file = @fopen($composerpath, 'w'); if ($file === false) { $errordetails = error_get_last(); $error = sprintf("Unable to create composer.phar\nPHP error: %s", $errordetails['message']); testing_error(TESTING_EXITCODE_COMPOSER, $error); } $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $composerurl); curl_setopt($curl, CURLOPT_FILE, $file); $result = curl_exec($curl); $curlerrno = curl_errno($curl); $curlerror = curl_error($curl); $curlinfo = curl_getinfo($curl); curl_close($curl); fclose($file); if (!$result) { $error = sprintf("Unable to download composer.phar\ncURL error (%d): %s", $curlerrno, $curlerror); testing_error(TESTING_EXITCODE_COMPOSER, $error); } else if ($curlinfo['http_code'] === 404) { if (file_exists($composerpath)) { // Deleting the resource as it would contain HTML. unlink($composerpath); } $error = sprintf("Unable to download composer.phar\n" . "404 http status code fetching $composerurl"); testing_error(TESTING_EXITCODE_COMPOSER, $error); } // Do not self-update after installation. $selfupdate = false; } if ($selfupdate) { passthru("php composer.phar self-update", $code); if ($code != 0) { exit($code); } } // If the vendor directory does not exist, force the installation of dependencies. $vendorpath = $dirroot . DIRECTORY_SEPARATOR . 'vendor'; if (!file_exists($vendorpath)) { $updatedependencies = true; } if ($updatedependencies) { // Update composer dependencies. passthru("php composer.phar install", $code); if ($code != 0) { exit($code); } } // Return to our original location. chdir($cwd); } /** * Fix DIRECTORY_SEPARATOR for windows. * * In PHP on Windows, DIRECTORY_SEPARATOR is set to the backslash (\) * character. However, if you're running a Cygwin/Msys/Git shell * exec() calls will return paths using the forward slash (/) character. * * NOTE: Because PHP on Windows will accept either forward or backslashes, * paths should be built using ONLY forward slashes, regardless of * OS. MOODLE_DIRECTORY_SEPARATOR should only be used when parsing * paths returned by the shell. * * @param string $path * @return string */ function testing_cli_fix_directory_separator($path) { global $CFG; static $dirseparator = null; if (!$dirseparator) { // Default directory separator. $dirseparator = DIRECTORY_SEPARATOR; // On windows we need to find what directory separator is used. if ($CFG->ostype = 'WINDOWS') { if (!empty($_SERVER['argv'][0])) { if (false === strstr($_SERVER['argv'][0], '\\')) { $dirseparator = '/'; } else { $dirseparator = '\\'; } } else if (testing_is_cygwin()) { $dirseparator = '/'; } } } // Normalize \ and / to directory separator. $path = str_replace('\\', $dirseparator, $path); $path = str_replace('/', $dirseparator, $path); return $path; } classes/privacy/provider.php000064400000004553152267206310012234 0ustar00. /** * Privacy Subsystem implementation for core_cache. * * @package core_cache * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_cache\privacy; use core_privacy\local\metadata\collection; /** * Privacy Subsystem implementation for core_cache. * * @copyright 2018 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements // Caches store data. \core_privacy\local\metadata\provider, // The cache subsystem stores data on behalf of other components. \core_privacy\local\request\subsystem\plugin_provider, \core_privacy\local\request\shared_userlist_provider { /** * Returns meta data about this system. * * Note, although this plugin does store user data, it is not able to * identify it, and that user data is typically very short lived. * * Therefore it is not realistically possible to export any of this * data as it is only identifiable by the plugin storing it, and that * plugin should already be exporting the data as part of it's own * implementation. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { // Data is stored in cache stores. $collection->add_plugintype_link('cachestore', [], 'privacy:metadata:cachestore'); // Cache locks do not store any personal user data. return $collection; } } classes/incrementing_clock.php000064400000004234152267206310012556 0ustar00. /** * Incrementing clock for testing purposes. * * @package core * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @property-read int $time The current time of the clock */ class incrementing_clock implements \core\clock { /** @var int The next time of the clock */ public int $time; /** @var DateTimeZone The system timezone. */ protected DateTimeZone $timezone; /** * Create a new instance of the incrementing clock. * * @param null|int $starttime The initial time to use. If not specified, the current time is used. */ public function __construct( ?int $starttime = null, ) { $this->time = $starttime ?? time(); $this->timezone = \core_date::get_server_timezone_object(); } public function now(): \DateTimeImmutable { return (new \DateTimeImmutable('@' . $this->time++))->setTimezone($this->timezone); } public function time(): int { return $this->now()->getTimestamp(); } /** * Set the time of the clock. * * @param int $time */ public function set_to(int $time): void { $this->time = $time; } /** * Bump the time by a number of seconds. * * Note: The act of fetching the time will also bump the time by one second. * * @param int $seconds */ public function bump(int $seconds = 1): void { $this->time += $seconds; } } classes/frozen_clock.php000064400000004625152267206310011403 0ustar00. /** * Frozen clock for testing purposes. * * @package core * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @property-read \DateTimeImmutable $time The current time of the clock */ class frozen_clock implements \core\clock { /** @var DateTimeImmutable The next time of the clock */ public DateTimeImmutable $time; /** * Create a new instance of the frozen clock. * * @param null|int $time The initial time to use. If not specified, the current time is used. */ public function __construct( ?int $time = null, ) { if ($time) { // Note that the constructor with time zone does not work when specifying a timestamp, // so we have to set timezone separately afterward. $this->time = (new \DateTimeImmutable("@{$time}")) ->setTimezone(\core_date::get_server_timezone_object()); } else { $this->time = (new \DateTimeImmutable())->setTimezone(\core_date::get_server_timezone_object()); } } public function now(): \DateTimeImmutable { return $this->time; } public function time(): int { return $this->time->getTimestamp(); } /** * Set the time of the clock. * * @param int $time */ public function set_to(int $time): void { $this->time = (new \DateTimeImmutable("@{$time}")) ->setTimezone(\core_date::get_server_timezone_object()); } /** * Bump the time by a number of seconds. * * @param int $seconds */ public function bump(int $seconds = 1): void { $this->time = $this->time->modify("+{$seconds} seconds"); } } classes/test_lock.php000064400000010157152267206310010711 0ustar00. /** * Tests lock * * @package core * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__.'/../lib.php'); /** * Tests lock to prevent concurrent executions of the same test suite * * @package core * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class test_lock { /** * @var array Array of resource used for prevention of parallel test execution */ protected static $lockhandles = array(); /** * Prevent parallel test execution - this can not work in Moodle because we modify database and dataroot. * * Note: do not call manually! * * @internal * @static * @param string $framework phpunit|behat * @param string $lockfilesuffix A sub-type used by the framework * @return void */ public static function acquire(string $framework, string $lockfilesuffix = '') { global $CFG; $datarootpath = $CFG->{$framework . '_dataroot'} . '/' . $framework; $lockfile = "{$datarootpath}/lock{$lockfilesuffix}"; if (!file_exists($datarootpath)) { // Dataroot not initialised yet. return; } if (!file_exists($lockfile)) { file_put_contents($lockfile, 'This file prevents concurrent execution of Moodle ' . $framework . ' tests'); testing_fix_file_permissions($lockfile); } $lockhandlename = self::get_lock_handle_name($framework, $lockfilesuffix); if (self::$lockhandles[$lockhandlename] = fopen($lockfile, 'r')) { $wouldblock = null; $locked = flock(self::$lockhandles[$lockhandlename], (LOCK_EX | LOCK_NB), $wouldblock); if (!$locked) { if ($wouldblock) { echo "Waiting for other test execution to complete...\n"; } $locked = flock(self::$lockhandles[$lockhandlename], LOCK_EX); } if (!$locked) { fclose(self::$lockhandles[$lockhandlename]); self::$lockhandles[$lockhandlename] = null; } } register_shutdown_function(['test_lock', 'release'], $framework, $lockfilesuffix); } /** * Note: do not call manually! * @internal * @static * @param string $framework phpunit|behat * @param string $lockfilesuffix A sub-type used by the framework * @return void */ public static function release(string $framework, string $lockfilesuffix = '') { $lockhandlename = self::get_lock_handle_name($framework, $lockfilesuffix); if (self::$lockhandles[$lockhandlename]) { flock(self::$lockhandles[$lockhandlename], LOCK_UN); fclose(self::$lockhandles[$lockhandlename]); self::$lockhandles[$lockhandlename] = null; } } /** * Get the name of the lock handle stored in the class. * * @param string $framework * @param string $lockfilesuffix * @return string */ protected static function get_lock_handle_name(string $framework, string $lockfilesuffix): string { $lockhandlepieces = [$framework]; if (!empty($lockfilesuffix)) { $lockhandlepieces[] = $lockfilesuffix; } return implode('%', $lockhandlepieces); } } classes/util.php000064400000101732152267206310007677 0ustar00. /** * Testing util classes * * @package core * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class testing_util { /** * @var string dataroot (likely to be $CFG->dataroot). */ private static $dataroot = null; /** * @var testing_data_generator */ protected static $generator = null; /** * @var string current version hash from php files */ protected static $versionhash = null; /** * @var array original content of all database tables */ protected static $tabledata = null; /** * @var array original structure of all database tables */ protected static $tablestructure = null; /** * @var array keep list of sequenceid used in a table. */ private static $tablesequences = []; /** * @var array list of updated tables. */ public static $tableupdated = []; /** * @var array original structure of all database tables */ protected static $sequencenames = null; /** * @var string name of the json file where we store the list of dataroot files to not reset during reset_dataroot. */ private static $originaldatafilesjson = 'originaldatafiles.json'; /** * @var boolean set to true once $originaldatafilesjson file is created. */ private static $originaldatafilesjsonadded = false; /** * @var int next sequence value for a single test cycle. */ protected static $sequencenextstartingid = null; /** * Return the name of the JSON file containing the init filenames. * * @static * @return string */ public static function get_originaldatafilesjson() { return self::$originaldatafilesjson; } /** * Return the dataroot. It's useful when mocking the dataroot when unit testing this class itself. * * @static * @return string the dataroot. */ public static function get_dataroot() { global $CFG; // By default it's the test framework dataroot. if (empty(self::$dataroot)) { self::$dataroot = $CFG->dataroot; } return self::$dataroot; } /** * Set the dataroot. It's useful when mocking the dataroot when unit testing this class itself. * * @param string $dataroot the dataroot of the test framework. * @static */ public static function set_dataroot($dataroot) { self::$dataroot = $dataroot; } /** * Returns the testing framework name * @static * @return string */ final protected static function get_framework() { $classname = get_called_class(); return substr($classname, 0, strpos($classname, '_')); } /** * Get data generator * @static * @return testing_data_generator */ public static function get_data_generator() { if (is_null(self::$generator)) { require_once(__DIR__ . '/../generator/lib.php'); self::$generator = new testing_data_generator(); } return self::$generator; } /** * Does this site (db and dataroot) appear to be used for production? * We try very hard to prevent accidental damage done to production servers!! * * @static * @return bool */ public static function is_test_site() { global $DB, $CFG; $framework = self::get_framework(); if (!file_exists(self::get_dataroot() . '/' . $framework . 'testdir.txt')) { // This is already tested in bootstrap script, // But anyway presence of this file means the dataroot is for testing. return false; } $tables = $DB->get_tables(false); if ($tables) { if (!$DB->get_manager()->table_exists('config')) { return false; } if (!get_config('core', $framework . 'test')) { return false; } } return true; } /** * Returns whether test database and dataroot were created using the current version codebase * * @return bool */ public static function is_test_data_updated() { global $DB; $framework = self::get_framework(); $datarootpath = self::get_dataroot() . '/' . $framework; if (!file_exists($datarootpath . '/tabledata.ser') || !file_exists($datarootpath . '/tablestructure.ser')) { return false; } if (!file_exists($datarootpath . '/versionshash.txt')) { return false; } $hash = core_component::get_all_versions_hash(); $oldhash = file_get_contents($datarootpath . '/versionshash.txt'); if ($hash !== $oldhash) { return false; } // A direct database request must be used to avoid any possible caching of an older value. $dbhash = $DB->get_field('config', 'value', ['name' => $framework . 'test']); if ($hash !== $dbhash) { return false; } return true; } /** * Stores the status of the database * * Serializes the contents and the structure and * stores it in the test framework space in dataroot */ protected static function store_database_state() { global $DB, $CFG; $framework = self::get_framework(); // Store data for all tables. $data = []; $structure = []; $tables = $DB->get_tables(); foreach ($tables as $table) { $columns = $DB->get_columns($table); $structure[$table] = $columns; if (isset($columns['id']) && $columns['id']->auto_increment) { $data[$table] = $DB->get_records($table, [], 'id ASC'); } else { // There should not be many of these. $data[$table] = $DB->get_records($table, []); } } $data = serialize($data); $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser'; file_put_contents($datafile, $data); testing_fix_file_permissions($datafile); $structure = serialize($structure); $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser'; file_put_contents($structurefile, $structure); testing_fix_file_permissions($structurefile); } /** * Stores the version hash in both database and dataroot */ protected static function store_versions_hash() { global $CFG; $framework = self::get_framework(); $hash = core_component::get_all_versions_hash(); // Add test db flag. set_config($framework . 'test', $hash); // Hash all plugin versions - helps with very fast detection of db structure changes. $hashfile = self::get_dataroot() . '/' . $framework . '/versionshash.txt'; file_put_contents($hashfile, $hash); testing_fix_file_permissions($hashfile); } /** * Returns contents of all tables right after installation. * @static * @return array $table=>$records */ protected static function get_tabledata() { if (!isset(self::$tabledata)) { $framework = self::get_framework(); $datafile = self::get_dataroot() . '/' . $framework . '/tabledata.ser'; if (!file_exists($datafile)) { // Not initialised yet. return []; } $data = file_get_contents($datafile); self::$tabledata = unserialize($data); } if (!is_array(self::$tabledata)) { testing_error( 1, 'Can not read dataroot/' . $framework . '/tabledata.ser or invalid format, reinitialize test database.', ); } return self::$tabledata; } /** * Returns structure of all tables right after installation. * @static * @return array $table=>$records */ public static function get_tablestructure() { if (!isset(self::$tablestructure)) { $framework = self::get_framework(); $structurefile = self::get_dataroot() . '/' . $framework . '/tablestructure.ser'; if (!file_exists($structurefile)) { // Not initialised yet. return []; } $data = file_get_contents($structurefile); self::$tablestructure = unserialize($data); } if (!is_array(self::$tablestructure)) { testing_error( 1, "Can not read dataroot/{$framework}/tablestructure.ser or invalid format, reinitialize test database.", ); } return self::$tablestructure; } /** * Returns the names of sequences for each autoincrementing id field in all standard tables. * @static * @return array $table=>$sequencename */ public static function get_sequencenames() { global $DB; if (isset(self::$sequencenames)) { return self::$sequencenames; } if (!$structure = self::get_tablestructure()) { return []; } self::$sequencenames = []; foreach ($structure as $table => $ignored) { $name = $DB->get_manager()->generator->getSequenceFromDB(new xmldb_table($table)); if ($name !== false) { self::$sequencenames[$table] = $name; } } return self::$sequencenames; } /** * Returns list of tables that are unmodified and empty. * * @static * @return array of table names, empty if unknown */ protected static function guess_unmodified_empty_tables() { global $DB; $dbfamily = $DB->get_dbfamily(); if ($dbfamily === 'mysql') { $empties = []; $prefix = $DB->get_prefix(); $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", [$prefix . '%']); foreach ($rs as $info) { $table = strtolower($info->name); if (strpos($table, $prefix) !== 0) { // Incorrect table match caused by _. continue; } if (!is_null($info->auto_increment) && $info->rows == 0 && ($info->auto_increment == 1)) { $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table); $empties[$table] = $table; } } $rs->close(); return $empties; } else if ($dbfamily === 'mssql') { $empties = []; $prefix = $DB->get_prefix(); $sql = "SELECT t.name FROM sys.identity_columns i JOIN sys.tables t ON t.object_id = i.object_id WHERE t.name LIKE ? AND i.name = 'id' AND i.last_value IS NULL"; $rs = $DB->get_recordset_sql($sql, [$prefix . '%']); foreach ($rs as $info) { $table = strtolower($info->name); if (strpos($table, $prefix) !== 0) { // Incorrect table match caused by _. continue; } $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table); $empties[$table] = $table; } $rs->close(); return $empties; } else { return []; } } /** * Determine the next unique starting id sequences. * * @static * @param array $records The records to use to determine the starting value for the table. * @param string $table table name. * @return int The value the sequence should be set to. */ private static function get_next_sequence_starting_value($records, $table) { if (isset(self::$tablesequences[$table])) { return self::$tablesequences[$table]; } $id = self::$sequencenextstartingid; // If there are records, calculate the minimum id we can use. // It must be bigger than the last record's id. if (!empty($records)) { $lastrecord = end($records); $id = max($id, $lastrecord->id + 1); } self::$sequencenextstartingid = $id + 1000; self::$tablesequences[$table] = $id; return $id; } /** * Reset all database sequences to initial values. * * @static * @param array $empties tables that are known to be unmodified and empty * @return void */ public static function reset_all_database_sequences(?array $empties = null) { global $DB; if (!$data = self::get_tabledata()) { // Not initialised yet. return; } if (!$structure = self::get_tablestructure()) { // Not initialised yet. return; } $updatedtables = self::$tableupdated; // If all starting Id's are the same, it's difficult to detect coding and testing // errors that use the incorrect id in tests. The classic case is cmid vs instance id. // To reduce the chance of the coding error, we start sequences at different values where possible. // In a attempt to avoid tables with existing id's we start at a high number. // Reset the value each time all database sequences are reset. if (defined('PHPUNIT_SEQUENCE_START') && PHPUNIT_SEQUENCE_START) { self::$sequencenextstartingid = PHPUNIT_SEQUENCE_START; } else { self::$sequencenextstartingid = 100000; } $dbfamily = $DB->get_dbfamily(); if ($dbfamily === 'postgres') { $queries = []; $prefix = $DB->get_prefix(); foreach ($data as $table => $records) { // If table is not modified then no need to do anything. if (!isset($updatedtables[$table])) { continue; } if (isset($structure[$table]['id']) && $structure[$table]['id']->auto_increment) { $nextid = self::get_next_sequence_starting_value($records, $table); $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid"; } } if ($queries) { $DB->change_database_structure(implode(';', $queries)); } } else if ($dbfamily === 'mysql') { $queries = []; $sequences = []; $prefix = $DB->get_prefix(); $rs = $DB->get_recordset_sql("SHOW TABLE STATUS LIKE ?", [$prefix . '%']); foreach ($rs as $info) { $table = strtolower($info->name); if (strpos($table, $prefix) !== 0) { // Incorrect table match caused by _. continue; } if (!is_null($info->auto_increment)) { $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table); $sequences[$table] = $info->auto_increment; } } $rs->close(); $prefix = $DB->get_prefix(); foreach ($data as $table => $records) { // If table is not modified then no need to do anything. if (!isset($updatedtables[$table])) { continue; } if (isset($structure[$table]['id']) && $structure[$table]['id']->auto_increment) { if (isset($sequences[$table])) { $nextid = self::get_next_sequence_starting_value($records, $table); if ($sequences[$table] != $nextid) { $queries[] = "ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid"; } } else { // Some problem exists, fallback to standard code. $DB->get_manager()->reset_sequence($table); } } } if ($queries) { $DB->change_database_structure(implode(';', $queries)); } } else { // Note: does mssql support any kind of faster reset? // This also implies mssql will not use unique sequence values. if (is_null($empties) && (empty($updatedtables))) { $empties = self::guess_unmodified_empty_tables(); } foreach ($data as $table => $records) { // If table is not modified then no need to do anything. if (isset($empties[$table]) || (!isset($updatedtables[$table]))) { continue; } if (isset($structure[$table]['id']) && $structure[$table]['id']->auto_increment) { $DB->get_manager()->reset_sequence($table); } } } } /** * Reset all database tables to default values. * @static * @return bool true if reset done, false if skipped */ public static function reset_database() { global $DB; $tables = $DB->get_tables(false); if (!$tables || empty($tables['config'])) { // Not installed yet. return false; } if (!$data = self::get_tabledata()) { // Not initialised yet. return false; } if (!$structure = self::get_tablestructure()) { // Not initialised yet. return false; } $empties = []; // Use local copy of self::$tableupdated, as list gets updated in for loop. $updatedtables = self::$tableupdated; // If empty tablesequences list then it's the very first run. if (empty(self::$tablesequences) && (($DB->get_dbfamily() != 'mysql') && ($DB->get_dbfamily() != 'postgres'))) { // Only Mysql and Postgres support random sequence, so don't guess, just reset everything on very first run. $empties = self::guess_unmodified_empty_tables(); } // Check if any table has been modified by behat selenium process. if (defined('BEHAT_SITE_RUNNING')) { // Crazy way to reset :(. $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path(); if ($tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true)) { self::$tableupdated = array_merge(self::$tableupdated, $tablesupdated); unlink($tablesupdatedfile); } $updatedtables = self::$tableupdated; } foreach ($data as $table => $records) { // If table is not modified then no need to do anything. // $updatedtables tables is set after the first run, so check before checking for specific table update. if (!empty($updatedtables) && !isset($updatedtables[$table])) { continue; } if (empty($records)) { if (!isset($empties[$table])) { // Table has been modified and is not empty. $DB->delete_records($table, []); } continue; } if (isset($structure[$table]['id']) && $structure[$table]['id']->auto_increment) { $currentrecords = $DB->get_records($table, [], 'id ASC'); $changed = false; foreach ($records as $id => $record) { if (!isset($currentrecords[$id])) { $changed = true; break; } if ((array)$record != (array)$currentrecords[$id]) { $changed = true; break; } unset($currentrecords[$id]); } if (!$changed) { if ($currentrecords) { $lastrecord = end($records); $DB->delete_records_select($table, "id > ?", [$lastrecord->id]); continue; } else { continue; } } } $DB->delete_records($table, []); foreach ($records as $record) { $DB->import_record($table, $record, false, true); } } // Reset all next record ids - aka sequences. self::reset_all_database_sequences($empties); // Remove extra tables. foreach ($tables as $table) { if (!isset($data[$table])) { $DB->get_manager()->drop_table(new xmldb_table($table)); } } self::reset_updated_table_list(); return true; } /** * Purge dataroot directory * @static * @return void */ public static function reset_dataroot() { global $CFG; $childclassname = self::get_framework() . '_util'; // Do not delete automatically installed files. self::skip_original_data_files($childclassname); // Clear file status cache, before checking file_exists. clearstatcache(); // Clean up the dataroot folder. $handle = opendir(self::get_dataroot()); while (false !== ($item = readdir($handle))) { if (in_array($item, $childclassname::$datarootskiponreset)) { continue; } if (is_dir(self::get_dataroot() . "/$item")) { remove_dir(self::get_dataroot() . "/$item", false); } else { unlink(self::get_dataroot() . "/$item"); } } closedir($handle); // Clean up the dataroot/filedir folder. if (file_exists(self::get_dataroot() . '/filedir')) { $handle = opendir(self::get_dataroot() . '/filedir'); while (false !== ($item = readdir($handle))) { if (in_array('filedir' . DIRECTORY_SEPARATOR . $item, $childclassname::$datarootskiponreset)) { continue; } if (is_dir(self::get_dataroot() . "/filedir/$item")) { remove_dir(self::get_dataroot() . "/filedir/$item", false); } else { unlink(self::get_dataroot() . "/filedir/$item"); } } closedir($handle); } make_temp_directory(''); make_backup_temp_directory(''); make_cache_directory(''); make_localcache_directory(''); // Purge all data from the caches. This is required for consistency between tests. // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache) // and now we will purge any other caches as well. This must be done before the cache_factory::reset() as that // removes all definitions of caches and purge does not have valid caches to operate on. cache_helper::purge_all(); // Reset the cache API so that it recreates it's required directories as well. cache_factory::reset(); } /** * Gets a text-based site version description. * * @return string The site info */ public static function get_site_info() { global $CFG; $output = ''; // All developers have to understand English, do not localise! $env = self::get_environment(); $output .= "Moodle " . $env['moodleversion']; if ($hash = self::get_git_hash()) { $output .= ", $hash"; } $output .= "\n"; // Add php version. require_once($CFG->libdir . '/environmentlib.php'); $output .= "Php: " . normalize_version($env['phpversion']); // Add database type and version. $output .= ", " . $env['dbtype'] . ": " . $env['dbversion']; // OS details. $output .= ", OS: " . $env['os'] . "\n"; return $output; } /** * Try to get current git hash of the Moodle in $CFG->dirroot. * @return string null if unknown, sha1 hash if known */ public static function get_git_hash() { global $CFG; // This is a bit naive, but it should mostly work for all platforms. if (!file_exists("$CFG->dirroot/.git/HEAD")) { return null; } $headcontent = file_get_contents("$CFG->dirroot/.git/HEAD"); if ($headcontent === false) { return null; } $headcontent = trim($headcontent); // If it is pointing to a hash we return it directly. if (strlen($headcontent) === 40) { return $headcontent; } if (strpos($headcontent, 'ref: ') !== 0) { return null; } $ref = substr($headcontent, 5); if (!file_exists("$CFG->dirroot/.git/$ref")) { return null; } $hash = file_get_contents("$CFG->dirroot/.git/$ref"); if ($hash === false) { return null; } $hash = trim($hash); if (strlen($hash) != 40) { return null; } return $hash; } /** * Set state of modified tables. * * @param string $sql sql which is updating the table. */ public static function set_table_modified_by_sql($sql) { global $DB; $prefix = $DB->get_prefix(); preg_match('/( ' . $prefix . '\w*)(.*)/', $sql, $matches); // Ignore random sql for testing like "XXUPDATE SET XSSD". if (!empty($matches[1])) { $table = trim($matches[1]); $table = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table); self::$tableupdated[$table] = true; if (defined('BEHAT_SITE_RUNNING')) { $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path(); $tablesupdated = @json_decode(file_get_contents($tablesupdatedfile), true); if (!isset($tablesupdated[$table])) { $tablesupdated[$table] = true; @file_put_contents($tablesupdatedfile, json_encode($tablesupdated, JSON_PRETTY_PRINT)); } } } } /** * Reset updated table list. This should be done after every reset. */ public static function reset_updated_table_list() { self::$tableupdated = []; } /** * Delete tablesupdatedbyscenario file. This should be called before suite, * to ensure full db reset. */ public static function clean_tables_updated_by_scenario_list() { $tablesupdatedfile = self::get_tables_updated_by_scenario_list_path(); if (file_exists($tablesupdatedfile)) { unlink($tablesupdatedfile); } // Reset static cache of cli process. self::reset_updated_table_list(); } /** * Returns the path to the file which holds list of tables updated in scenario. * @return string */ final protected static function get_tables_updated_by_scenario_list_path() { return self::get_dataroot() . '/tablesupdatedbyscenario.json'; } /** * Drop the whole test database * @static * @param bool $displayprogress */ protected static function drop_database($displayprogress = false) { global $DB; $tables = $DB->get_tables(false); if (isset($tables['config'])) { // Config always last to prevent problems with interrupted drops! unset($tables['config']); $tables['config'] = 'config'; } if ($displayprogress) { echo "Dropping tables:\n"; } $dotsonline = 0; foreach ($tables as $tablename) { $table = new xmldb_table($tablename); $DB->get_manager()->drop_table($table); if ($dotsonline == 60) { if ($displayprogress) { echo "\n"; } $dotsonline = 0; } if ($displayprogress) { echo '.'; } $dotsonline += 1; } if ($displayprogress) { echo "\n"; } } /** * Drops the test framework dataroot * @static */ protected static function drop_dataroot() { global $CFG; $framework = self::get_framework(); $childclassname = $framework . '_util'; $files = scandir(self::get_dataroot() . '/' . $framework); foreach ($files as $file) { if (in_array($file, $childclassname::$datarootskipondrop)) { continue; } $path = self::get_dataroot() . '/' . $framework . '/' . $file; if (is_dir($path)) { remove_dir($path, false); } else { unlink($path); } } $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson; if (file_exists($jsonfilepath)) { // Delete the json file. unlink($jsonfilepath); // Delete the dataroot filedir. remove_dir(self::get_dataroot() . '/filedir', false); } } /** * Skip the original dataroot files to not been reset. * * @static * @param string $utilclassname the util class name.. */ protected static function skip_original_data_files($utilclassname) { $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson; if (file_exists($jsonfilepath)) { $listfiles = file_get_contents($jsonfilepath); // Mark each files as to not be reset. if (!empty($listfiles) && !self::$originaldatafilesjsonadded) { $originaldatarootfiles = json_decode($listfiles); // Keep the json file. Only drop_dataroot() should delete it. $originaldatarootfiles[] = self::$originaldatafilesjson; $utilclassname::$datarootskiponreset = array_merge( $utilclassname::$datarootskiponreset, $originaldatarootfiles ); self::$originaldatafilesjsonadded = true; } } } /** * Save the list of the original dataroot files into a json file. */ protected static function save_original_data_files() { global $CFG; $jsonfilepath = self::get_dataroot() . '/' . self::$originaldatafilesjson; // Save the original dataroot files if not done (only executed the first time). if (!file_exists($jsonfilepath)) { $listfiles = []; $currentdir = 'filedir' . DIRECTORY_SEPARATOR . '.'; $parentdir = 'filedir' . DIRECTORY_SEPARATOR . '..'; $listfiles[$currentdir] = $currentdir; $listfiles[$parentdir] = $parentdir; $filedir = self::get_dataroot() . '/filedir'; if (file_exists($filedir)) { $directory = new RecursiveDirectoryIterator($filedir); foreach (new RecursiveIteratorIterator($directory) as $file) { if ($file->isDir()) { $key = substr($file->getPath(), strlen(self::get_dataroot() . '/')); } else { $key = substr($file->getPathName(), strlen(self::get_dataroot() . '/')); } $listfiles[$key] = $key; } } // Save the file list in a JSON file. $fp = fopen($jsonfilepath, 'w'); fwrite($fp, json_encode(array_values($listfiles))); fclose($fp); } } /** * Return list of environment versions on which tests will run. * Environment includes: * - moodleversion * - phpversion * - dbtype * - dbversion * - os * * @return array */ public static function get_environment() { global $CFG, $DB; $env = []; // Add moodle version. $release = null; require("$CFG->dirroot/version.php"); $env['moodleversion'] = $release; // Add php version. $phpversion = phpversion(); $env['phpversion'] = $phpversion; // Add database type and version. $dbtype = $CFG->dbtype; $dbinfo = $DB->get_server_info(); $dbversion = $dbinfo['version']; $env['dbtype'] = $dbtype; $env['dbversion'] = $dbversion; // OS details. $osdetails = php_uname('s') . " " . php_uname('r') . " " . php_uname('m'); $env['os'] = $osdetails; return $env; } } classes/nasty_strings.php000064400000007272152267206310011635 0ustar00. /** * Nasty strings to use in tests. * * @package core * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * Nasty strings manager. * * Responds to nasty strings requests with a random string of the list * to try with different combinations in different places. * * @package core * @category test * @copyright 2013 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class nasty_strings { /** * List of different strings to fill fields and assert against them * * Non of these strings can be a part of another one, this would not be good * when using more one string at the same time and asserting results. * * @static * @var array */ protected static $strings = array( '< > & < > & \' \\" \ \'$@NULL@$ @@TEST@@ \\\" \\ , ; : . 日本語­% %%', '& \' \\" \ \'$@NULL@$ < > & < > @@TEST@@ \\\" \\ , ; : . 日本語­% %%', '< > & < > & \' \\" \ \\\" \\ , ; : . \'$@NULL@$ @@TEST@@ 日本語­% %%', '< > & < > & \' \\" \ \'$@NULL@$ 日本語­% %%@@TEST@@ \. \\" \\ , ; :', '< > & < > \\\" \\ , ; : . 日本語& \' \\" \ \'$@NULL@$ @@TEST@@­% %%', '\' \\" \ \'$@NULL@$ @@TEST@@ < > & < > & \\\" \\ , ; : . 日本語­% %%', '\\\" \\ , ; : . 日本語­% < > & < > & \' \\" \ \'$@NULL@$ @@TEST@@ %%', '< > & < > & \' \\" \ \'$@NULL@$ 日本語­% %% @@TEST@@ \\\" \\ . , ; :', '. 日本語& \' \\" < > & < > \\ , ; : \ \'$@NULL@$ \\\" @@TEST@@­% %%', '& \' \\" \ < > & < > \\\" \\ , ; : . 日本語\'$@NULL@$ @@TEST@@­% %%', ); /** * Already used nasty strings. * * This array will be cleaned before each scenario. * * @static * @var array */ protected static $usedstrings = array(); /** * Returns a nasty string and stores the key mapping. * * @static * @param string $key The key * @return string */ public static function get($key) { // If have been used during the this tests return it. if (isset(self::$usedstrings[$key])) { return self::$strings[self::$usedstrings[$key]]; } // Getting non-used random string. do { $index = self::random_index(); } while (in_array($index, self::$usedstrings)); // Mark the string as already used. self::$usedstrings[$key] = $index; return self::$strings[$index]; } /** * Resets the used strings var. * @static * @return void */ public static function reset_used_strings() { self::$usedstrings = array(); } /** * Returns a random index. * @static * @return int */ protected static function random_index() { return mt_rand(0, count(self::$strings) - 1); } } classes/tests_finder.php000064400000015720152267206310011414 0ustar00. /** * Tests finder * * @package core * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Finds components and plugins with tests * * @package core * @category test * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class tests_finder { /** * Returns all the components with tests of the specified type * @param string $testtype The kind of test we are looking for * @return array */ public static function get_components_with_tests($testtype) { // Get all the components $components = self::get_all_plugins_with_tests($testtype) + self::get_all_subsystems_with_tests($testtype); // Get all the directories having tests $directories = self::get_all_directories_with_tests($testtype); // Find any directory not covered by proper components $remaining = array_diff($directories, $components); // Add them to the list of components $components += $remaining; return $components; } /** * Returns all the plugins having tests * @param string $testtype The kind of test we are looking for * @return array all the plugins having tests */ private static function get_all_plugins_with_tests($testtype) { $pluginswithtests = array(); $plugintypes = core_component::get_plugin_types(); ksort($plugintypes); foreach ($plugintypes as $type => $unused) { $plugs = core_component::get_plugin_list($type); ksort($plugs); foreach ($plugs as $plug => $fullplug) { // Look for tests recursively if (self::directory_has_tests($fullplug, $testtype)) { $pluginswithtests[$type . '_' . $plug] = $fullplug; } } } return $pluginswithtests; } /** * Returns all the subsystems having tests * * Note we are hacking here the list of subsystems * to cover some well-known subsystems that are not properly * returned by the {@link get_core_subsystems()} function. * * @param string $testtype The kind of test we are looking for * @return array all the subsystems having tests */ private static function get_all_subsystems_with_tests($testtype) { global $CFG; $subsystemswithtests = array(); $subsystems = core_component::get_core_subsystems(); // Hack the list a bit to cover some well-known ones $subsystems['backup'] = $CFG->dirroot.'/backup'; $subsystems['db-dml'] = $CFG->dirroot.'/lib/dml'; $subsystems['db-ddl'] = $CFG->dirroot.'/lib/ddl'; ksort($subsystems); foreach ($subsystems as $subsys => $fullsubsys) { if ($fullsubsys === null) { continue; } if (!is_dir($fullsubsys)) { continue; } // Look for tests recursively if (self::directory_has_tests($fullsubsys, $testtype)) { $subsystemswithtests['core_' . $subsys] = $fullsubsys; } } return $subsystemswithtests; } /** * Returns all the directories having tests * * @param string $testtype The kind of test we are looking for * @return array all directories having tests */ private static function get_all_directories_with_tests($testtype) { global $CFG; // List of directories to exclude from test file searching. $excludedir = array('node_modules', 'vendor'); // Get first level directories in which tests should be searched. $directoriestosearch = array(); $alldirs = glob($CFG->dirroot . DIRECTORY_SEPARATOR . '*' , GLOB_ONLYDIR); foreach ($alldirs as $dir) { if (!in_array(basename($dir), $excludedir) && (filetype($dir) != 'link')) { $directoriestosearch[] = $dir; } } // Search for tests in valid directories. $dirs = array(); foreach ($directoriestosearch as $dir) { $dirite = new RecursiveDirectoryIterator($dir); $iteite = new RecursiveIteratorIterator($dirite); $regexp = self::get_regexp($testtype); $regite = new RegexIterator($iteite, $regexp); foreach ($regite as $path => $element) { $key = dirname(dirname($path)); $value = trim(str_replace(DIRECTORY_SEPARATOR, '_', str_replace($CFG->dirroot, '', $key)), '_'); $dirs[$key] = $value; } } ksort($dirs); return array_flip($dirs); } /** * Returns if a given directory has tests (recursively) * * @param string $dir full path to the directory to look for phpunit tests * @param string $testtype phpunit|behat * @return bool if a given directory has tests (true) or no (false) */ private static function directory_has_tests($dir, $testtype) { if (!is_dir($dir)) { return false; } $dirite = new RecursiveDirectoryIterator($dir); $iteite = new RecursiveIteratorIterator($dirite); $regexp = self::get_regexp($testtype); $regite = new RegexIterator($iteite, $regexp); $regite->rewind(); if ($regite->valid()) { return true; } return false; } /** * Returns the regular expression to match by the test files * @param string $testtype * @return string */ private static function get_regexp($testtype) { $sep = preg_quote(DIRECTORY_SEPARATOR, '|'); switch ($testtype) { case 'phpunit': $regexp = '|'.$sep.'tests'.$sep.'.*_test\.php$|'; break; case 'features': $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'.*\.feature$|'; break; case 'stepsdefinitions': $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'behat_.*\.php$|'; break; case 'behat': $regexp = '!'.$sep.'tests'.$sep.'behat'.$sep.'(.*\.feature)|(behat_.*\.php)$!'; break; } return $regexp; } } tests/testing_generator_test.php000064400000074700152267206310013215 0ustar00. namespace core; /** * Test data generator * * @package core * @category phpunit * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \testing_data_generator */ final class testing_generator_test extends \advanced_testcase { public function test_get_plugin_generator_good_case(): void { $generator = $this->getDataGenerator()->get_plugin_generator('core_question'); $this->assertInstanceOf('core_question_generator', $generator); } public function test_get_plugin_generator_sloppy_name(): void { $generator = $this->getDataGenerator()->get_plugin_generator('quiz'); $this->assertDebuggingCalled('Please specify the component you want a generator for as ' . 'mod_quiz, not quiz.', DEBUG_DEVELOPER); $this->assertInstanceOf('mod_quiz_generator', $generator); } public function test_get_default_generator(): void { $generator = $this->getDataGenerator()->get_plugin_generator('block_somethingthatdoesnotexist'); $this->assertInstanceOf('default_block_generator', $generator); } /** * Test plugin generator, with no component directory. */ public function test_get_plugin_generator_no_component_dir(): void { $this->expectException(\coding_exception::class); $this->expectExceptionMessage('Component core_cohort does not support generators yet. Missing tests/generator/lib.php.'); $generator = $this->getDataGenerator()->get_plugin_generator('core_cohort'); } public function test_create_user(): void { global $DB, $CFG; require_once($CFG->dirroot.'/user/lib.php'); $this->resetAfterTest(true); $generator = $this->getDataGenerator(); $count = $DB->count_records('user'); $this->setCurrentTimeStart(); $user = $generator->create_user(); $this->assertEquals($count + 1, $DB->count_records('user')); $this->assertSame($user->username, \core_user::clean_field($user->username, 'username')); $this->assertSame($user->email, \core_user::clean_field($user->email, 'email')); $this->assertNotEmpty($user->firstnamephonetic); $this->assertNotEmpty($user->lastnamephonetic); $this->assertNotEmpty($user->alternatename); $this->assertNotEmpty($user->middlename); $this->assertSame('manual', $user->auth); $this->assertSame('en', $user->lang); $this->assertSame('1', $user->confirmed); $this->assertSame('0', $user->deleted); $this->assertTimeCurrent($user->timecreated); $this->assertSame($user->timecreated, $user->timemodified); $this->assertSame('0.0.0.0', $user->lastip); $record = array( 'auth' => 'email', 'firstname' => 'Žluťoučký', 'lastname' => 'Koníček', 'firstnamephonetic' => 'Zhlutyoucky', 'lastnamephonetic' => 'Koniiczek', 'middlename' => 'Hopper', 'alternatename' => 'horse', 'idnumber' => 'abc1', 'mnethostid' => (string)$CFG->mnet_localhost_id, 'username' => 'konic666', 'password' => 'password1', 'email' => 'email@example.com', 'confirmed' => '1', 'maildisplay' => '1', 'mailformat' => '0', 'maildigest' => '1', 'autosubscribe' => '0', 'trackforums' => '0', 'deleted' => '0', 'timecreated' => '666', ); $user = $generator->create_user($record); $this->assertEquals($count + 2, $DB->count_records('user')); foreach ($record as $k => $v) { if ($k === 'password') { $this->assertTrue(password_verify($v, $user->password)); } else { $this->assertSame($v, $user->{$k}); } } $record = array( 'firstname' => 'Some', 'lastname' => 'User', 'idnumber' => 'def', 'username' => 'user666', 'email' => 'email666@example.com', 'deleted' => '1', ); $user = $generator->create_user($record); $this->assertEquals($count + 3, $DB->count_records('user')); $this->assertSame('', $user->idnumber); $this->assertSame(md5($record['username']), $user->email); $this->assertEquals(1, $user->deleted); // Test generating user with interests. $user = $generator->create_user(array('interests' => 'Cats, Dogs')); $userdetails = user_get_user_details($user); $this->assertSame('Cats, Dogs', $userdetails['interests']); } public function test_create(): void { global $DB; $this->resetAfterTest(true); $generator = $this->getDataGenerator(); $count = $DB->count_records('course_categories'); $category = $generator->create_category(); $this->assertEquals($count+1, $DB->count_records('course_categories')); $this->assertMatchesRegularExpression('/^Course category \d/', $category->name); $this->assertSame('', $category->idnumber); $this->assertMatchesRegularExpression('/^Test course category \d/', $category->description); $this->assertSame(FORMAT_MOODLE, $category->descriptionformat); $count = $DB->count_records('cohort'); $cohort = $generator->create_cohort(); $this->assertEquals($count+1, $DB->count_records('cohort')); $this->assertEquals(\context_system::instance()->id, $cohort->contextid); $this->assertMatchesRegularExpression('/^Cohort \d/', $cohort->name); $this->assertSame('', $cohort->idnumber); $this->assertMatchesRegularExpression("/^Description for '{$cohort->name}' \\n/", $cohort->description); $this->assertSame(FORMAT_MOODLE, $cohort->descriptionformat); $this->assertSame('', $cohort->component); $this->assertLessThanOrEqual(time(), $cohort->timecreated); $this->assertSame($cohort->timecreated, $cohort->timemodified); $count = $DB->count_records('course'); $course = $generator->create_course(); $this->assertEquals($count+1, $DB->count_records('course')); $this->assertMatchesRegularExpression('/^Test course \d/', $course->fullname); $this->assertMatchesRegularExpression('/^tc_\d/', $course->shortname); $this->assertSame('', $course->idnumber); $this->assertSame('topics', $course->format); $this->assertEquals(0, $course->newsitems); $this->assertEquals(5, course_get_format($course)->get_last_section_number()); $this->assertMatchesRegularExpression('/^Test course \d/', $course->summary); $this->assertSame(FORMAT_MOODLE, $course->summaryformat); $section = $generator->create_course_section(array('course'=>$course->id, 'section'=>3)); $this->assertEquals($course->id, $section->course); $this->assertNull($section->name); $course = $generator->create_course(array('tags' => 'Cat, Dog')); $this->assertEquals(array('Cat', 'Dog'), array_values(\core_tag_tag::get_item_tags_array('core', 'course', $course->id))); $scale = $generator->create_scale(); $this->assertNotEmpty($scale); } /** * Test case for the `test_create_course_initsections` method. * * This method tests the behavior of creating a course with initialized sections. * It checks that the sections are renamed to "Section x" when `initsections` is true, * and that the sections are not renamed when `initsections` is false. * * @covers ::create_course */ public function test_create_course_initsections(): void { global $DB; $this->resetAfterTest(true); $generator = $this->getDataGenerator(); // Check that the sections are renamed to "Section x" when initsections is true. $course = $generator->create_course(['initsections' => 1]); $sections = course_get_format($course)->get_sections(); foreach ($sections as $section) { if ($section->sectionnum > 0) { $this->assertEquals(get_string('section') . ' ' . $section->sectionnum, $section->name); } } // Check that the sections are not renamed when initsections is false. $course = $generator->create_course(['initsections' => 0]); $sections = course_get_format($course)->get_sections(); foreach ($sections as $section) { if ($section->sectionnum > 0) { $this->assertNull($section->name); } } } public function test_create_module(): void { global $CFG, $SITE, $DB; $this->setAdminUser(); if (!file_exists("$CFG->dirroot/mod/page/")) { $this->markTestSkipped('Can not find standard Page module'); } $this->resetAfterTest(true); $generator = $this->getDataGenerator(); $page = $generator->create_module('page', array('course'=>$SITE->id)); $this->assertNotEmpty($page); $cm = get_coursemodule_from_instance('page', $page->id, $SITE->id, true); $this->assertEquals(0, $cm->sectionnum); $page = $generator->create_module('page', array('course'=>$SITE->id), array('section'=>3)); $this->assertNotEmpty($page); $cm = get_coursemodule_from_instance('page', $page->id, $SITE->id, true); $this->assertEquals(3, $cm->sectionnum); $page = $generator->create_module('page', array('course' => $SITE->id, 'tags' => 'Cat, Dog')); $this->assertEquals(array('Cat', 'Dog'), array_values(\core_tag_tag::get_item_tags_array('core', 'course_modules', $page->cmid))); // Prepare environment to generate modules with all possible options. // Enable advanced functionality. $CFG->enablecompletion = 1; $CFG->enableavailability = 1; $CFG->enableoutcomes = 1; require_once($CFG->libdir.'/gradelib.php'); require_once($CFG->libdir.'/completionlib.php'); require_once($CFG->dirroot.'/rating/lib.php'); // Create a course with enabled completion. $course = $generator->create_course(array('enablecompletion' => true)); // Create new grading category in this course. $grade_category = new \grade_category(); $grade_category->courseid = $course->id; $grade_category->fullname = 'Grade category'; $grade_category->insert(); // Create group and grouping. $group = $generator->create_group(array('courseid' => $course->id)); $grouping = $generator->create_grouping(array('courseid' => $course->id)); $generator->create_grouping_group(array('groupid' => $group->id, 'groupingid' => $grouping->id)); // Prepare arrays with properties that we can both use for creating modules and asserting the data in created modules. // General properties. $optionsgeneral = array( 'visible' => 0, // Note: 'visibleold' will always be set to the same value as 'visible'. 'section' => 3, // Note: section will be created if does not exist. // Module supports FEATURE_IDNUMBER. 'cmidnumber' => 'IDNUM', // Note: alternatively can have key 'idnumber'. // Module supports FEATURE_GROUPS; 'groupmode' => SEPARATEGROUPS, // Note: will be reset to 0 if course groupmodeforce is set. // Module supports FEATURE_GROUPINGS. 'groupingid' => $grouping->id, ); // In case completion is enabled on site and for course every module can have manual completion. $featurecompletionmanual = array( 'completion' => COMPLETION_TRACKING_MANUAL, // "Students can manually mark activity as completed." 'completionexpected' => time() + 7 * DAYSECS, ); // Automatic completion is possible if module supports FEATURE_COMPLETION_TRACKS_VIEWS or FEATURE_GRADE_HAS_GRADE. // Note: completionusegrade is stored in DB and can be found in cm_info as 'completiongradeitemnumber' - either NULL or 0. // Note: module can have more autocompletion rules as defined in moodleform_mod::add_completion_rules(). $featurecompletionautomatic = array( 'completion' => COMPLETION_TRACKING_AUTOMATIC, // "Show activity as complete when conditions are met." 'completionview' => 1, // "Student must view this activity to complete it" 'completionusegrade' => 1, // "Student must receive a grade to complete this activity" 'completionpassgrade' => 1, // "Student must receive a passing grade to complete this activity" ); // Module supports FEATURE_RATE: $featurerate = array( 'assessed' => RATING_AGGREGATE_AVERAGE, // "Aggregate type" 'scale' => 100, // Either max grade or negative number for scale id. 'ratingtime' => 1, // "Restrict ratings to items with dates in this range". 'assesstimestart' => time() - DAYSECS, // Note: Will be ignored if neither 'assessed' nor 'ratingtime' is set. 'assesstimefinish' => time() + DAYSECS, // Note: Will be ignored if neither 'assessed' nor 'ratingtime' is set. ); // Module supports FEATURE_GRADE_HAS_GRADE: $featuregrade = array( 'grade' => 10, 'gradecat' => $grade_category->id, // Note: if $CFG->enableoutcomes is set, this can be set to -1 to automatically create new grade category. ); // Now let's create several modules with different options. $m1 = $generator->create_module('assign', array('course' => $course->id) + $optionsgeneral); $m2 = $generator->create_module('data', array('course' => $course->id) + $featurecompletionmanual + $featurerate); $m3 = $generator->create_module('assign', array('course' => $course->id) + $featurecompletionautomatic + $featuregrade); // We need id of the grading item for the second module to create availability dependency in the 3rd module. $gradingitem = \grade_item::fetch(array('courseid'=>$course->id, 'itemtype'=>'mod', 'itemmodule' => 'assign', 'iteminstance' => $m3->id)); // Now prepare option to create the 4th module with an availability condition. $optionsavailability = array( 'availability' => '{"op":"&","showc":[true],"c":[' . '{"type":"date","d":">=","t":' . (time() - WEEKSECS) . '}]}', ); // Create module with conditional availability. $m4 = $generator->create_module('assign', array('course' => $course->id) + $optionsavailability ); // Verifying that everything is generated correctly. $modinfo = get_fast_modinfo($course->id); $cm1 = $modinfo->cms[$m1->cmid]; $this->assertEquals($optionsgeneral['visible'], $cm1->visible); $this->assertEquals($optionsgeneral['section'], $cm1->sectionnum); // Note difference in key. $this->assertEquals($optionsgeneral['cmidnumber'], $cm1->idnumber); // Note difference in key. $this->assertEquals($optionsgeneral['groupmode'], $cm1->groupmode); $this->assertEquals($optionsgeneral['groupingid'], $cm1->groupingid); $cm2 = $modinfo->cms[$m2->cmid]; $this->assertEquals($featurecompletionmanual['completion'], $cm2->completion); $this->assertEquals($featurecompletionmanual['completionexpected'], $cm2->completionexpected); $this->assertEquals(null, $cm2->completiongradeitemnumber); // Rating info is stored in the module's table (in our test {data}). $data = $DB->get_record('data', array('id' => $m2->id)); $this->assertEquals($featurerate['assessed'], $data->assessed); $this->assertEquals($featurerate['scale'], $data->scale); $this->assertEquals($featurerate['assesstimestart'], $data->assesstimestart); $this->assertEquals($featurerate['assesstimefinish'], $data->assesstimefinish); // No validation for 'ratingtime'. It is only used in to enable/disable assesstime* when adding module. $cm3 = $modinfo->cms[$m3->cmid]; $this->assertEquals($featurecompletionautomatic['completion'], $cm3->completion); $this->assertEquals($featurecompletionautomatic['completionview'], $cm3->completionview); $this->assertEquals($featurecompletionautomatic['completionpassgrade'], $cm3->completionpassgrade); $this->assertEquals(0, $cm3->completiongradeitemnumber); // Zero instead of default null since 'completionusegrade' was set. $gradingitem = \grade_item::fetch(array('courseid'=>$course->id, 'itemtype'=>'mod', 'itemmodule' => 'assign', 'iteminstance' => $m3->id)); $this->assertEquals(0, $gradingitem->grademin); $this->assertEquals($featuregrade['grade'], $gradingitem->grademax); $this->assertEquals($featuregrade['gradecat'], $gradingitem->categoryid); $cm4 = $modinfo->cms[$m4->cmid]; $this->assertEquals($optionsavailability['availability'], $cm4->availability); } public function test_create_block(): void { global $CFG; if (!file_exists("$CFG->dirroot/blocks/online_users/")) { $this->markTestSkipped('Can not find standard Online users block'); } $this->resetAfterTest(true); $generator = $this->getDataGenerator(); $page = $generator->create_block('online_users'); $this->assertNotEmpty($page); } public function test_enrol_user(): void { global $DB; $this->resetAfterTest(); $selfplugin = enrol_get_plugin('self'); $this->assertNotEmpty($selfplugin); $manualplugin = enrol_get_plugin('manual'); $this->assertNotEmpty($manualplugin); // Prepare some data. $studentrole = $DB->get_record('role', array('shortname'=>'student')); $this->assertNotEmpty($studentrole); $teacherrole = $DB->get_record('role', array('shortname'=>'teacher')); $this->assertNotEmpty($teacherrole); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $context1 = \context_course::instance($course1->id); $context2 = \context_course::instance($course2->id); $context3 = \context_course::instance($course3->id); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $user4 = $this->getDataGenerator()->create_user(); $this->assertEquals(3, $DB->count_records('enrol', array('enrol'=>'self'))); $instance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'self'), '*', MUST_EXIST); $instance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'self'), '*', MUST_EXIST); $instance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'self'), '*', MUST_EXIST); $this->assertEquals($studentrole->id, $instance1->roleid); $this->assertEquals($studentrole->id, $instance2->roleid); $this->assertEquals($studentrole->id, $instance3->roleid); $this->assertEquals(3, $DB->count_records('enrol', array('enrol'=>'manual'))); $maninstance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'manual'), '*', MUST_EXIST); $maninstance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'manual'), '*', MUST_EXIST); $maninstance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'manual'), '*', MUST_EXIST); $maninstance3->roleid = $teacherrole->id; $DB->update_record('enrol', $maninstance3, array('id'=>$maninstance3->id)); $this->assertEquals($studentrole->id, $maninstance1->roleid); $this->assertEquals($studentrole->id, $maninstance2->roleid); $this->assertEquals($teacherrole->id, $maninstance3->roleid); $result = $this->getDataGenerator()->enrol_user($user1->id, $course1->id); $this->assertTrue($result); $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance1->id, 'userid'=>$user1->id))); $this->assertTrue($DB->record_exists('role_assignments', array('contextid'=>$context1->id, 'userid'=>$user1->id, 'roleid'=>$studentrole->id))); $result = $this->getDataGenerator()->enrol_user($user1->id, $course2->id, $teacherrole->id, 'manual'); $this->assertTrue($result); $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance2->id, 'userid'=>$user1->id))); $this->assertTrue($DB->record_exists('role_assignments', array('contextid'=>$context2->id, 'userid'=>$user1->id, 'roleid'=>$teacherrole->id))); $result = $this->getDataGenerator()->enrol_user($user4->id, $course2->id, 'teacher', 'manual'); $this->assertTrue($result); $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid' => $maninstance2->id, 'userid' => $user4->id))); $this->assertTrue($DB->record_exists('role_assignments', array('contextid' => $context2->id, 'userid' => $user4->id, 'roleid' => $teacherrole->id))); $result = $this->getDataGenerator()->enrol_user($user1->id, $course3->id, 0, 'manual'); $this->assertTrue($result); $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance3->id, 'userid'=>$user1->id))); $this->assertFalse($DB->record_exists('role_assignments', array('contextid'=>$context3->id, 'userid'=>$user1->id))); $result = $this->getDataGenerator()->enrol_user($user2->id, $course1->id, null, 'self'); $this->assertTrue($result); $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user2->id))); $this->assertTrue($DB->record_exists('role_assignments', array('contextid'=>$context1->id, 'userid'=>$user2->id, 'roleid'=>$studentrole->id))); $selfplugin->add_instance($course2, array('status'=>ENROL_INSTANCE_ENABLED, 'roleid'=>$teacherrole->id)); $result = $this->getDataGenerator()->enrol_user($user2->id, $course2->id, null, 'self'); $this->assertFalse($result); $DB->delete_records('enrol', array('enrol'=>'self', 'courseid'=>$course3->id)); $result = $this->getDataGenerator()->enrol_user($user2->id, $course3->id, null, 'self'); $this->assertFalse($result); } public function test_create_grade_category(): void { global $DB, $CFG; require_once $CFG->libdir . '/grade/constants.php'; $this->resetAfterTest(true); $generator = $this->getDataGenerator(); $course = $generator->create_course(); // Generate category and make sure number of records in DB table increases. // Note we only count grade cats with depth > 1 because the course grade category // is lazily created. $count = $DB->count_records_select('grade_categories', 'depth <> 1'); $gradecategory = $generator->create_grade_category(array('courseid'=>$course->id)); $this->assertEquals($count+1, $DB->count_records_select('grade_categories', 'depth <> 1')); $this->assertEquals(2, $gradecategory->depth); $this->assertEquals($course->id, $gradecategory->courseid); $this->assertEquals('Grade category 1', $gradecategory->fullname); // Generate category and make sure aggregation is set. $gradecategory = $generator->create_grade_category( array('courseid' => $course->id, 'aggregation' => GRADE_AGGREGATE_MEDIAN)); $this->assertEquals(GRADE_AGGREGATE_MEDIAN, $gradecategory->aggregation); // Generate category and make sure parent is set. $gradecategory2 = $generator->create_grade_category( array('courseid' => $course->id, 'parent' => $gradecategory->id)); $this->assertEquals($gradecategory->id, $gradecategory2->parent); } public function test_create_custom_profile_field_category(): void { global $DB; $this->resetAfterTest(); $generator = $this->getDataGenerator(); // Insert first category without specified sortorder. $result = $generator->create_custom_profile_field_category(['name' => 'Frogs']); $record = $DB->get_record('user_info_category', ['name' => 'Frogs']); $this->assertEquals(1, $record->sortorder); // Also check the return value. $this->assertEquals(1, $result->sortorder); $this->assertEquals('Frogs', $result->name); $this->assertEquals($record->id, $result->id); // Insert next category without specified sortorder. $generator->create_custom_profile_field_category(['name' => 'Zombies']); $record = $DB->get_record('user_info_category', ['name' => 'Zombies']); $this->assertEquals(2, $record->sortorder); // Insert category with specified sortorder. $generator->create_custom_profile_field_category(['name' => 'Toads', 'sortorder' => 9]); $record = $DB->get_record('user_info_category', ['name' => 'Toads']); $this->assertEquals(9, $record->sortorder); // Insert another with unspecified sortorder. $generator->create_custom_profile_field_category(['name' => 'Werewolves']); $record = $DB->get_record('user_info_category', ['name' => 'Werewolves']); $this->assertEquals(10, $record->sortorder); } public function test_create_custom_profile_field(): void { global $DB; $this->resetAfterTest(); $generator = $this->getDataGenerator(); // Insert minimal field without specified category. $field1 = $generator->create_custom_profile_field( ['datatype' => 'text', 'shortname' => 'colour', 'name' => 'Colour']); $record = $DB->get_record('user_info_field', ['shortname' => 'colour']); // Check specified values. $this->assertEquals('Colour', $record->name); $this->assertEquals('text', $record->datatype); // Check sortorder (first in category). $this->assertEquals(1, $record->sortorder); // Check shared defaults for most datatypes. $this->assertEquals('', $record->description); $this->assertEquals(0, $record->descriptionformat); $this->assertEquals(0, $record->required); $this->assertEquals(0, $record->locked); $this->assertEquals(PROFILE_VISIBLE_ALL, $record->visible); $this->assertEquals(0, $record->forceunique); $this->assertEquals(0, $record->signup); $this->assertEquals('', $record->defaultdata); $this->assertEquals(0, $record->defaultdataformat); // Check specific defaults for text datatype. $this->assertEquals(30, $record->param1); $this->assertEquals(2048, $record->param2); // Check the returned value matches the database data. $this->assertEquals($record, $field1); // The category should relate to a new 'testing' category. $catrecord = $DB->get_record('user_info_category', ['id' => $record->categoryid]); $this->assertEquals('Testing', $catrecord->name); $this->assertEquals(1, $catrecord->sortorder); // Create another field, this time supplying values for a few of the fields. $generator->create_custom_profile_field( ['datatype' => 'text', 'shortname' => 'brightness', 'name' => 'Brightness', 'required' => 1, 'forceunique' => 1]); $record = $DB->get_record('user_info_field', ['shortname' => 'brightness']); // Same testing category, next sortorder. $this->assertEquals($catrecord->id, $record->categoryid); $this->assertEquals(2, $record->sortorder); // Check modified fields. $this->assertEquals(1, $record->required); $this->assertEquals(1, $record->forceunique); // Create a field in specified category by id or name... $category = $generator->create_custom_profile_field_category(['name' => 'Amphibians']); $field3 = $generator->create_custom_profile_field( ['datatype' => 'text', 'shortname' => 'frog', 'name' => 'Frog', 'categoryid' => $category->id]); $this->assertEquals($category->id, $field3->categoryid); $this->assertEquals(1, $field3->sortorder); $field4 = $generator->create_custom_profile_field( ['datatype' => 'text', 'shortname' => 'toad', 'name' => 'Toad', 'category' => 'Amphibians', 'sortorder' => 4]); $this->assertEquals($category->id, $field4->categoryid); $this->assertEquals(4, $field4->sortorder); // Check defaults for menu, datetime, and checkbox. $field5 = $generator->create_custom_profile_field( ['datatype' => 'menu', 'shortname' => 'cuisine', 'name' => 'Cuisine']); $this->assertEquals("Yes\nNo", $field5->param1); $this->assertEquals('No', $field5->defaultdata); $field6 = $generator->create_custom_profile_field( ['datatype' => 'datetime', 'shortname' => 'epoch', 'name' => 'Epoch']); $this->assertEquals(2010, $field6->param1); $this->assertEquals(2015, $field6->param2); $this->assertEquals(1, $field6->param3); $field7 = $generator->create_custom_profile_field( ['datatype' => 'checkbox', 'shortname' => 'areyousure', 'name' => 'Are you sure?']); $this->assertEquals(0, $field7->defaultdata); // Check setting options for menu using \n work as expected. $field8 = $generator->create_custom_profile_field([ 'datatype' => 'menu', 'shortname' => 'cuisine', 'name' => 'Cuisine', 'param1' => 'French\nChinese\nLebanese', 'defaultdata' => 'Chinese' ]); $this->assertEquals("French\nChinese\nLebanese", $field8->param1); } } tests/util_test.php000064400000004555152267206310010450 0ustar00. namespace core\testing; /** * Testing util tests. * * @package core * @category phpunit * @copyright 2023 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class util_test extends \advanced_testcase { /** * Note: This test is required for the other two parts because the first time * a table is written to it may not have had the initial value reset. * * @coversNothing */ public function test_increment_reset_part_one(): void { global $DB; switch ($DB->get_dbfamily()) { case 'mssql': $this->markTestSkipped('MSSQL does not support sequences'); return; } $this->resetAfterTest(); $DB->insert_record('config_plugins', [ 'plugin' => 'example', 'name' => 'test_increment', 'value' => 0, ]); } /** * @coversNothing * @depends test_increment_reset_part_one */ public function test_increment_reset_part_two(): int { global $DB; $this->resetAfterTest(); return $DB->insert_record('config_plugins', [ 'plugin' => 'example', 'name' => 'test_increment', 'value' => 0, ]); } /** * @depends test_increment_reset_part_two */ public function test_increment_reset_part_three(int $previousid): void { global $DB; $this->resetAfterTest(); $id = $DB->insert_record('config_plugins', [ 'plugin' => 'example', 'name' => 'test_increment', 'value' => 0, ]); $this->assertEquals($previousid, $id); } } tests/clock_test.php000064400000007231152267206310010560 0ustar00. namespace core; use frozen_clock; use incrementing_clock; /** * Tests for testing clocks. * * @package core * @copyright 2024 Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class clock_test extends \advanced_testcase { /** * Test the incrementing mock clock. * * @covers \incrementing_clock */ public function test_clock_with_incrementing(): void { require_once(__DIR__ . '/../classes/incrementing_clock.php'); $clock = new incrementing_clock(); $this->assertInstanceOf(\incrementing_clock::class, $clock); $initialtime = $clock->now()->getTimestamp(); // Test the functionality. $this->assertEquals($initialtime + 1, $clock->now()->getTimestamp()); $this->assertEquals($initialtime + 2, $clock->time()); $this->assertEquals($initialtime + 3, $clock->now()->getTimestamp()); // Specify a specific start time. $clock = new incrementing_clock(12345); $this->assertEquals(12345, $clock->now()->getTimestamp()); $this->assertEquals(12346, $clock->time()); $this->assertEquals(12347, $clock->now()->getTimestamp()); $clock->set_to(12345); $this->assertEquals(12345, $clock->time()); $this->assertEquals(12346, $clock->time()); $clock->bump(); $this->assertEquals(12348, $clock->time()); $clock->bump(); $this->assertEquals(12350, $clock->time()); $clock->bump(5); $this->assertEquals(12356, $clock->time()); } /** * Test the incrementing mock clock. * * @covers \frozen_clock */ public function test_mock_clock_with_frozen(): void { require_once(__DIR__ . '/../classes/frozen_clock.php'); $clock = new frozen_clock(); // Test the functionality. $initialtime = $clock->now()->getTimestamp(); $this->assertEquals($initialtime, $clock->now()->getTimestamp()); $this->assertEquals($initialtime, $clock->now()->getTimestamp()); $this->assertEquals($initialtime, $clock->now()->getTimestamp()); $this->assertEquals($initialtime, $clock->time()); // Specify a specific start time. $clock = new frozen_clock(12345); $initialtime = $clock->now(); $this->assertEquals($initialtime, $clock->now()); $this->assertEquals($initialtime, $clock->now()); $this->assertEquals($initialtime, $clock->now()); $clock->set_to(12345); $this->assertEquals(12345, $clock->now()->getTimestamp()); $this->assertEquals(12345, $clock->now()->getTimestamp()); $this->assertEquals(12345, $clock->now()->getTimestamp()); $this->assertEquals(12345, $clock->time()); $clock->bump(); $this->assertEquals(12346, $clock->time()); $clock->bump(); $this->assertEquals(12347, $clock->time()); $clock->bump(5); $this->assertEquals(12352, $clock->time()); } }