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   * 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       * Do the work to generate an entity.
 170       *
 171       * This is called by {@link behat_data_generators::the_following_entities_exist()}.
 172       *
 173       * @param string    $generatortype The name of the entity to create.
 174       * @param TableNode $data from the step.
 175       * @param bool      $singular Whether there is only one record and it is pivotted
 176       */
 177      public function generate_items(string $generatortype, TableNode $data, bool $singular = false) {
 178          // Now that we need them require the data generators.
 179          require_once (__DIR__ . '/../../testing/generator/lib.php');
 180  
 181          $elements = $this->get_creatable_entities();
 182  
 183          foreach ($elements as $key => $configuration) {
 184              if (array_key_exists('singular', $configuration)) {
 185                  $singularverb = $configuration['singular'];
 186                  unset($configuration['singular']);
 187                  unset($elements[$key]['singular']);
 188                  $elements[$singularverb] = $configuration;
 189              }
 190          }
 191  
 192          if (!isset($elements[$generatortype])) {
 193              throw new PendingException($this->name_for_errors($generatortype) .
 194                      ' is not a known type of entity that can be generated.');
 195          }
 196          $entityinfo = $elements[$generatortype];
 197  
 198          $this->datagenerator = testing_util::get_data_generator();
 199          if ($this->component === 'core') {
 200              $this->componentdatagenerator = $this->datagenerator;
 201          } else {
 202              $this->componentdatagenerator = $this->datagenerator->get_plugin_generator($this->component);
 203          }
 204  
 205          $generatortype = $entityinfo['datagenerator'];
 206  
 207          if ($singular) {
 208              // There is only one record to generate, and the table has been pivotted.
 209              // The rows each represent a single field.
 210              $rows = [$data->getRowsHash()];
 211          } else {
 212              // There are multiple records to generate.
 213              // The rows represent an item to create.
 214              $rows = $data->getHash();
 215          }
 216  
 217          foreach ($rows as $elementdata) {
 218              // Check if all the required fields are there.
 219              foreach ($entityinfo['required'] as $requiredfield) {
 220                  if (!isset($elementdata[$requiredfield])) {
 221                      throw new Exception($this->name_for_errors($generatortype) .
 222                              ' requires the field ' . $requiredfield . ' to be specified');
 223                  }
 224              }
 225  
 226              // Switch from human-friendly references to ids.
 227              if (!empty($entityinfo['switchids'])) {
 228                  foreach ($entityinfo['switchids'] as $element => $field) {
 229                      $methodname = 'get_' . $element . '_id';
 230  
 231                      // Not all the switch fields are required, default vars will be assigned by data generators.
 232                      if (isset($elementdata[$element])) {
 233                          if (!method_exists($this, $methodname)) {
 234                              throw new coding_exception('The generator for ' .
 235                                      $this->name_for_errors($generatortype) .
 236                                      ' entities specifies \'switchids\' => [..., \'' . $element .
 237                                      '\' => \'' . $field . '\', ...] but the required method ' .
 238                                      $methodname . '() has not been defined in ' .
 239                                      get_class($this) . '.');
 240                          }
 241                          // Temp $id var to avoid problems when $element == $field.
 242                          $id = $this->{$methodname}($elementdata[$element]);
 243                          unset($elementdata[$element]);
 244                          $elementdata[$field] = $id;
 245                      }
 246                  }
 247              }
 248  
 249              // Preprocess the entities that requires a special treatment.
 250              if (method_exists($this, 'preprocess_' . $generatortype)) {
 251                  $elementdata = $this->{'preprocess_' . $generatortype}($elementdata);
 252              }
 253  
 254              // Creates element.
 255              if (method_exists($this, 'process_' . $generatortype)) {
 256                  // Use a method on this class to do the work.
 257                  $this->{'process_' . $generatortype}($elementdata);
 258  
 259              } else if (method_exists($this->componentdatagenerator, 'create_' . $generatortype)) {
 260                  // Using the component't own data generator if it exists.
 261                  $this->componentdatagenerator->{'create_' . $generatortype}($elementdata);
 262  
 263              } else if (method_exists($this->datagenerator, 'create_' . $generatortype)) {
 264                  // Use a method on the core data geneator, if there is one.
 265                  $this->datagenerator->{'create_' . $generatortype}($elementdata);
 266  
 267              } else {
 268                  // Give up.
 269                  throw new PendingException($this->name_for_errors($generatortype) .
 270                          ' data generator is not implemented');
 271              }
 272          }
 273      }
 274  
 275      /**
 276       * Helper for formatting error messages.
 277       *
 278       * @param string $entitytype entity type without prefix, e.g. 'frog'.
 279       * @return string either 'frog' for core entities, or 'mod_mymod > frog' for components.
 280       */
 281      protected function name_for_errors(string $entitytype): string {
 282          if ($this->component === 'core') {
 283              return '"' . $entitytype . '"';
 284          } else {
 285              return '"' . $this->component . ' > ' . $entitytype . '"';
 286          }
 287      }
 288  
 289      /**
 290       * Gets the grade category id from the grade category fullname
 291       *
 292       * @param string $fullname the grade category name.
 293       * @return int corresponding id.
 294       */
 295      protected function get_gradecategory_id($fullname) {
 296          global $DB;
 297  
 298          if (!$id = $DB->get_field('grade_categories', 'id', array('fullname' => $fullname))) {
 299              throw new Exception('The specified grade category with fullname "' . $fullname . '" does not exist');
 300          }
 301          return $id;
 302      }
 303  
 304      /**
 305       * Gets the user id from it's username.
 306       * @throws Exception
 307       * @param string $username
 308       * @return int
 309       */
 310      protected function get_user_id($username) {
 311          global $DB;
 312  
 313          if (!$id = $DB->get_field('user', 'id', array('username' => $username))) {
 314              throw new Exception('The specified user with username "' . $username . '" does not exist');
 315          }
 316          return $id;
 317      }
 318  
 319      /**
 320       * Gets the role id from it's shortname.
 321       * @throws Exception
 322       * @param string $roleshortname
 323       * @return int
 324       */
 325      protected function get_role_id($roleshortname) {
 326          global $DB;
 327  
 328          if (!$id = $DB->get_field('role', 'id', array('shortname' => $roleshortname))) {
 329              throw new Exception('The specified role with shortname "' . $roleshortname . '" does not exist');
 330          }
 331  
 332          return $id;
 333      }
 334  
 335      /**
 336       * Gets the category id from it's idnumber.
 337       * @throws Exception
 338       * @param string $idnumber
 339       * @return int
 340       */
 341      protected function get_category_id($idnumber) {
 342          global $DB;
 343  
 344          // If no category was specified use the data generator one.
 345          if ($idnumber == false) {
 346              return null;
 347          }
 348  
 349          if (!$id = $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber))) {
 350              throw new Exception('The specified category with idnumber "' . $idnumber . '" does not exist');
 351          }
 352  
 353          return $id;
 354      }
 355  
 356      /**
 357       * Gets the course id from it's shortname.
 358       * @throws Exception
 359       * @param string $shortname
 360       * @return int
 361       */
 362      protected function get_course_id($shortname) {
 363          global $DB;
 364  
 365          if (!$id = $DB->get_field('course', 'id', array('shortname' => $shortname))) {
 366              throw new Exception('The specified course with shortname "' . $shortname . '" does not exist');
 367          }
 368          return $id;
 369      }
 370  
 371      /**
 372       * Gets the course cmid for the specified activity based on the activity's idnumber.
 373       *
 374       * Note: this does not check the module type, only the idnumber.
 375       *
 376       * @throws Exception
 377       * @param string $idnumber
 378       * @return int
 379       */
 380      protected function get_activity_id(string $idnumber) {
 381          global $DB;
 382  
 383          if (!$id = $DB->get_field('course_modules', 'id', ['idnumber' => $idnumber])) {
 384              throw new Exception('The specified activity with idnumber "' . $idnumber . '" could not be found.');
 385          }
 386  
 387          return $id;
 388      }
 389  
 390      /**
 391       * Gets the group id from it's idnumber.
 392       * @throws Exception
 393       * @param string $idnumber
 394       * @return int
 395       */
 396      protected function get_group_id($idnumber) {
 397          global $DB;
 398  
 399          if (!$id = $DB->get_field('groups', 'id', array('idnumber' => $idnumber))) {
 400              throw new Exception('The specified group with idnumber "' . $idnumber . '" does not exist');
 401          }
 402          return $id;
 403      }
 404  
 405      /**
 406       * Gets the grouping id from it's idnumber.
 407       * @throws Exception
 408       * @param string $idnumber
 409       * @return int
 410       */
 411      protected function get_grouping_id($idnumber) {
 412          global $DB;
 413  
 414          // Do not fetch grouping ID for empty grouping idnumber.
 415          if (empty($idnumber)) {
 416              return null;
 417          }
 418  
 419          if (!$id = $DB->get_field('groupings', 'id', array('idnumber' => $idnumber))) {
 420              throw new Exception('The specified grouping with idnumber "' . $idnumber . '" does not exist');
 421          }
 422          return $id;
 423      }
 424  
 425      /**
 426       * Gets the cohort id from it's idnumber.
 427       * @throws Exception
 428       * @param string $idnumber
 429       * @return int
 430       */
 431      protected function get_cohort_id($idnumber) {
 432          global $DB;
 433  
 434          if (!$id = $DB->get_field('cohort', 'id', array('idnumber' => $idnumber))) {
 435              throw new Exception('The specified cohort with idnumber "' . $idnumber . '" does not exist');
 436          }
 437          return $id;
 438      }
 439  
 440      /**
 441       * Gets the outcome item id from its shortname.
 442       * @throws Exception
 443       * @param string $shortname
 444       * @return int
 445       */
 446      protected function get_outcome_id($shortname) {
 447          global $DB;
 448  
 449          if (!$id = $DB->get_field('grade_outcomes', 'id', array('shortname' => $shortname))) {
 450              throw new Exception('The specified outcome with shortname "' . $shortname . '" does not exist');
 451          }
 452          return $id;
 453      }
 454  
 455      /**
 456       * Get the id of a named scale.
 457       * @param string $name the name of the scale.
 458       * @return int the scale id.
 459       */
 460      protected function get_scale_id($name) {
 461          global $DB;
 462  
 463          if (!$id = $DB->get_field('scale', 'id', array('name' => $name))) {
 464              throw new Exception('The specified scale with name "' . $name . '" does not exist');
 465          }
 466          return $id;
 467      }
 468  
 469      /**
 470       * Get the id of a named question category (must be globally unique).
 471       * Note that 'Top' is a special value, used when setting the parent of another
 472       * category, meaning top-level.
 473       *
 474       * @param string $name the question category name.
 475       * @return int the question category id.
 476       */
 477      protected function get_questioncategory_id($name) {
 478          global $DB;
 479  
 480          if ($name == 'Top') {
 481              return 0;
 482          }
 483  
 484          if (!$id = $DB->get_field('question_categories', 'id', array('name' => $name))) {
 485              throw new Exception('The specified question category with name "' . $name . '" does not exist');
 486          }
 487          return $id;
 488      }
 489  
 490      /**
 491       * Gets the internal context id from the context reference.
 492       *
 493       * The context reference changes depending on the context
 494       * level, it can be the system, a user, a category, a course or
 495       * a module.
 496       *
 497       * @throws Exception
 498       * @param string $levelname The context level string introduced by the test writer
 499       * @param string $contextref The context reference introduced by the test writer
 500       * @return context
 501       */
 502      protected function get_context($levelname, $contextref) {
 503          return behat_base::get_context($levelname, $contextref);
 504      }
 505  
 506      /**
 507       * Gets the contact id from it's username.
 508       * @throws Exception
 509       * @param string $username
 510       * @return int
 511       */
 512      protected function get_contact_id($username) {
 513          global $DB;
 514  
 515          if (!$id = $DB->get_field('user', 'id', array('username' => $username))) {
 516              throw new Exception('The specified user with username "' . $username . '" does not exist');
 517          }
 518          return $id;
 519      }
 520  
 521      /**
 522       * Gets the external backpack id from it's backpackweburl.
 523       * @param string $backpackweburl
 524       * @return mixed
 525       * @throws dml_exception
 526       */
 527      protected function get_externalbackpack_id($backpackweburl) {
 528          global $DB;
 529          if (!$id = $DB->get_field('badge_external_backpack', 'id', ['backpackweburl' => $backpackweburl])) {
 530              throw new Exception('The specified external backpack with backpackweburl "' . $username . '" does not exist');
 531          }
 532          return $id;
 533      }
 534  
 535      /**
 536       * Get a coursemodule from an activity name or idnumber.
 537       *
 538       * @param string $activity
 539       * @param string $identifier
 540       * @return cm_info
 541       */
 542      protected function get_cm_by_activity_name(string $activity, string $identifier): cm_info {
 543          global $DB;
 544  
 545          $coursetable = new \core\dml\table('course', 'c', 'c');
 546          $courseselect = $coursetable->get_field_select();
 547          $coursefrom = $coursetable->get_from_sql();
 548  
 549          $cmtable = new \core\dml\table('course_modules', 'cm', 'cm');
 550          $cmfrom = $cmtable->get_from_sql();
 551  
 552          $acttable = new \core\dml\table($activity, 'a', 'a');
 553          $actselect = $acttable->get_field_select();
 554          $actfrom = $acttable->get_from_sql();
 555  
 556          $sql = <<<EOF
 557      SELECT cm.id as cmid, {$courseselect}, {$actselect}
 558        FROM {$cmfrom}
 559  INNER JOIN {$coursefrom} ON c.id = cm.course
 560  INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
 561  INNER JOIN {$actfrom} ON cm.instance = a.id
 562       WHERE cm.idnumber = :idnumber OR a.name = :name
 563  EOF;
 564  
 565          $result = $DB->get_record_sql($sql, [
 566              'modname' => $activity,
 567              'idnumber' => $identifier,
 568              'name' => $identifier,
 569          ], MUST_EXIST);
 570  
 571          $course = $coursetable->extract_from_result($result);
 572          $instancedata = $acttable->extract_from_result($result);
 573  
 574          return get_fast_modinfo($course)->get_cm($result->cmid);
 575      }
 576  }