Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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  
 284      /**
 285       * Helper for formatting error messages.
 286       *
 287       * @param string $entitytype entity type without prefix, e.g. 'frog'.
 288       * @return string either 'frog' for core entities, or 'mod_mymod > frog' for components.
 289       */
 290      protected function name_for_errors(string $entitytype): string {
 291          if ($this->component === 'core') {
 292              return '"' . $entitytype . '"';
 293          } else {
 294              return '"' . $this->component . ' > ' . $entitytype . '"';
 295          }
 296      }
 297  
 298      /**
 299       * Gets the grade category id from the grade category fullname
 300       *
 301       * @param string $fullname the grade category name.
 302       * @return int corresponding id.
 303       */
 304      protected function get_gradecategory_id($fullname) {
 305          global $DB;
 306  
 307          if (!$id = $DB->get_field('grade_categories', 'id', array('fullname' => $fullname))) {
 308              throw new Exception('The specified grade category with fullname "' . $fullname . '" does not exist');
 309          }
 310          return $id;
 311      }
 312  
 313      /**
 314       * Gets the user id from it's username.
 315       * @throws Exception
 316       * @param string $username
 317       * @return int
 318       */
 319      protected function get_user_id($username) {
 320          global $DB;
 321  
 322          if (!$id = $DB->get_field('user', 'id', array('username' => $username))) {
 323              throw new Exception('The specified user with username "' . $username . '" does not exist');
 324          }
 325          return $id;
 326      }
 327  
 328      /**
 329       * Gets the user id from it's username.
 330       * @throws Exception
 331       * @param string $username
 332       * @return int
 333       */
 334      protected function get_userfrom_id(string $username) {
 335          global $DB;
 336  
 337          if (!$id = $DB->get_field('user', 'id', ['username' => $username])) {
 338              throw new Exception('The specified user with username "' . $username . '" does not exist');
 339          }
 340          return $id;
 341      }
 342  
 343      /**
 344       * Gets the user id from it's username.
 345       * @throws Exception
 346       * @param string $username
 347       * @return int
 348       */
 349      protected function get_userto_id(string $username) {
 350          global $DB;
 351  
 352          if (!$id = $DB->get_field('user', 'id', ['username' => $username])) {
 353              throw new Exception('The specified user with username "' . $username . '" does not exist');
 354          }
 355          return $id;
 356      }
 357  
 358      /**
 359       * Gets the role id from it's shortname.
 360       * @throws Exception
 361       * @param string $roleshortname
 362       * @return int
 363       */
 364      protected function get_role_id($roleshortname) {
 365          global $DB;
 366  
 367          if (!$id = $DB->get_field('role', 'id', array('shortname' => $roleshortname))) {
 368              throw new Exception('The specified role with shortname "' . $roleshortname . '" does not exist');
 369          }
 370  
 371          return $id;
 372      }
 373  
 374      /**
 375       * Gets the category id from it's idnumber.
 376       * @throws Exception
 377       * @param string $idnumber
 378       * @return int
 379       */
 380      protected function get_category_id($idnumber) {
 381          global $DB;
 382  
 383          // If no category was specified use the data generator one.
 384          if ($idnumber == false) {
 385              return null;
 386          }
 387  
 388          if (!$id = $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber))) {
 389              throw new Exception('The specified category with idnumber "' . $idnumber . '" does not exist');
 390          }
 391  
 392          return $id;
 393      }
 394  
 395      /**
 396       * Gets the course id from it's shortname.
 397       * @throws Exception
 398       * @param string $shortname
 399       * @return int
 400       */
 401      protected function get_course_id($shortname) {
 402          global $DB;
 403  
 404          if (!$id = $DB->get_field('course', 'id', array('shortname' => $shortname))) {
 405              throw new Exception('The specified course with shortname "' . $shortname . '" does not exist');
 406          }
 407          return $id;
 408      }
 409  
 410      /**
 411       * Gets the course cmid for the specified activity based on the activity's idnumber.
 412       *
 413       * Note: this does not check the module type, only the idnumber.
 414       *
 415       * @throws Exception
 416       * @param string $idnumber
 417       * @return int
 418       */
 419      protected function get_activity_id(string $idnumber) {
 420          global $DB;
 421  
 422          if (!$id = $DB->get_field('course_modules', 'id', ['idnumber' => $idnumber])) {
 423              throw new Exception('The specified activity with idnumber "' . $idnumber . '" could not be found.');
 424          }
 425  
 426          return $id;
 427      }
 428  
 429      /**
 430       * Gets the group id from it's idnumber.
 431       * @throws Exception
 432       * @param string $idnumber
 433       * @return int
 434       */
 435      protected function get_group_id($idnumber) {
 436          global $DB;
 437  
 438          if (!$id = $DB->get_field('groups', 'id', array('idnumber' => $idnumber))) {
 439              throw new Exception('The specified group with idnumber "' . $idnumber . '" does not exist');
 440          }
 441          return $id;
 442      }
 443  
 444      /**
 445       * Gets the grouping id from it's idnumber.
 446       * @throws Exception
 447       * @param string $idnumber
 448       * @return int
 449       */
 450      protected function get_grouping_id($idnumber) {
 451          global $DB;
 452  
 453          // Do not fetch grouping ID for empty grouping idnumber.
 454          if (empty($idnumber)) {
 455              return null;
 456          }
 457  
 458          if (!$id = $DB->get_field('groupings', 'id', array('idnumber' => $idnumber))) {
 459              throw new Exception('The specified grouping with idnumber "' . $idnumber . '" does not exist');
 460          }
 461          return $id;
 462      }
 463  
 464      /**
 465       * Gets the cohort id from it's idnumber.
 466       * @throws Exception
 467       * @param string $idnumber
 468       * @return int
 469       */
 470      protected function get_cohort_id($idnumber) {
 471          global $DB;
 472  
 473          if (!$id = $DB->get_field('cohort', 'id', array('idnumber' => $idnumber))) {
 474              throw new Exception('The specified cohort with idnumber "' . $idnumber . '" does not exist');
 475          }
 476          return $id;
 477      }
 478  
 479      /**
 480       * Gets the outcome item id from its shortname.
 481       * @throws Exception
 482       * @param string $shortname
 483       * @return int
 484       */
 485      protected function get_outcome_id($shortname) {
 486          global $DB;
 487  
 488          if (!$id = $DB->get_field('grade_outcomes', 'id', array('shortname' => $shortname))) {
 489              throw new Exception('The specified outcome with shortname "' . $shortname . '" does not exist');
 490          }
 491          return $id;
 492      }
 493  
 494      /**
 495       * Get the id of a named scale.
 496       * @param string $name the name of the scale.
 497       * @return int the scale id.
 498       */
 499      protected function get_scale_id($name) {
 500          global $DB;
 501  
 502          if (!$id = $DB->get_field('scale', 'id', array('name' => $name))) {
 503              throw new Exception('The specified scale with name "' . $name . '" does not exist');
 504          }
 505          return $id;
 506      }
 507  
 508      /**
 509       * Get the id of a named question category (must be globally unique).
 510       * Note that 'Top' is a special value, used when setting the parent of another
 511       * category, meaning top-level.
 512       *
 513       * @param string $name the question category name.
 514       * @return int the question category id.
 515       */
 516      protected function get_questioncategory_id($name) {
 517          global $DB;
 518  
 519          if ($name == 'Top') {
 520              return 0;
 521          }
 522  
 523          if (!$id = $DB->get_field('question_categories', 'id', array('name' => $name))) {
 524              throw new Exception('The specified question category with name "' . $name . '" does not exist');
 525          }
 526          return $id;
 527      }
 528  
 529      /**
 530       * Gets the internal context id from the context reference.
 531       *
 532       * The context reference changes depending on the context
 533       * level, it can be the system, a user, a category, a course or
 534       * a module.
 535       *
 536       * @throws Exception
 537       * @param string $levelname The context level string introduced by the test writer
 538       * @param string $contextref The context reference introduced by the test writer
 539       * @return context
 540       */
 541      protected function get_context($levelname, $contextref) {
 542          return behat_base::get_context($levelname, $contextref);
 543      }
 544  
 545      /**
 546       * Gets the contact id from it's username.
 547       * @throws Exception
 548       * @param string $username
 549       * @return int
 550       */
 551      protected function get_contact_id($username) {
 552          global $DB;
 553  
 554          if (!$id = $DB->get_field('user', 'id', array('username' => $username))) {
 555              throw new Exception('The specified user with username "' . $username . '" does not exist');
 556          }
 557          return $id;
 558      }
 559  
 560      /**
 561       * Gets the external backpack id from it's backpackweburl.
 562       * @param string $backpackweburl
 563       * @return mixed
 564       * @throws dml_exception
 565       */
 566      protected function get_externalbackpack_id($backpackweburl) {
 567          global $DB;
 568          if (!$id = $DB->get_field('badge_external_backpack', 'id', ['backpackweburl' => $backpackweburl])) {
 569              throw new Exception('The specified external backpack with backpackweburl "' . $username . '" does not exist');
 570          }
 571          return $id;
 572      }
 573  
 574      /**
 575       * Get a coursemodule from an activity name or idnumber.
 576       *
 577       * @param string $activity
 578       * @param string $identifier
 579       * @return cm_info
 580       */
 581      protected function get_cm_by_activity_name(string $activity, string $identifier): cm_info {
 582          global $DB;
 583  
 584          $coursetable = new \core\dml\table('course', 'c', 'c');
 585          $courseselect = $coursetable->get_field_select();
 586          $coursefrom = $coursetable->get_from_sql();
 587  
 588          $cmtable = new \core\dml\table('course_modules', 'cm', 'cm');
 589          $cmfrom = $cmtable->get_from_sql();
 590  
 591          $acttable = new \core\dml\table($activity, 'a', 'a');
 592          $actselect = $acttable->get_field_select();
 593          $actfrom = $acttable->get_from_sql();
 594  
 595          $sql = <<<EOF
 596      SELECT cm.id as cmid, {$courseselect}, {$actselect}
 597        FROM {$cmfrom}
 598  INNER JOIN {$coursefrom} ON c.id = cm.course
 599  INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname
 600  INNER JOIN {$actfrom} ON cm.instance = a.id
 601       WHERE cm.idnumber = :idnumber OR a.name = :name
 602  EOF;
 603  
 604          $result = $DB->get_record_sql($sql, [
 605              'modname' => $activity,
 606              'idnumber' => $identifier,
 607              'name' => $identifier,
 608          ], MUST_EXIST);
 609  
 610          $course = $coursetable->extract_from_result($result);
 611          $instancedata = $acttable->extract_from_result($result);
 612  
 613          return get_fast_modinfo($course)->get_cm($result->cmid);
 614      }
 615  }