Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Data generator.
  19   *
  20   * @package    core
  21   * @category   test
  22   * @copyright  2012 Petr Skoda {@link http://skodak.org}
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  /**
  29   * Data generator class for unit tests and other tools that need to create fake test sites.
  30   *
  31   * @package    core
  32   * @category   test
  33   * @copyright  2012 Petr Skoda {@link http://skodak.org}
  34   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class testing_data_generator {
  37      /** @var int The number of grade categories created */
  38      protected $gradecategorycounter = 0;
  39      /** @var int The number of grade items created */
  40      protected $gradeitemcounter = 0;
  41      /** @var int The number of grade outcomes created */
  42      protected $gradeoutcomecounter = 0;
  43      protected $usercounter = 0;
  44      protected $categorycount = 0;
  45      protected $cohortcount = 0;
  46      protected $coursecount = 0;
  47      protected $scalecount = 0;
  48      protected $groupcount = 0;
  49      protected $groupingcount = 0;
  50      protected $rolecount = 0;
  51      protected $tagcount = 0;
  52  
  53      /** @var array list of plugin generators */
  54      protected $generators = array();
  55  
  56      /** @var array lis of common last names */
  57      public $lastnames = array(
  58          'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller', 'Davis', 'García', 'Rodríguez', 'Wilson',
  59          'Müller', 'Schmidt', 'Schneider', 'Fischer', 'Meyer', 'Weber', 'Schulz', 'Wagner', 'Becker', 'Hoffmann',
  60          'Novák', 'Svoboda', 'Novotný', 'Dvořák', 'Černý', 'Procházková', 'Kučerová', 'Veselá', 'Horáková', 'Němcová',
  61          'Смирнов', 'Иванов', 'Кузнецов', 'Соколов', 'Попов', 'Лебедева', 'Козлова', 'Новикова', 'Морозова', 'Петрова',
  62          '王', '李', '张', '刘', '陈', '楊', '黃', '趙', '吳', '周',
  63          '佐藤', '鈴木', '高橋', '田中', '渡辺', '伊藤', '山本', '中村', '小林', '斎藤',
  64      );
  65  
  66      /** @var array lis of common first names */
  67      public $firstnames = array(
  68          'Jacob', 'Ethan', 'Michael', 'Jayden', 'William', 'Isabella', 'Sophia', 'Emma', 'Olivia', 'Ava',
  69          'Lukas', 'Leon', 'Luca', 'Timm', 'Paul', 'Leonie', 'Leah', 'Lena', 'Hanna', 'Laura',
  70          'Jakub', 'Jan', 'Tomáš', 'Lukáš', 'Matěj', 'Tereza', 'Eliška', 'Anna', 'Adéla', 'Karolína',
  71          'Даниил', 'Максим', 'Артем', 'Иван', 'Александр', 'София', 'Анастасия', 'Дарья', 'Мария', 'Полина',
  72          '伟', '伟', '芳', '伟', '秀英', '秀英', '娜', '秀英', '伟', '敏',
  73          '翔', '大翔', '拓海', '翔太', '颯太', '陽菜', 'さくら', '美咲', '葵', '美羽',
  74      );
  75  
  76      public $loremipsum = <<<EOD
  77  Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla non arcu lacinia neque faucibus fringilla. Vivamus porttitor turpis ac leo. Integer in sapien. Nullam eget nisl. Aliquam erat volutpat. Cras elementum. Mauris suscipit, ligula sit amet pharetra semper, nibh ante cursus purus, vel sagittis velit mauris vel metus. Integer malesuada. Nullam lectus justo, vulputate eget mollis sed, tempor sed magna. Mauris elementum mauris vitae tortor. Aliquam erat volutpat.
  78  Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Pellentesque ipsum. Cras pede libero, dapibus nec, pretium sit amet, tempor quis. Aliquam ante. Proin in tellus sit amet nibh dignissim sagittis. Vivamus porttitor turpis ac leo. Duis bibendum, lectus ut viverra rhoncus, dolor nunc faucibus libero, eget facilisis enim ipsum id lacus. In sem justo, commodo ut, suscipit at, pharetra vitae, orci. Aliquam erat volutpat. Nulla est.
  79  Vivamus luctus egestas leo. Aenean fermentum risus id tortor. Mauris dictum facilisis augue. Aliquam erat volutpat. Aliquam ornare wisi eu metus. Aliquam id dolor. Duis condimentum augue id magna semper rutrum. Donec iaculis gravida nulla. Pellentesque ipsum. Etiam dictum tincidunt diam. Quisque tincidunt scelerisque libero. Etiam egestas wisi a erat.
  80  Integer lacinia. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris tincidunt sem sed arcu. Nullam feugiat, turpis at pulvinar vulputate, erat libero tristique tellus, nec bibendum odio risus sit amet ante. Aliquam id dolor. Maecenas sollicitudin. Et harum quidem rerum facilis est et expedita distinctio. Mauris suscipit, ligula sit amet pharetra semper, nibh ante cursus purus, vel sagittis velit mauris vel metus. Nullam dapibus fermentum ipsum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Pellentesque sapien. Duis risus. Mauris elementum mauris vitae tortor. Suspendisse nisl. Integer rutrum, orci vestibulum ullamcorper ultricies, lacus quam ultricies odio, vitae placerat pede sem sit amet enim.
  81  In laoreet, magna id viverra tincidunt, sem odio bibendum justo, vel imperdiet sapien wisi sed libero. Proin pede metus, vulputate nec, fermentum fringilla, vehicula vitae, justo. Nullam justo enim, consectetuer nec, ullamcorper ac, vestibulum in, elit. Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? Maecenas lorem. Etiam posuere lacus quis dolor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Curabitur ligula sapien, pulvinar a vestibulum quis, facilisis vel sapien. Nam sed tellus id magna elementum tincidunt. Suspendisse nisl. Vivamus luctus egestas leo. Nulla non arcu lacinia neque faucibus fringilla. Etiam dui sem, fermentum vitae, sagittis id, malesuada in, quam. Etiam dictum tincidunt diam. Etiam commodo dui eget wisi. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Proin pede metus, vulputate nec, fermentum fringilla, vehicula vitae, justo. Duis ante orci, molestie vitae vehicula venenatis, tincidunt ac pede. Pellentesque sapien.
  82  EOD;
  83  
  84      /**
  85       * To be called from data reset code only,
  86       * do not use in tests.
  87       * @return void
  88       */
  89      public function reset() {
  90          $this->usercounter = 0;
  91          $this->categorycount = 0;
  92          $this->coursecount = 0;
  93          $this->scalecount = 0;
  94  
  95          foreach ($this->generators as $generator) {
  96              $generator->reset();
  97          }
  98      }
  99  
 100      /**
 101       * Return generator for given plugin or component.
 102       * @param string $component the component name, e.g. 'mod_forum' or 'core_question'.
 103       * @return component_generator_base or rather an instance of the appropriate subclass.
 104       */
 105      public function get_plugin_generator($component) {
 106          // Note: This global is included so that generator have access to it.
 107          // CFG is widely used in require statements.
 108          global $CFG;
 109          list($type, $plugin) = core_component::normalize_component($component);
 110          $cleancomponent = $type . '_' . $plugin;
 111          if ($cleancomponent != $component) {
 112              debugging("Please specify the component you want a generator for as " .
 113                      "{$cleancomponent}, not {$component}.", DEBUG_DEVELOPER);
 114              $component = $cleancomponent;
 115          }
 116  
 117          if (isset($this->generators[$component])) {
 118              return $this->generators[$component];
 119          }
 120  
 121          $dir = core_component::get_component_directory($component);
 122          $lib = $dir . '/tests/generator/lib.php';
 123          if (!$dir || !is_readable($lib)) {
 124              $this->generators[$component] = $this->get_default_plugin_generator($component);
 125  
 126              return $this->generators[$component];
 127          }
 128  
 129          include_once($lib);
 130          $classname = $component . '_generator';
 131  
 132          if (class_exists($classname)) {
 133              $this->generators[$component] = new $classname($this);
 134          } else {
 135              $this->generators[$component] = $this->get_default_plugin_generator($component, $classname);
 136          }
 137  
 138          return $this->generators[$component];
 139      }
 140  
 141      /**
 142       * Create a test user
 143       * @param array|stdClass $record
 144       * @param array $options
 145       * @return stdClass user record
 146       */
 147      public function create_user($record=null, array $options=null) {
 148          global $DB, $CFG;
 149          require_once($CFG->dirroot.'/user/lib.php');
 150  
 151          $this->usercounter++;
 152          $i = $this->usercounter;
 153  
 154          $record = (array)$record;
 155  
 156          if (!isset($record['auth'])) {
 157              $record['auth'] = 'manual';
 158          }
 159  
 160          if (!isset($record['firstname']) and !isset($record['lastname'])) {
 161              $country = rand(0, 5);
 162              $firstname = rand(0, 4);
 163              $lastname = rand(0, 4);
 164              $female = rand(0, 1);
 165              $record['firstname'] = $this->firstnames[($country*10) + $firstname + ($female*5)];
 166              $record['lastname'] = $this->lastnames[($country*10) + $lastname + ($female*5)];
 167  
 168          } else if (!isset($record['firstname'])) {
 169              $record['firstname'] = 'Firstname'.$i;
 170  
 171          } else if (!isset($record['lastname'])) {
 172              $record['lastname'] = 'Lastname'.$i;
 173          }
 174  
 175          if (!isset($record['firstnamephonetic'])) {
 176              $firstnamephonetic = rand(0, 59);
 177              $record['firstnamephonetic'] = $this->firstnames[$firstnamephonetic];
 178          }
 179  
 180          if (!isset($record['lastnamephonetic'])) {
 181              $lastnamephonetic = rand(0, 59);
 182              $record['lastnamephonetic'] = $this->lastnames[$lastnamephonetic];
 183          }
 184  
 185          if (!isset($record['middlename'])) {
 186              $middlename = rand(0, 59);
 187              $record['middlename'] = $this->firstnames[$middlename];
 188          }
 189  
 190          if (!isset($record['alternatename'])) {
 191              $alternatename = rand(0, 59);
 192              $record['alternatename'] = $this->firstnames[$alternatename];
 193          }
 194  
 195          if (!isset($record['idnumber'])) {
 196              $record['idnumber'] = '';
 197          }
 198  
 199          if (!isset($record['mnethostid'])) {
 200              $record['mnethostid'] = $CFG->mnet_localhost_id;
 201          }
 202  
 203          if (!isset($record['username'])) {
 204              $record['username'] = 'username'.$i;
 205              $j = 2;
 206              while ($DB->record_exists('user', array('username'=>$record['username'], 'mnethostid'=>$record['mnethostid']))) {
 207                  $record['username'] = 'username'.$i.'_'.$j;
 208                  $j++;
 209              }
 210          }
 211  
 212          if (isset($record['password'])) {
 213              $record['password'] = hash_internal_user_password($record['password']);
 214          }
 215  
 216          if (!isset($record['email'])) {
 217              $record['email'] = $record['username'].'@example.com';
 218          }
 219  
 220          if (!isset($record['confirmed'])) {
 221              $record['confirmed'] = 1;
 222          }
 223  
 224          if (!isset($record['lastip'])) {
 225              $record['lastip'] = '0.0.0.0';
 226          }
 227  
 228          $tobedeleted = !empty($record['deleted']);
 229          unset($record['deleted']);
 230  
 231          $userid = user_create_user($record, false, false);
 232  
 233          if ($extrafields = array_intersect_key($record, ['password' => 1, 'timecreated' => 1])) {
 234              $DB->update_record('user', ['id' => $userid] + $extrafields);
 235          }
 236  
 237          if (!$tobedeleted) {
 238              // All new not deleted users must have a favourite self-conversation.
 239              $selfconversation = \core_message\api::create_conversation(
 240                  \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
 241                  [$userid]
 242              );
 243              \core_message\api::set_favourite_conversation($selfconversation->id, $userid);
 244  
 245              // Save custom profile fields data.
 246              $hasprofilefields = array_filter($record, function($key){
 247                  return strpos($key, 'profile_field_') === 0;
 248              }, ARRAY_FILTER_USE_KEY);
 249              if ($hasprofilefields) {
 250                  require_once($CFG->dirroot.'/user/profile/lib.php');
 251                  $usernew = (object)(['id' => $userid] + $record);
 252                  profile_save_data($usernew);
 253              }
 254          }
 255  
 256          $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
 257  
 258          if (!$tobedeleted && isset($record['interests'])) {
 259              require_once($CFG->dirroot . '/user/editlib.php');
 260              if (!is_array($record['interests'])) {
 261                  $record['interests'] = preg_split('/\s*,\s*/', trim($record['interests']), -1, PREG_SPLIT_NO_EMPTY);
 262              }
 263              useredit_update_interests($user, $record['interests']);
 264          }
 265  
 266          \core\event\user_created::create_from_userid($userid)->trigger();
 267  
 268          if ($tobedeleted) {
 269              delete_user($user);
 270              $user = $DB->get_record('user', array('id' => $userid));
 271          }
 272          return $user;
 273      }
 274  
 275      /**
 276       * Create a test course category
 277       * @param array|stdClass $record
 278       * @param array $options
 279       * @return core_course_category course category record
 280       */
 281      public function create_category($record=null, array $options=null) {
 282          $this->categorycount++;
 283          $i = $this->categorycount;
 284  
 285          $record = (array)$record;
 286  
 287          if (!isset($record['name'])) {
 288              $record['name'] = 'Course category '.$i;
 289          }
 290  
 291          if (!isset($record['description'])) {
 292              $record['description'] = "Test course category $i\n$this->loremipsum";
 293          }
 294  
 295          if (!isset($record['idnumber'])) {
 296              $record['idnumber'] = '';
 297          }
 298  
 299          return core_course_category::create($record);
 300      }
 301  
 302      /**
 303       * Create test cohort.
 304       * @param array|stdClass $record
 305       * @param array $options
 306       * @return stdClass cohort record
 307       */
 308      public function create_cohort($record=null, array $options=null) {
 309          global $DB, $CFG;
 310          require_once("$CFG->dirroot/cohort/lib.php");
 311  
 312          $this->cohortcount++;
 313          $i = $this->cohortcount;
 314  
 315          $record = (array)$record;
 316  
 317          if (!isset($record['contextid'])) {
 318              $record['contextid'] = context_system::instance()->id;
 319          }
 320  
 321          if (!isset($record['name'])) {
 322              $record['name'] = 'Cohort '.$i;
 323          }
 324  
 325          if (!isset($record['idnumber'])) {
 326              $record['idnumber'] = '';
 327          }
 328  
 329          if (!isset($record['description'])) {
 330              $record['description'] = "Description for '{$record['name']}' \n$this->loremipsum";
 331          }
 332  
 333          if (!isset($record['descriptionformat'])) {
 334              $record['descriptionformat'] = FORMAT_MOODLE;
 335          }
 336  
 337          if (!isset($record['visible'])) {
 338              $record['visible'] = 1;
 339          }
 340  
 341          if (!isset($record['component'])) {
 342              $record['component'] = '';
 343          }
 344  
 345          $id = cohort_add_cohort((object)$record);
 346  
 347          return $DB->get_record('cohort', array('id'=>$id), '*', MUST_EXIST);
 348      }
 349  
 350      /**
 351       * Create a test course
 352       * @param array|stdClass $record
 353       * @param array $options with keys:
 354       *      'createsections'=>bool precreate all sections
 355       * @return stdClass course record
 356       */
 357      public function create_course($record=null, array $options=null) {
 358          global $DB, $CFG;
 359          require_once("$CFG->dirroot/course/lib.php");
 360  
 361          $this->coursecount++;
 362          $i = $this->coursecount;
 363  
 364          $record = (array)$record;
 365  
 366          if (!isset($record['fullname'])) {
 367              $record['fullname'] = 'Test course '.$i;
 368          }
 369  
 370          if (!isset($record['shortname'])) {
 371              $record['shortname'] = 'tc_'.$i;
 372          }
 373  
 374          if (!isset($record['idnumber'])) {
 375              $record['idnumber'] = '';
 376          }
 377  
 378          if (!isset($record['format'])) {
 379              $record['format'] = 'topics';
 380          }
 381  
 382          if (!isset($record['newsitems'])) {
 383              $record['newsitems'] = 0;
 384          }
 385  
 386          if (!isset($record['numsections'])) {
 387              $record['numsections'] = 5;
 388          }
 389  
 390          if (!isset($record['summary'])) {
 391              $record['summary'] = "Test course $i\n$this->loremipsum";
 392          }
 393  
 394          if (!isset($record['summaryformat'])) {
 395              $record['summaryformat'] = FORMAT_MOODLE;
 396          }
 397  
 398          if (!isset($record['category'])) {
 399              $record['category'] = $DB->get_field_select('course_categories', "MIN(id)", "parent=0");
 400          }
 401  
 402          if (!isset($record['startdate'])) {
 403              $record['startdate'] = usergetmidnight(time());
 404          }
 405  
 406          if (isset($record['tags']) && !is_array($record['tags'])) {
 407              $record['tags'] = preg_split('/\s*,\s*/', trim($record['tags']), -1, PREG_SPLIT_NO_EMPTY);
 408          }
 409  
 410          if (!empty($options['createsections']) && empty($record['numsections'])) {
 411              // Since Moodle 3.3 function create_course() automatically creates sections if numsections is specified.
 412              // For BC if 'createsections' is given but 'numsections' is not, assume the default value from config.
 413              $record['numsections'] = get_config('moodlecourse', 'numsections');
 414          }
 415  
 416          if (!empty($record['customfields'])) {
 417              foreach ($record['customfields'] as $field) {
 418                  $record['customfield_'.$field['shortname']] = $field['value'];
 419              }
 420          }
 421  
 422          $course = create_course((object)$record);
 423          context_course::instance($course->id);
 424  
 425          return $course;
 426      }
 427  
 428      /**
 429       * Create course section if does not exist yet
 430       * @param array|stdClass $record must contain 'course' and 'section' attributes
 431       * @param array|null $options
 432       * @return stdClass
 433       * @throws coding_exception
 434       */
 435      public function create_course_section($record = null, array $options = null) {
 436          global $DB;
 437  
 438          $record = (array)$record;
 439  
 440          if (empty($record['course'])) {
 441              throw new coding_exception('course must be present in testing_data_generator::create_course_section() $record');
 442          }
 443  
 444          if (!isset($record['section'])) {
 445              throw new coding_exception('section must be present in testing_data_generator::create_course_section() $record');
 446          }
 447  
 448          course_create_sections_if_missing($record['course'], $record['section']);
 449          return get_fast_modinfo($record['course'])->get_section_info($record['section']);
 450      }
 451  
 452      /**
 453       * Create a test block.
 454       *
 455       * The $record passed in becomes the basis for the new row added to the
 456       * block_instances table. You only need to supply the values of interest.
 457       * Any missing values have sensible defaults filled in, and ->blockname will be set based on $blockname.
 458       *
 459       * The $options array provides additional data, not directly related to what
 460       * will be inserted in the block_instance table, which may affect the block
 461       * that is created. The meanings of any data passed here depends on the particular
 462       * type of block being created.
 463       *
 464       * @param string $blockname the type of block to create. E.g. 'html'.
 465       * @param array|stdClass $record forms the basis for the entry to be inserted in the block_instances table.
 466       * @param array $options further, block-specific options to control how the block is created.
 467       * @return stdClass new block_instance record.
 468       */
 469      public function create_block($blockname, $record=null, array $options=array()) {
 470          $generator = $this->get_plugin_generator('block_'.$blockname);
 471          return $generator->create_instance($record, $options);
 472      }
 473  
 474      /**
 475       * Create a test activity module.
 476       *
 477       * The $record should contain the same data that you would call from
 478       * ->get_data() when the mod_[type]_mod_form is submitted, except that you
 479       * only need to supply values of interest. The only required value is
 480       * 'course'. Any missing values will have a sensible default supplied.
 481       *
 482       * The $options array provides additional data, not directly related to what
 483       * would come back from the module edit settings form, which may affect the activity
 484       * that is created. The meanings of any data passed here depends on the particular
 485       * type of activity being created.
 486       *
 487       * @param string $modulename the type of activity to create. E.g. 'forum' or 'quiz'.
 488       * @param array|stdClass $record data, as if from the module edit settings form.
 489       * @param array $options additional data that may affect how the module is created.
 490       * @return stdClass activity record new new record that was just inserted in the table
 491       *      like 'forum' or 'quiz', with a ->cmid field added.
 492       */
 493      public function create_module($modulename, $record=null, array $options=null) {
 494          $generator = $this->get_plugin_generator('mod_'.$modulename);
 495          return $generator->create_instance($record, $options);
 496      }
 497  
 498      /**
 499       * Create a test group for the specified course
 500       *
 501       * $record should be either an array or a stdClass containing infomation about the group to create.
 502       * At the very least it needs to contain courseid.
 503       * Default values are added for name, description, and descriptionformat if they are not present.
 504       *
 505       * This function calls groups_create_group() to create the group within the database.
 506       * @see groups_create_group
 507       * @param array|stdClass $record
 508       * @return stdClass group record
 509       */
 510      public function create_group($record) {
 511          global $DB, $CFG;
 512  
 513          require_once($CFG->dirroot . '/group/lib.php');
 514  
 515          $this->groupcount++;
 516          $i = str_pad($this->groupcount, 4, '0', STR_PAD_LEFT);
 517  
 518          $record = (array)$record;
 519  
 520          if (empty($record['courseid'])) {
 521              throw new coding_exception('courseid must be present in testing_data_generator::create_group() $record');
 522          }
 523  
 524          if (!isset($record['name'])) {
 525              $record['name'] = 'group-' . $i;
 526          }
 527  
 528          if (!isset($record['description'])) {
 529              $record['description'] = "Test Group $i\n{$this->loremipsum}";
 530          }
 531  
 532          if (!isset($record['descriptionformat'])) {
 533              $record['descriptionformat'] = FORMAT_MOODLE;
 534          }
 535  
 536          $id = groups_create_group((object)$record);
 537  
 538          // Allow tests to set group pictures.
 539          if (!empty($record['picturepath'])) {
 540              require_once($CFG->dirroot . '/lib/gdlib.php');
 541              $grouppicture = process_new_icon(\context_course::instance($record['courseid']), 'group', 'icon', $id,
 542                  $record['picturepath']);
 543  
 544              $DB->set_field('groups', 'picture', $grouppicture, ['id' => $id]);
 545  
 546              // Invalidate the group data as we've updated the group record.
 547              cache_helper::invalidate_by_definition('core', 'groupdata', array(), [$record['courseid']]);
 548          }
 549  
 550          return $DB->get_record('groups', array('id'=>$id));
 551      }
 552  
 553      /**
 554       * Create a test group member
 555       * @param array|stdClass $record
 556       * @throws coding_exception
 557       * @return boolean
 558       */
 559      public function create_group_member($record) {
 560          global $DB, $CFG;
 561  
 562          require_once($CFG->dirroot . '/group/lib.php');
 563  
 564          $record = (array)$record;
 565  
 566          if (empty($record['userid'])) {
 567              throw new coding_exception('user must be present in testing_util::create_group_member() $record');
 568          }
 569  
 570          if (!isset($record['groupid'])) {
 571              throw new coding_exception('group must be present in testing_util::create_group_member() $record');
 572          }
 573  
 574          if (!isset($record['component'])) {
 575              $record['component'] = null;
 576          }
 577          if (!isset($record['itemid'])) {
 578              $record['itemid'] = 0;
 579          }
 580  
 581          return groups_add_member($record['groupid'], $record['userid'], $record['component'], $record['itemid']);
 582      }
 583  
 584      /**
 585       * Create a test grouping for the specified course
 586       *
 587       * $record should be either an array or a stdClass containing infomation about the grouping to create.
 588       * At the very least it needs to contain courseid.
 589       * Default values are added for name, description, and descriptionformat if they are not present.
 590       *
 591       * This function calls groups_create_grouping() to create the grouping within the database.
 592       * @see groups_create_grouping
 593       * @param array|stdClass $record
 594       * @return stdClass grouping record
 595       */
 596      public function create_grouping($record) {
 597          global $DB, $CFG;
 598  
 599          require_once($CFG->dirroot . '/group/lib.php');
 600  
 601          $this->groupingcount++;
 602          $i = $this->groupingcount;
 603  
 604          $record = (array)$record;
 605  
 606          if (empty($record['courseid'])) {
 607              throw new coding_exception('courseid must be present in testing_data_generator::create_grouping() $record');
 608          }
 609  
 610          if (!isset($record['name'])) {
 611              $record['name'] = 'grouping-' . $i;
 612          }
 613  
 614          if (!isset($record['description'])) {
 615              $record['description'] = "Test Grouping $i\n{$this->loremipsum}";
 616          }
 617  
 618          if (!isset($record['descriptionformat'])) {
 619              $record['descriptionformat'] = FORMAT_MOODLE;
 620          }
 621  
 622          $id = groups_create_grouping((object)$record);
 623  
 624          return $DB->get_record('groupings', array('id'=>$id));
 625      }
 626  
 627      /**
 628       * Create a test grouping group
 629       * @param array|stdClass $record
 630       * @throws coding_exception
 631       * @return boolean
 632       */
 633      public function create_grouping_group($record) {
 634          global $DB, $CFG;
 635  
 636          require_once($CFG->dirroot . '/group/lib.php');
 637  
 638          $record = (array)$record;
 639  
 640          if (empty($record['groupingid'])) {
 641              throw new coding_exception('grouping must be present in testing::create_grouping_group() $record');
 642          }
 643  
 644          if (!isset($record['groupid'])) {
 645              throw new coding_exception('group must be present in testing_util::create_grouping_group() $record');
 646          }
 647  
 648          return groups_assign_grouping($record['groupingid'], $record['groupid']);
 649      }
 650  
 651      /**
 652       * Create an instance of a repository.
 653       *
 654       * @param string type of repository to create an instance for.
 655       * @param array|stdClass $record data to use to up set the instance.
 656       * @param array $options options
 657       * @return stdClass repository instance record
 658       * @since Moodle 2.5.1
 659       */
 660      public function create_repository($type, $record=null, array $options = null) {
 661          $generator = $this->get_plugin_generator('repository_'.$type);
 662          return $generator->create_instance($record, $options);
 663      }
 664  
 665      /**
 666       * Create an instance of a repository.
 667       *
 668       * @param string type of repository to create an instance for.
 669       * @param array|stdClass $record data to use to up set the instance.
 670       * @param array $options options
 671       * @return repository_type object
 672       * @since Moodle 2.5.1
 673       */
 674      public function create_repository_type($type, $record=null, array $options = null) {
 675          $generator = $this->get_plugin_generator('repository_'.$type);
 676          return $generator->create_type($record, $options);
 677      }
 678  
 679  
 680      /**
 681       * Create a test scale
 682       * @param array|stdClass $record
 683       * @param array $options
 684       * @return stdClass block instance record
 685       */
 686      public function create_scale($record=null, array $options=null) {
 687          global $DB;
 688  
 689          $this->scalecount++;
 690          $i = $this->scalecount;
 691  
 692          $record = (array)$record;
 693  
 694          if (!isset($record['name'])) {
 695              $record['name'] = 'Test scale '.$i;
 696          }
 697  
 698          if (!isset($record['scale'])) {
 699              $record['scale'] = 'A,B,C,D,F';
 700          }
 701  
 702          if (!isset($record['courseid'])) {
 703              $record['courseid'] = 0;
 704          }
 705  
 706          if (!isset($record['userid'])) {
 707              $record['userid'] = 0;
 708          }
 709  
 710          if (!isset($record['description'])) {
 711              $record['description'] = 'Test scale description '.$i;
 712          }
 713  
 714          if (!isset($record['descriptionformat'])) {
 715              $record['descriptionformat'] = FORMAT_MOODLE;
 716          }
 717  
 718          $record['timemodified'] = time();
 719  
 720          if (isset($record['id'])) {
 721              $DB->import_record('scale', $record);
 722              $DB->get_manager()->reset_sequence('scale');
 723              $id = $record['id'];
 724          } else {
 725              $id = $DB->insert_record('scale', $record);
 726          }
 727  
 728          return $DB->get_record('scale', array('id'=>$id), '*', MUST_EXIST);
 729      }
 730  
 731      /**
 732       * Creates a new role in the system.
 733       *
 734       * You can fill $record with the role 'name',
 735       * 'shortname', 'description' and 'archetype'.
 736       *
 737       * If an archetype is specified it's capabilities,
 738       * context where the role can be assigned and
 739       * all other properties are copied from the archetype;
 740       * if no archetype is specified it will create an
 741       * empty role.
 742       *
 743       * @param array|stdClass $record
 744       * @return int The new role id
 745       */
 746      public function create_role($record=null) {
 747          global $DB;
 748  
 749          $this->rolecount++;
 750          $i = $this->rolecount;
 751  
 752          $record = (array)$record;
 753  
 754          if (empty($record['shortname'])) {
 755              $record['shortname'] = 'role-' . $i;
 756          }
 757  
 758          if (empty($record['name'])) {
 759              $record['name'] = 'Test role ' . $i;
 760          }
 761  
 762          if (empty($record['description'])) {
 763              $record['description'] = 'Test role ' . $i . ' description';
 764          }
 765  
 766          if (empty($record['archetype'])) {
 767              $record['archetype'] = '';
 768          } else {
 769              $archetypes = get_role_archetypes();
 770              if (empty($archetypes[$record['archetype']])) {
 771                  throw new coding_exception('\'role\' requires the field \'archetype\' to specify a ' .
 772                      'valid archetype shortname (editingteacher, student...)');
 773              }
 774          }
 775  
 776          // Creates the role.
 777          if (!$newroleid = create_role($record['name'], $record['shortname'], $record['description'], $record['archetype'])) {
 778              throw new coding_exception('There was an error creating \'' . $record['shortname'] . '\' role');
 779          }
 780  
 781          // If no archetype was specified we allow it to be added to all contexts,
 782          // otherwise we allow it in the archetype contexts.
 783          if (!$record['archetype']) {
 784              $contextlevels = array_keys(context_helper::get_all_levels());
 785          } else {
 786              // Copying from the archetype default rol.
 787              $archetyperoleid = $DB->get_field(
 788                  'role',
 789                  'id',
 790                  array('shortname' => $record['archetype'], 'archetype' => $record['archetype'])
 791              );
 792              $contextlevels = get_role_contextlevels($archetyperoleid);
 793          }
 794          set_role_contextlevels($newroleid, $contextlevels);
 795  
 796          if ($record['archetype']) {
 797  
 798              // We copy all the roles the archetype can assign, override, switch to and view.
 799              if ($record['archetype']) {
 800                  $types = array('assign', 'override', 'switch', 'view');
 801                  foreach ($types as $type) {
 802                      $rolestocopy = get_default_role_archetype_allows($type, $record['archetype']);
 803                      foreach ($rolestocopy as $tocopy) {
 804                          $functionname = "core_role_set_{$type}_allowed";
 805                          $functionname($newroleid, $tocopy);
 806                      }
 807                  }
 808              }
 809  
 810              // Copying the archetype capabilities.
 811              $sourcerole = $DB->get_record('role', array('id' => $archetyperoleid));
 812              role_cap_duplicate($sourcerole, $newroleid);
 813          }
 814  
 815          return $newroleid;
 816      }
 817  
 818      /**
 819       * Create a tag.
 820       *
 821       * @param array|stdClass $record
 822       * @return stdClass the tag record
 823       */
 824      public function create_tag($record = null) {
 825          global $DB, $USER;
 826  
 827          $this->tagcount++;
 828          $i = $this->tagcount;
 829  
 830          $record = (array) $record;
 831  
 832          if (!isset($record['userid'])) {
 833              $record['userid'] = $USER->id;
 834          }
 835  
 836          if (!isset($record['rawname'])) {
 837              if (isset($record['name'])) {
 838                  $record['rawname'] = $record['name'];
 839              } else {
 840                  $record['rawname'] = 'Tag name ' . $i;
 841              }
 842          }
 843  
 844          // Attribute 'name' should be a lowercase version of 'rawname', if not set.
 845          if (!isset($record['name'])) {
 846              $record['name'] = core_text::strtolower($record['rawname']);
 847          } else {
 848              $record['name'] = core_text::strtolower($record['name']);
 849          }
 850  
 851          if (!isset($record['tagcollid'])) {
 852              $record['tagcollid'] = core_tag_collection::get_default();
 853          }
 854  
 855          if (!isset($record['description'])) {
 856              $record['description'] = 'Tag description';
 857          }
 858  
 859          if (!isset($record['descriptionformat'])) {
 860              $record['descriptionformat'] = FORMAT_MOODLE;
 861          }
 862  
 863          if (!isset($record['flag'])) {
 864              $record['flag'] = 0;
 865          }
 866  
 867          if (!isset($record['timemodified'])) {
 868              $record['timemodified'] = time();
 869          }
 870  
 871          $id = $DB->insert_record('tag', $record);
 872  
 873          return $DB->get_record('tag', array('id' => $id), '*', MUST_EXIST);
 874      }
 875  
 876      /**
 877       * Helper method which combines $defaults with the values specified in $record.
 878       * If $record is an object, it is converted to an array.
 879       * Then, for each key that is in $defaults, but not in $record, the value
 880       * from $defaults is copied.
 881       * @param array $defaults the default value for each field with
 882       * @param array|stdClass $record
 883       * @return array updated $record.
 884       */
 885      public function combine_defaults_and_record(array $defaults, $record) {
 886          $record = (array) $record;
 887  
 888          foreach ($defaults as $key => $defaults) {
 889              if (!array_key_exists($key, $record)) {
 890                  $record[$key] = $defaults;
 891              }
 892          }
 893          return $record;
 894      }
 895  
 896      /**
 897       * Simplified enrolment of user to course using default options.
 898       *
 899       * It is strongly recommended to use only this method for 'manual' and 'self' plugins only!!!
 900       *
 901       * @param int $userid
 902       * @param int $courseid
 903       * @param int|string $roleidorshortname optional role id or role shortname, use only with manual plugin
 904       * @param string $enrol name of enrol plugin,
 905       *     there must be exactly one instance in course,
 906       *     it must support enrol_user() method.
 907       * @param int $timestart (optional) 0 means unknown
 908       * @param int $timeend (optional) 0 means forever
 909       * @param int $status (optional) default to ENROL_USER_ACTIVE for new enrolments
 910       * @return bool success
 911       */
 912      public function enrol_user($userid, $courseid, $roleidorshortname = null, $enrol = 'manual',
 913              $timestart = 0, $timeend = 0, $status = null) {
 914          global $DB;
 915  
 916          // If role is specified by shortname, convert it into an id.
 917          if (!is_numeric($roleidorshortname) && is_string($roleidorshortname)) {
 918              $roleid = $DB->get_field('role', 'id', array('shortname' => $roleidorshortname), MUST_EXIST);
 919          } else {
 920              $roleid = $roleidorshortname;
 921          }
 922  
 923          if (!$plugin = enrol_get_plugin($enrol)) {
 924              return false;
 925          }
 926  
 927          $instances = $DB->get_records('enrol', array('courseid'=>$courseid, 'enrol'=>$enrol));
 928          if (count($instances) != 1) {
 929              return false;
 930          }
 931          $instance = reset($instances);
 932  
 933          if (is_null($roleid) and $instance->roleid) {
 934              $roleid = $instance->roleid;
 935          }
 936  
 937          $plugin->enrol_user($instance, $userid, $roleid, $timestart, $timeend, $status);
 938          return true;
 939      }
 940  
 941      /**
 942       * Assigns the specified role to a user in the context.
 943       *
 944       * @param int $roleid
 945       * @param int $userid
 946       * @param int $contextid Defaults to the system context
 947       * @return int new/existing id of the assignment
 948       */
 949      public function role_assign($roleid, $userid, $contextid = false) {
 950  
 951          // Default to the system context.
 952          if (!$contextid) {
 953              $context = context_system::instance();
 954              $contextid = $context->id;
 955          }
 956  
 957          if (empty($roleid)) {
 958              throw new coding_exception('roleid must be present in testing_data_generator::role_assign() arguments');
 959          }
 960  
 961          if (empty($userid)) {
 962              throw new coding_exception('userid must be present in testing_data_generator::role_assign() arguments');
 963          }
 964  
 965          return role_assign($roleid, $userid, $contextid);
 966      }
 967  
 968      /**
 969       * Create a grade_category.
 970       *
 971       * @param array|stdClass $record
 972       * @return stdClass the grade category record
 973       */
 974      public function create_grade_category($record = null) {
 975          global $CFG;
 976  
 977          $this->gradecategorycounter++;
 978  
 979          $record = (array)$record;
 980  
 981          if (empty($record['courseid'])) {
 982              throw new coding_exception('courseid must be present in testing::create_grade_category() $record');
 983          }
 984  
 985          if (!isset($record['fullname'])) {
 986              $record['fullname'] = 'Grade category ' . $this->gradecategorycounter;
 987          }
 988  
 989          // For gradelib classes.
 990          require_once($CFG->libdir . '/gradelib.php');
 991          // Create new grading category in this course.
 992          $gradecategory = new grade_category(array('courseid' => $record['courseid']), false);
 993          $gradecategory->apply_default_settings();
 994          grade_category::set_properties($gradecategory, $record);
 995          $gradecategory->apply_forced_settings();
 996          $gradecategory->insert();
 997  
 998          // This creates a default grade item for the category
 999          $gradeitem = $gradecategory->load_grade_item();
1000  
1001          $gradecategory->update_from_db();
1002          return $gradecategory->get_record_data();
1003      }
1004  
1005      /**
1006       * Create a grade_item.
1007       *
1008       * @param array|stdClass $record
1009       * @return stdClass the grade item record
1010       */
1011      public function create_grade_item($record = null) {
1012          global $CFG;
1013          require_once("$CFG->libdir/gradelib.php");
1014  
1015          $this->gradeitemcounter++;
1016  
1017          if (!isset($record['itemtype'])) {
1018              $record['itemtype'] = 'manual';
1019          }
1020  
1021          if (!isset($record['itemname'])) {
1022              $record['itemname'] = 'Grade item ' . $this->gradeitemcounter;
1023          }
1024  
1025          if (isset($record['outcomeid'])) {
1026              $outcome = new grade_outcome(array('id' => $record['outcomeid']));
1027              $record['scaleid'] = $outcome->scaleid;
1028          }
1029          if (isset($record['scaleid'])) {
1030              $record['gradetype'] = GRADE_TYPE_SCALE;
1031          } else if (!isset($record['gradetype'])) {
1032              $record['gradetype'] = GRADE_TYPE_VALUE;
1033          }
1034  
1035          // Create new grade item in this course.
1036          $gradeitem = new grade_item($record, false);
1037          $gradeitem->insert();
1038  
1039          $gradeitem->update_from_db();
1040          return $gradeitem->get_record_data();
1041      }
1042  
1043      /**
1044       * Create a grade_outcome.
1045       *
1046       * @param array|stdClass $record
1047       * @return stdClass the grade outcome record
1048       */
1049      public function create_grade_outcome($record = null) {
1050          global $CFG;
1051  
1052          $this->gradeoutcomecounter++;
1053          $i = $this->gradeoutcomecounter;
1054  
1055          if (!isset($record['fullname'])) {
1056              $record['fullname'] = 'Grade outcome ' . $i;
1057          }
1058  
1059          // For gradelib classes.
1060          require_once($CFG->libdir . '/gradelib.php');
1061          // Create new grading outcome in this course.
1062          $gradeoutcome = new grade_outcome($record, false);
1063          $gradeoutcome->insert();
1064  
1065          $gradeoutcome->update_from_db();
1066          return $gradeoutcome->get_record_data();
1067      }
1068  
1069      /**
1070       * Helper function used to create an LTI tool.
1071       *
1072       * @param array $data
1073       * @return stdClass the tool
1074       */
1075      public function create_lti_tool($data = array()) {
1076          global $DB;
1077  
1078          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1079          $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
1080  
1081          // Create a course if no course id was specified.
1082          if (empty($data->courseid)) {
1083              $course = $this->create_course();
1084              $data->courseid = $course->id;
1085          } else {
1086              $course = get_course($data->courseid);
1087          }
1088  
1089          if (!empty($data->cmid)) {
1090              $data->contextid = context_module::instance($data->cmid)->id;
1091          } else {
1092              $data->contextid = context_course::instance($data->courseid)->id;
1093          }
1094  
1095          // Set it to enabled if no status was specified.
1096          if (!isset($data->status)) {
1097              $data->status = ENROL_INSTANCE_ENABLED;
1098          }
1099  
1100          // Add some extra necessary fields to the data.
1101          $data->name = 'Test LTI';
1102          $data->roleinstructor = $studentrole->id;
1103          $data->rolelearner = $teacherrole->id;
1104  
1105          // Get the enrol LTI plugin.
1106          $enrolplugin = enrol_get_plugin('lti');
1107          $instanceid = $enrolplugin->add_instance($course, (array) $data);
1108  
1109          // Get the tool associated with this instance.
1110          return $DB->get_record('enrol_lti_tools', array('enrolid' => $instanceid));
1111      }
1112  
1113      /**
1114       * Helper function used to create an event.
1115       *
1116       * @param   array   $data
1117       * @return  stdClass
1118       */
1119      public function create_event($data = []) {
1120          global $CFG;
1121  
1122          require_once($CFG->dirroot . '/calendar/lib.php');
1123          $record = new \stdClass();
1124          $record->name = 'event name';
1125          $record->repeat = 0;
1126          $record->repeats = 0;
1127          $record->timestart = time();
1128          $record->timeduration = 0;
1129          $record->timesort = 0;
1130          $record->eventtype = 'user';
1131          $record->courseid = 0;
1132          $record->categoryid = 0;
1133  
1134          foreach ($data as $key => $value) {
1135              $record->$key = $value;
1136          }
1137  
1138          switch ($record->eventtype) {
1139              case 'user':
1140                  unset($record->categoryid);
1141                  unset($record->courseid);
1142                  unset($record->groupid);
1143                  break;
1144              case 'group':
1145                  unset($record->categoryid);
1146                  break;
1147              case 'course':
1148                  unset($record->categoryid);
1149                  unset($record->groupid);
1150                  break;
1151              case 'category':
1152                  unset($record->courseid);
1153                  unset($record->groupid);
1154                  break;
1155              case 'site':
1156                  unset($record->categoryid);
1157                  unset($record->courseid);
1158                  unset($record->groupid);
1159                  break;
1160          }
1161  
1162          $event = new calendar_event($record);
1163          $event->create($record);
1164  
1165          return $event->properties();
1166      }
1167  
1168      /**
1169       * Create a new course custom field category with the given name.
1170       *
1171       * @param   array $data Array with data['name'] of category
1172       * @return  \core_customfield\category_controller   The created category
1173       */
1174      public function create_custom_field_category($data) : \core_customfield\category_controller {
1175          return $this->get_plugin_generator('core_customfield')->create_category($data);
1176      }
1177  
1178      /**
1179       * Create a new custom field
1180       *
1181       * @param   array $data Array with 'name', 'shortname' and 'type' of the field
1182       * @return  \core_customfield\field_controller   The created field
1183       */
1184      public function create_custom_field($data) : \core_customfield\field_controller {
1185          global $DB;
1186          if (empty($data['categoryid']) && !empty($data['category'])) {
1187              $data['categoryid'] = $DB->get_field('customfield_category', 'id', ['name' => $data['category']]);
1188              unset($data['category']);
1189          }
1190          return $this->get_plugin_generator('core_customfield')->create_field($data);
1191      }
1192  
1193      /**
1194       * Create a new user, and enrol them in the specified course as the supplied role.
1195       *
1196       * @param   \stdClass   $course The course to enrol in
1197       * @param   string      $role The role to give within the course
1198       * @param   \stdClass   $userparams User parameters
1199       * @return  \stdClass   The created user
1200       */
1201      public function create_and_enrol($course, $role = 'student', $userparams = null, $enrol = 'manual',
1202              $timestart = 0, $timeend = 0, $status = null) {
1203          global $DB;
1204  
1205          $user = $this->create_user($userparams);
1206          $roleid = $DB->get_field('role', 'id', ['shortname' => $role ]);
1207  
1208          $this->enrol_user($user->id, $course->id, $roleid, $enrol, $timestart, $timeend, $status);
1209  
1210          return $user;
1211      }
1212  
1213      /**
1214       * Create a new last access record for a given user in a course.
1215       *
1216       * @param   \stdClass   $user The user
1217       * @param   \stdClass   $course The course the user accessed
1218       * @param   int         $timestamp The timestamp for when the user last accessed the course
1219       * @return  \stdClass   The user_lastaccess record
1220       */
1221      public function create_user_course_lastaccess(\stdClass $user, \stdClass $course, int $timestamp): \stdClass {
1222          global $DB;
1223  
1224          $record = [
1225              'userid' => $user->id,
1226              'courseid' => $course->id,
1227              'timeaccess' => $timestamp,
1228          ];
1229  
1230          $recordid = $DB->insert_record('user_lastaccess', $record);
1231  
1232          return $DB->get_record('user_lastaccess', ['id' => $recordid], '*', MUST_EXIST);
1233      }
1234  
1235      /**
1236       * Gets a default generator for a given component.
1237       *
1238       * @param string $component The component name, e.g. 'mod_forum' or 'core_question'.
1239       * @param string $classname The name of the class missing from the generators file.
1240       * @return component_generator_base The generator.
1241       */
1242      protected function get_default_plugin_generator(string $component, ?string $classname = null) {
1243          [$type, $plugin] = core_component::normalize_component($component);
1244  
1245          switch ($type) {
1246              case 'block':
1247                  return new default_block_generator($this, $plugin);
1248          }
1249  
1250          if (is_null($classname)) {
1251              throw new coding_exception("Component {$component} does not support " .
1252                  "generators yet. Missing tests/generator/lib.php.");
1253          }
1254  
1255          throw new coding_exception("Component {$component} does not support " .
1256              "data generators yet. Class {$classname} not found.");
1257      }
1258  
1259  }