Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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

   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   * Base class for data generators component support for acceptance testing.
  19   *
  20   * @package   core
  21   * @category  test
  22   * @copyright 2012 David MonllaĆ³
  23   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  require_once (__DIR__ . '/../../behat/behat_base.php');
  29  
  30  use Behat\Gherkin\Node\TableNode as TableNode;
  31  use Behat\Behat\Tester\Exception\PendingException as PendingException;
  32  
  33  /**
  34   * Class to quickly create Behat test data using component data generators.
  35   *
  36   * There is a subclass of class for each component that wants to be able to
  37   * generate entities using the Behat step
  38   *     Given the following "entity types" exist:
  39   *       | test | data |
  40   *
  41   * For core entities, the entity type is like "courses" or "users" and
  42   * generating those is handled by behat_core_generator. For other components
  43   * the entity type is like "mod_quiz > User override" and that is handled by
  44   * behat_mod_quiz_generator defined in mod/quiz/tests/generator/behat_mod_quiz_generator.php.
  45   *
  46   * The types of entities that can be generated are described by the array returned
  47   * by the {@link get_generateable_entities()} method. The list in
  48   * {@link behat_core_generator} is a good (if complex) example.
  49   *
  50   * How things work is best explained with a few examples. All this is implemented
  51   * in the {@link generate_items()} method below, if you want to see every detail of
  52   * how it works.
  53   *
  54   * Simple example from behat_core_generator:
  55   * 'users' => [
  56   *     'datagenerator' => 'user',
  57   *     'required' => ['username'],
  58   * ],
  59   * The steps performed are:
  60   *
  61   * 1. 'datagenerator' => 'user' means that the word used in the method names below is 'user'.
  62   *
  63   * 2. Because 'required' is present, check the supplied data exists 'username' column is present
  64   *    in the supplied data table and if not display an error.
  65   *
  66   * 3. Then for each row in the table as an array $elementdata (array keys are column names)
  67   *    and process it as follows
  68   *
  69   * 4. (Not used in this example.)
  70   *
  71   * 5. If the method 'preprocess_user' exists, then call it to update $elementdata.
  72   *    (It does, in this case it sets the password to the username, if password was not given.)
  73   *
  74   * We then do one of 4 things:
  75   *
  76   * 6a. If there is a method 'process_user' we call it. (It doesn't for user,
  77   *     but there are other examples like process_enrol_user() in behat_core_generator.)
  78   *
  79   * 6b. (Not used in this example.)
  80   *
  81   * 6c. Else, if testing_data_generator::create_user exists, we call it with $elementdata. (it does.)
  82   *
  83   * 6d. If none of these three things work. an error is thrown.
  84   *
  85   * To understand the missing steps above, consider the example from behat_mod_quiz_generator:
  86   * 'group override' => [
  87   *      'datagenerator' => 'override',
  88   *      'required' => ['quiz', 'group'],
  89   *      'switchids' => ['quiz' => 'quiz', 'group' => 'groupid'],
  90   * ],
  91   * Processing is as above, except that:
  92   *
  93   * 1. Note 'datagenerator' is 'override' (not group_override). 'user override' maps to the
  94   *    same datagenerator. This works fine.
  95   *
  96   * 4. Because 'switchids' is present, human-readable data in the table gets converted to ids.
  97   *    They array key 'group' refers to a column which may be present in the table (it will be
  98   *    here because it is required, but it does not have to be in general). If that column
  99   *    is present and contains a value, then the method matching name like get_group_id() is
 100   *    called with the value from that column in the data table. You must implement this
 101   *    method. You can see several examples of this sort of method below.
 102   *
 103   *    If that method returns a group id, then $elementdata['group'] is unset and
 104   *    $elementdata['groupid'] is set to the result of the get_group_id() call. 'groupid' here
 105   *    because of the definition is 'switchids' => [..., 'group' => 'groupid'].
 106   *    If get_group_id() cannot find the group, it should throw a helpful exception.
 107   *
 108   *    Similarly, 'quiz' (the quiz name) is looked up with a call to get_quiz_id(). Here, the
 109   *    new array key set matches the old one removed. This is fine.
 110   *
 111   * 6b. We are in a plugin, so before checking whether testing_data_generator::create_override
 112   *     exists we first check whether mod_quiz_generator::create_override() exists. It does,
 113   *     and this is what gets called.
 114   *
 115   * This second example shows why the get_..._id methods for core entities are in this base
 116   * class, not in behat_core_generator. Plugins may need to look up the ids of
 117   * core entities.
 118   *
 119   * behat_core_generator is defined in lib/behat/classes/behat_core_generator.php
 120   * and for components, behat_..._generator is defined in tests/generator/behat_..._generator.php
 121   * inside the plugin. For example behat_mod_quiz_generator is defined in
 122   * mod/quiz/tests/generator/behat_mod_quiz_generator.php.
 123   *
 124   * @package   core
 125   * @category  test
 126   * @copyright 2012 David MonllaĆ³
 127   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 128   */
 129  abstract class behat_generator_base {
 130  
 131      /**
 132       * @var string the name of the component we belong to.
 133       *
 134       * This should probably only be used to make error messages clearer.
 135       */
 136      protected $component;
 137  
 138      /**
 139       * @var testing_data_generator the core data generator
 140       */
 141      protected $datagenerator;
 142  
 143      /**
 144       * @var testing_data_generator the data generator for this component.
 145       */
 146      protected $componentdatagenerator;
 147  
 148      /**
 149       * Constructor.
 150       *
 151       * @param string $component component name, to make error messages more readable.
 152       */
 153      public function __construct(string $component) {
 154          $this->component = $component;
 155      }
 156  
 157      /**
 158       * Get a list of the entities that can be created for this component.
 159       *
 160       * This function must be overridden in subclasses. See class comment
 161       * above for a description of the data structure.
 162       * See {@link behat_core_generator} for an example.
 163       *
 164       * @return array entity name => information about how to generate.
 165       */
 166      protected abstract function get_creatable_entities(): array;
 167  
 168      /**
 169       * Get the list of available generators for this class.
 170       *
 171       * @return array
 172       */
 173      final public function get_available_generators(): array {
 174          return $this->get_creatable_entities();
 175      }
 176  
 177      /**
 178       * Do the work to generate an entity.
 179       *
 180       * This is called by {@link behat_data_generators::the_following_entities_exist()}.
 181       *
 182       * @param string    $generatortype The name of the entity to create.
 183       * @param TableNode $data from the step.
 184       * @param bool      $singular Whether there is only one record and it is pivotted
 185       */
 186      public function generate_items(string $generatortype, TableNode $data, bool $singular = false) {
 187          // Now that we need them require the data generators.
 188          require_once (__DIR__ . '/../../testing/generator/lib.php');
 189  
 190          $elements = $this->get_creatable_entities();
 191  
 192          foreach ($elements as $key => $configuration) {
 193              if (array_key_exists('singular', $configuration)) {
 194                  $singularverb = $configuration['singular'];
 195                  unset($configuration['singular']);
 196                  unset($elements[$key]['singular']);
 197                  $elements[$singularverb] = $configuration;
 198              }
 199          }
 200  
 201          if (!isset($elements[$generatortype])) {
 202              throw new PendingException($this->name_for_errors($generatortype) .
 203                      ' is not a known type of entity that can be generated.');
 204          }
 205          $entityinfo = $elements[$generatortype];
 206  
 207          $this->datagenerator = testing_util::get_data_generator();
 208          if ($this->component === 'core') {
 209              $this->componentdatagenerator = $this->datagenerator;
 210          } else {
 211              $this->componentdatagenerator = $this->datagenerator->get_plugin_generator($this->component);
 212          }
 213  
 214          $generatortype = $entityinfo['datagenerator'];
 215  
 216          if ($singular) {
 217              // There is only one record to generate, and the table has been pivotted.
 218              // The rows each represent a single field.
 219              $rows = [$data->getRowsHash()];
 220          } else {
 221              // There are multiple records to generate.
 222              // The rows represent an item to create.
 223              $rows = $data->getHash();
 224          }
 225  
 226          foreach ($rows as $elementdata) {
 227              // Check if all the required fields are there.
 228              foreach ($entityinfo['required'] as $requiredfield) {
 229                  if (!isset($elementdata[$requiredfield])) {
 230                      throw new Exception($this->name_for_errors($generatortype) .
 231                              ' requires the field ' . $requiredfield . ' to be specified');
 232                  }
 233              }
 234  
 235              // Switch from human-friendly references to ids.
 236              if (!empty($entityinfo['switchids'])) {
 237                  foreach ($entityinfo['switchids'] as $element => $field) {
 238                      $methodname = 'get_' . $element . '_id';
 239  
 240                      // Not all the switch fields are required, default vars will be assigned by data generators.
 241                      if (isset($elementdata[$element])) {
 242                          if (!method_exists($this, $methodname)) {
 243                              throw new coding_exception('The generator for ' .
 244                                      $this->name_for_errors($generatortype) .
 245                                      ' entities specifies \'switchids\' => [..., \'' . $element .
 246                                      '\' => \'' . $field . '\', ...] but the required method ' .
 247                                      $methodname . '() has not been defined in ' .
 248                                      get_class($this) . '.');
 249                          }
 250                          // Temp $id var to avoid problems when $element == $field.
 251                          $id = $this->{$methodname}($elementdata[$element]);
 252                          unset($elementdata[$element]);
 253                          $elementdata[$field] = $id;
 254                      }
 255                  }
 256              }
 257  
 258              // Preprocess the entities that requires a special treatment.
 259              if (method_exists($this, 'preprocess_' . $generatortype)) {
 260                  $elementdata = $this->{'preprocess_' . $generatortype}($elementdata);
 261              }
 262  
 263              // Creates element.
 264              if (method_exists($this, 'process_' . $generatortype)) {
 265                  // Use a method on this class to do the work.
 266                  $this->{'process_' . $generatortype}($elementdata);
 267  
 268              } else if (method_exists($this->componentdatagenerator, 'create_' . $generatortype)) {
 269                  // Using the component't own data generator if it exists.
 270                  $this->componentdatagenerator->{'create_' . $generatortype}($elementdata);
 271  
 272              } else if (method_exists($this->datagenerator, 'create_' . $generatortype)) {
 273                  // Use a method on the core data geneator, if there is one.
 274                  $this->datagenerator->{'create_' . $generatortype}($elementdata);
 275  
 276              } else {
 277                  // Give up.
 278                  throw new PendingException($this->name_for_errors($generatortype) .
 279                          ' data generator is not implemented');
 280              }
 281          }
 282  
 283          // Notify that the all the elements have been generated.
 284          if (method_exists($this->componentdatagenerator, 'finish_generate_' . $generatortype)) {
 285              // Using the component's own data generator if it exists.
 286              $this->componentdatagenerator->{'finish_generate_' . $generatortype}();
 287  
 288          } else if (method_exists($this->datagenerator, 'finish_generate_' . $generatortype)) {
 289              // Use a method on the core data geneator, if there is one.
 290              $this->datagenerator->{'finish_generate_' . $generatortype}();
 291  
 292          }
 293      }
 294  
 295      /**
 296       * Helper for formatting error messages.
 297       *
 298       * @param string $entitytype entity type without prefix, e.g. 'frog'.
 299       * @return string either 'frog' for core entities, or 'mod_mymod > frog' for components.
 300       */
 301      protected function name_for_errors(string $entitytype): string {
 302          if ($this->component === 'core') {
 303              return '"' . $entitytype . '"';
 304          } else {
 305              return '"' . $this->component . ' > ' . $entitytype . '"';
 306          }
 307      }
 308  
 309      /**
 310       * Gets the grade category id from the grade category fullname
 311       *
 312       * @param string $fullname the grade category name.
 313       * @return int corresponding id.
 314       */
 315      protected function get_gradecategory_id($fullname) {
 316          global $DB;
 317  
 318          if (!$id = $DB->get_field('grade_categories', 'id', array('fullname' => $fullname))) {
 319              throw new Exception('The specified grade category with fullname "' . $fullname . '" does not exist');
 320          }
 321          return $id;
 322      }
 323  
 324      /**
 325       * Gets the user id from it's username.
 326       * @throws Exception
 327       * @param string $username
 328       * @return int
 329       */
 330      protected function get_user_id($username) {
 331          global $DB;
 332  
 333          if (!$id = $DB->get_field('user', 'id', array('username' => $username))) {
 334              throw new Exception('The specified user with username "' . $username . '" does not exist');
 335          }
 336          return $id;
 337      }
 338  
 339      /**
 340       * Gets the user id from it's username.
 341       * @throws Exception
 342       * @param string $username
 343       * @return int
 344       */
 345      protected function get_userfrom_id(string $username) {
 346          global $DB;
 347  
 348          if (!$id = $DB->get_field('user', 'id', ['username' => $username])) {
 349              throw new Exception('The specified user with username "' . $username . '" does not exist');
 350          }
 351          return $id;
 352      }
 353  
 354      /**
 355       * Gets the user id from it's username.
 356       * @throws Exception
 357       * @param string $username
 358       * @return int
 359       */
 360      protected function get_userto_id(string $username) {
 361          global $DB;
 362  
 363          if (!$id = $DB->get_field('user', 'id', ['username' => $username])) {
 364              throw new Exception('The specified user with username "' . $username . '" does not exist');
 365          }
 366          return $id;
 367      }
 368  
 369      /**
 370       * Gets the role id from it's shortname.
 371       * @throws Exception
 372       * @param string $roleshortname
 373       * @return int
 374       */
 375      protected function get_role_id($roleshortname) {
 376          global $DB;
 377  
 378          if (!$id = $DB->get_field('role', 'id', array('shortname' => $roleshortname))) {
 379              throw new Exception('The specified role with shortname "' . $roleshortname . '" does not exist');
 380          }
 381  
 382          return $id;
 383      }
 384  
 385      /**
 386       * Gets the category id from it's idnumber.
 387       * @throws Exception
 388       * @param string $idnumber
 389       * @return int
 390       */
 391      protected function get_category_id($idnumber) {
 392          global $DB;
 393  
 394          // If no category was specified use the data generator one.
 395          if ($idnumber == false) {
 396              return null;
 397          }
 398  
 399          if (!$id = $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber))) {
 400              throw new Exception('The specified category with idnumber "' . $idnumber . '" does not exist');
 401          }
 402  
 403          return $id;
 404      }
 405  
 406      /**
 407       * Gets the course id from it's shortname.
 408       * @throws Exception
 409       * @param string $shortname
 410       * @return int
 411       */
 412      protected function get_course_id($shortname) {
 413          global $DB;
 414  
 415          if (!$id = $DB->get_field('course', 'id', array('shortname' => $shortname))) {
 416              throw new Exception('The specified course with shortname "' . $shortname . '" does not exist');
 417          }
 418          return $id;
 419      }
 420  
 421      /**
 422       * Gets the course cmid for the specified activity based on the activity's idnumber.
 423       *
 424       * Note: this does not check the module type, only the idnumber.
 425       *
 426       * @throws Exception
 427       * @param string $idnumber
 428       * @return int
 429       */
 430      protected function get_activity_id(string $idnumber) {
 431          global $DB;
 432  
 433          if (!$id = $DB->get_field('course_modules', 'id', ['idnumber' => $idnumber])) {
 434              throw new Exception('The specified activity with idnumber "' . $idnumber . '" could not be found.');
 435          }
 436  
 437          return $id;
 438      }
 439  
 440      /**
 441       * Gets the group id from it's idnumber.
 442       * @throws Exception
 443       * @param string $idnumber
 444       * @return int
 445       */
 446      protected function get_group_id($idnumber) {
 447          global $DB;
 448  
 449          if (!$id = $DB->get_field('groups', 'id', array('idnumber' => $idnumber))) {
 450              throw new Exception('The specified group with idnumber "' . $idnumber . '" does not exist');
 451          }
 452          return $id;
 453      }
 454  
 455      /**
 456       * Gets the grouping id from it's idnumber.
 457       * @throws Exception
 458       * @param string $idnumber
 459       * @return int
 460       */
 461      protected function get_grouping_id($idnumber) {
 462          global $DB;
 463  
 464          // Do not fetch grouping ID for empty grouping idnumber.
 465          if (empty($idnumber)) {
 466              return null;
 467          }
 468  
 469          if (!$id = $DB->get_field('groupings', 'id', array('idnumber' => $idnumber))) {
 470              throw new Exception('The specified grouping with idnumber "' . $idnumber . '" does not exist');
 471          }
 472          return $id;
 473      }
 474  
 475      /**
 476       * Gets the cohort id from it's idnumber.
 477       * @throws Exception
 478       * @param string $idnumber
 479       * @return int
 480       */
 481      protected function get_cohort_id($idnumber) {
 482          global $DB;
 483  
 484          if (!$id = $DB->get_field('cohort', 'id', array('idnumber' => $idnumber))) {
 485              throw new Exception('The specified cohort with idnumber "' . $idnumber . '" does not exist');
 486          }
 487          return $id;
 488      }
 489  
 490      /**
 491       * Gets the outcome item id from its shortname.
 492       * @throws Exception
 493       * @param string $shortname
 494       * @return int
 495       */
 496      protected function get_outcome_id($shortname) {
 497          global $DB;
 498  
 499          if (!$id = $DB->get_field('grade_outcomes', 'id', array('shortname' => $shortname))) {
 500              throw new Exception('The specified outcome with shortname "' . $shortname . '" does not exist');
 501          }
 502          return $id;
 503      }
 504  
 505      /**
 506       * Get the id of a named scale.
 507       * @param string $name the name of the scale.
 508       * @return int the scale id.
 509       */
 510      protected function get_scale_id($name) {
 511          global $DB;
 512  
 513          if (!$id = $DB->get_field('scale', 'id', array('name' => $name))) {
 514              throw new Exception('The specified scale with name "' . $name . '" does not exist');
 515          }
 516          return $id;
 517      }
 518  
 519      /**
 520       * Get the id of a named question category (must be globally unique).
 521       * Note that 'Top' is a special value, used when setting the parent of another
 522       * category, meaning top-level.
 523       *
 524       * @param string $name the question category name.
 525       * @return int the question category id.
 526       */
 527      protected function get_questioncategory_id($name) {
 528          global $DB;
 529  
 530          if ($name == 'Top') {
 531              return 0;
 532          }
 533  
 534          if (!$id = $DB->get_field('question_categories', 'id', array('name' => $name))) {
 535              throw new Exception('The specified question category with name "' . $name . '" does not exist');
 536          }
 537          return $id;
 538      }
 539  
 540      /**
 541       * Gets the internal context id from the context reference.
 542       *
 543       * The context reference changes depending on the context
 544       * level, it can be the system, a user, a category, a course or
 545       * a module.
 546       *
 547       * @throws Exception
 548       * @param string $levelname The context level string introduced by the test writer
 549       * @param string $contextref The context reference introduced by the test writer
 550       * @return context
 551       */
 552      protected function get_context($levelname, $contextref) {
 553          return behat_base::get_context($levelname, $contextref);
 554      }
 555  
 556      /**
 557       * Gets the contact id from it's username.
 558       * @throws Exception
 559       * @param string $username
 560       * @return int
 561       */
 562      protected function get_contact_id($username) {
 563          global $DB;
 564  
 565          if (!$id = $DB->get_field('user', 'id', array('username' => $username))) {
 566              throw new Exception('The specified user with username "' . $username . '" does not exist');
 567          }
 568          return $id;
 569      }
 570  
 571      /**
 572       * Gets the external backpack id from it's backpackweburl.
 573       * @param string $backpackweburl
 574       * @return mixed
 575       * @throws dml_exception
 576       */
 577      protected function get_externalbackpack_id($backpackweburl) {
 578          global $DB;
 579          if (!$id = $DB->get_field('badge_external_backpack', 'id', ['backpackweburl' => $backpackweburl])) {
 580              throw new Exception('The specified external backpack with backpackweburl "' . $username . '" does not exist');
 581          }
 582          return $id;
 583      }
 584  
 585      /**
 586       * Get a coursemodule from an activity name or idnumber.
 587       *
 588       * @param string $activity
 589       * @param string $identifier
 590       * @return cm_info
 591       */
 592      protected function get_cm_by_activity_name(string $activity, string $identifier): cm_info {
 593          global $DB;
 594  
 595          $coursetable = new \core\dml\table('course', 'c', 'c');
 596          $courseselect = $coursetable->get_field_select();
 597          $coursefrom = $coursetable->get_from_sql();
 598  
 599          $cmtable = new \core\dml\table('course_modules', 'cm', 'cm');
 600          $cmfrom = $cmtable->get_from_sql();
 601  
 602          $acttable = new \core\dml\table($activity, 'a', 'a');
 603          $actselect = $acttable->get_field_select();
 604          $actfrom = $acttable->get_from_sql();
 605  
 606          $sql = <<<EOF
 607      SELECT cm.id as cmid, {$courseselect}, {$actselect}
 608        FROM {$cmfrom}
 609  INNER JOIN {$coursefrom} ON c.id = cm.course
 610  INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
 611  INNER JOIN {$actfrom} ON cm.instance = a.id
 612       WHERE cm.idnumber = :idnumber OR a.name = :name
 613  EOF;
 614  
 615          $result = $DB->get_record_sql($sql, [
 616              'modname' => $activity,
 617              'idnumber' => $identifier,
 618              'name' => $identifier,
 619          ], MUST_EXIST);
 620  
 621          $course = $coursetable->extract_from_result($result);
 622          $instancedata = $acttable->extract_from_result($result);
 623  
 624          return get_fast_modinfo($course)->get_cm($result->cmid);
 625      }
 626  }