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