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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body