See Release Notes
Long Term Support Release
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body