MDL-69068 behat: New step to support singular generators
[moodle.git] / lib / behat / classes / behat_generator_base.php
CommitLineData
285c7036
TH
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
26defined('MOODLE_INTERNAL') || die();
27
28use Behat\Gherkin\Node\TableNode as TableNode;
29use Behat\Behat\Tester\Exception\PendingException as PendingException;
30
31
32/**
33 * Class to quickly create Behat test data using component data generators.
34 *
35 * There is a subclass of class for each component that wants to be able to
36 * generate entities using the Behat step
37 * Given the following "entity types" exist:
38 * | test | data |
39 *
40 * For core entities, the entity type is like "courses" or "users" and
41 * generating those is handled by behat_core_generator. For other components
42 * the entity type is like "mod_quiz > User override" and that is handled by
43 * behat_mod_quiz_generator defined in mod/quiz/tests/generator/behat_mod_quiz_generator.php.
44 *
45 * The types of entities that can be generated are described by the array returned
46 * by the {@link get_generateable_entities()} method. The list in
47 * {@link behat_core_generator} is a good (if complex) example.
48 *
49 * How things work is best explained with a few examples. All this is implemented
50 * in the {@link generate_items()} method below, if you want to see every detail of
51 * how it works.
52 *
53 * Simple example from behat_core_generator:
54 * 'users' => [
55 * 'datagenerator' => 'user',
56 * 'required' => ['username'],
57 * ],
58 * The steps performed are:
59 *
60 * 1. 'datagenerator' => 'user' means that the word used in the method names below is 'user'.
61 *
62 * 2. Because 'required' is present, check the supplied data exists 'username' column is present
63 * in the supplied data table and if not display an error.
64 *
65 * 3. Then for each row in the table as an array $elementdata (array keys are column names)
66 * and process it as follows
67 *
68 * 4. (Not used in this example.)
69 *
70 * 5. If the method 'preprocess_user' exists, then call it to update $elementdata.
71 * (It does, in this case it sets the password to the username, if password was not given.)
72 *
73 * We then do one of 4 things:
74 *
75 * 6a. If there is a method 'process_user' we call it. (It doesn't for user,
76 * but there are other examples like process_enrol_user() in behat_core_generator.)
77 *
78 * 6b. (Not used in this example.)
79 *
80 * 6c. Else, if testing_data_generator::create_user exists, we call it with $elementdata. (it does.)
81 *
82 * 6d. If none of these three things work. an error is thrown.
83 *
84 * To understand the missing steps above, consider the example from behat_mod_quiz_generator:
85 * 'group override' => [
86 * 'datagenerator' => 'override',
87 * 'required' => ['quiz', 'group'],
88 * 'switchids' => ['quiz' => 'quiz', 'group' => 'groupid'],
89 * ],
90 * Processing is as above, except that:
91 *
92 * 1. Note 'datagenerator' is 'override' (not group_override). 'user override' maps to the
93 * same datagenerator. This works fine.
94 *
95 * 4. Because 'switchids' is present, human-readable data in the table gets converted to ids.
96 * They array key 'group' refers to a column which may be present in the table (it will be
97 * here because it is required, but it does not have to be in general). If that column
98 * is present and contains a value, then the method matching name like get_group_id() is
99 * called with the value from that column in the data table. You must implement this
100 * method. You can see several examples of this sort of method below.
101 *
102 * If that method returns a group id, then $elementdata['group'] is unset and
103 * $elementdata['groupid'] is set to the result of the get_group_id() call. 'groupid' here
104 * because of the definition is 'switchids' => [..., 'group' => 'groupid'].
105 * If get_group_id() cannot find the group, it should throw a helpful exception.
106 *
107 * Similarly, 'quiz' (the quiz name) is looked up with a call to get_quiz_id(). Here, the
108 * new array key set matches the old one removed. This is fine.
109 *
110 * 6b. We are in a plugin, so before checking whether testing_data_generator::create_override
111 * exists we first check whether mod_quiz_generator::create_override() exists. It does,
112 * and this is what gets called.
113 *
114 * This second example shows why the get_..._id methods for core entities are in this base
115 * class, not in behat_core_generator. Plugins may need to look up the ids of
116 * core entities.
117 *
118 * behat_core_generator is defined in lib/behat/classes/behat_core_generator.php
119 * and for components, behat_..._generator is defined in tests/generator/behat_..._generator.php
120 * inside the plugin. For example behat_mod_quiz_generator is defined in
121 * mod/quiz/tests/generator/behat_mod_quiz_generator.php.
122 *
123 * @package core
124 * @category test
125 * @copyright 2012 David MonllaĆ³
126 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
127 */
128abstract class behat_generator_base {
129
130 /**
131 * @var string the name of the component we belong to.
132 *
133 * This should probably only be used to make error messages clearer.
134 */
135 protected $component;
136
137 /**
138 * @var testing_data_generator the core data generator
139 */
140 protected $datagenerator;
141
142 /**
143 * @var testing_data_generator the data generator for this component.
144 */
145 protected $componentdatagenerator;
146
147 /**
148 * Constructor.
149 *
150 * @param string $component component name, to make error messages more readable.
151 */
152 public function __construct(string $component) {
153 $this->component = $component;
154 }
155
156 /**
157 * Get a list of the entities that can be created for this component.
158 *
159 * This function must be overridden in subclasses. See class comment
160 * above for a description of the data structure.
161 * See {@link behat_core_generator} for an example.
162 *
163 * @return array entity name => information about how to generate.
164 */
165 protected abstract function get_creatable_entities(): array;
166
167 /**
168 * Do the work to generate an entity.
169 *
170 * This is called by {@link behat_data_generators::the_following_entities_exist()}.
171 *
172 * @param string $generatortype The name of the entity to create.
173 * @param TableNode $data from the step.
b49cb92e 174 * @param bool $singular Whether there is only one record and it is pivotted
285c7036 175 */
b49cb92e 176 public function generate_items(string $generatortype, TableNode $data, bool $singular = false) {
285c7036
TH
177 // Now that we need them require the data generators.
178 require_once(__DIR__ . '/../../testing/generator/lib.php');
179
180 $elements = $this->get_creatable_entities();
181
b49cb92e
AN
182 foreach ($elements as $key => $configuration) {
183 if (array_key_exists('singular', $configuration)) {
184 $singularverb = $configuration['singular'];
185 unset($configuration['singular']);
186 unset($elements[$key]['singular']);
187 $elements[$singularverb] = $configuration;
188 }
189 }
190
285c7036
TH
191 if (!isset($elements[$generatortype])) {
192 throw new PendingException($this->name_for_errors($generatortype) .
193 ' is not a known type of entity that can be generated.');
194 }
195 $entityinfo = $elements[$generatortype];
196
197 $this->datagenerator = testing_util::get_data_generator();
198 if ($this->component === 'core') {
199 $this->componentdatagenerator = $this->datagenerator;
200 } else {
201 $this->componentdatagenerator = $this->datagenerator->get_plugin_generator($this->component);
202 }
203
204 $generatortype = $entityinfo['datagenerator'];
205
b49cb92e
AN
206 if ($singular) {
207 // There is only one record to generate, and the table has been pivotted.
208 // The rows each represent a single field.
209 $rows = [$data->getRowsHash()];
210 } else {
211 // There are multiple records to generate.
212 // The rows represent an item to create.
213 $rows = $data->getHash();
214 }
285c7036 215
b49cb92e 216 foreach ($rows as $elementdata) {
285c7036
TH
217 // Check if all the required fields are there.
218 foreach ($entityinfo['required'] as $requiredfield) {
219 if (!isset($elementdata[$requiredfield])) {
220 throw new Exception($this->name_for_errors($generatortype) .
221 ' requires the field ' . $requiredfield . ' to be specified');
222 }
223 }
224
225 // Switch from human-friendly references to ids.
226 if (!empty($entityinfo['switchids'])) {
227 foreach ($entityinfo['switchids'] as $element => $field) {
228 $methodname = 'get_' . $element . '_id';
229
230 // Not all the switch fields are required, default vars will be assigned by data generators.
231 if (isset($elementdata[$element])) {
232 if (!method_exists($this, $methodname)) {
233 throw new coding_exception('The generator for ' .
234 $this->name_for_errors($generatortype) .
235 ' entities specifies \'switchids\' => [..., \'' . $element .
236 '\' => \'' . $field . '\', ...] but the required method ' .
237 $methodname . '() has not been defined in ' .
238 get_class($this) . '.');
239 }
240 // Temp $id var to avoid problems when $element == $field.
241 $id = $this->{$methodname}($elementdata[$element]);
242 unset($elementdata[$element]);
243 $elementdata[$field] = $id;
244 }
245 }
246 }
247
248 // Preprocess the entities that requires a special treatment.
249 if (method_exists($this, 'preprocess_' . $generatortype)) {
250 $elementdata = $this->{'preprocess_' . $generatortype}($elementdata);
251 }
252
253 // Creates element.
254 if (method_exists($this, 'process_' . $generatortype)) {
255 // Use a method on this class to do the work.
256 $this->{'process_' . $generatortype}($elementdata);
257
258 } else if (method_exists($this->componentdatagenerator, 'create_' . $generatortype)) {
259 // Using the component't own data generator if it exists.
260 $this->componentdatagenerator->{'create_' . $generatortype}($elementdata);
261
262 } else if (method_exists($this->datagenerator, 'create_' . $generatortype)) {
263 // Use a method on the core data geneator, if there is one.
264 $this->datagenerator->{'create_' . $generatortype}($elementdata);
265
266 } else {
267 // Give up.
268 throw new PendingException($this->name_for_errors($generatortype) .
269 ' data generator is not implemented');
270 }
271 }
272 }
273
274 /**
275 * Helper for formatting error messages.
276 *
277 * @param string $entitytype entity type without prefix, e.g. 'frog'.
278 * @return string either 'frog' for core entities, or 'mod_mymod > frog' for components.
279 */
280 protected function name_for_errors(string $entitytype): string {
281 if ($this->component === 'core') {
282 return '"' . $entitytype . '"';
283 } else {
284 return '"' . $this->component . ' > ' . $entitytype . '"';
285 }
286 }
287
288 /**
289 * Gets the grade category id from the grade category fullname
290 *
291 * @param string $fullname the grade category name.
292 * @return int corresponding id.
293 */
294 protected function get_gradecategory_id($fullname) {
295 global $DB;
296
297 if (!$id = $DB->get_field('grade_categories', 'id', array('fullname' => $fullname))) {
298 throw new Exception('The specified grade category with fullname "' . $fullname . '" does not exist');
299 }
300 return $id;
301 }
302
303 /**
304 * Gets the user id from it's username.
305 * @throws Exception
306 * @param string $username
307 * @return int
308 */
309 protected function get_user_id($username) {
310 global $DB;
311
312 if (!$id = $DB->get_field('user', 'id', array('username' => $username))) {
313 throw new Exception('The specified user with username "' . $username . '" does not exist');
314 }
315 return $id;
316 }
317
318 /**
319 * Gets the role id from it's shortname.
320 * @throws Exception
321 * @param string $roleshortname
322 * @return int
323 */
324 protected function get_role_id($roleshortname) {
325 global $DB;
326
327 if (!$id = $DB->get_field('role', 'id', array('shortname' => $roleshortname))) {
328 throw new Exception('The specified role with shortname "' . $roleshortname . '" does not exist');
329 }
330
331 return $id;
332 }
333
334 /**
335 * Gets the category id from it's idnumber.
336 * @throws Exception
337 * @param string $idnumber
338 * @return int
339 */
340 protected function get_category_id($idnumber) {
341 global $DB;
342
343 // If no category was specified use the data generator one.
344 if ($idnumber == false) {
345 return null;
346 }
347
348 if (!$id = $DB->get_field('course_categories', 'id', array('idnumber' => $idnumber))) {
349 throw new Exception('The specified category with idnumber "' . $idnumber . '" does not exist');
350 }
351
352 return $id;
353 }
354
355 /**
356 * Gets the course id from it's shortname.
357 * @throws Exception
358 * @param string $shortname
359 * @return int
360 */
361 protected function get_course_id($shortname) {
362 global $DB;
363
364 if (!$id = $DB->get_field('course', 'id', array('shortname' => $shortname))) {
365 throw new Exception('The specified course with shortname "' . $shortname . '" does not exist');
366 }
367 return $id;
368 }
369
370 /**
371 * Gets the group id from it's idnumber.
372 * @throws Exception
373 * @param string $idnumber
374 * @return int
375 */
376 protected function get_group_id($idnumber) {
377 global $DB;
378
379 if (!$id = $DB->get_field('groups', 'id', array('idnumber' => $idnumber))) {
380 throw new Exception('The specified group with idnumber "' . $idnumber . '" does not exist');
381 }
382 return $id;
383 }
384
385 /**
386 * Gets the grouping id from it's idnumber.
387 * @throws Exception
388 * @param string $idnumber
389 * @return int
390 */
391 protected function get_grouping_id($idnumber) {
392 global $DB;
393
394 // Do not fetch grouping ID for empty grouping idnumber.
395 if (empty($idnumber)) {
396 return null;
397 }
398
399 if (!$id = $DB->get_field('groupings', 'id', array('idnumber' => $idnumber))) {
400 throw new Exception('The specified grouping with idnumber "' . $idnumber . '" does not exist');
401 }
402 return $id;
403 }
404
405 /**
406 * Gets the cohort id from it's idnumber.
407 * @throws Exception
408 * @param string $idnumber
409 * @return int
410 */
411 protected function get_cohort_id($idnumber) {
412 global $DB;
413
414 if (!$id = $DB->get_field('cohort', 'id', array('idnumber' => $idnumber))) {
415 throw new Exception('The specified cohort with idnumber "' . $idnumber . '" does not exist');
416 }
417 return $id;
418 }
419
420 /**
421 * Gets the outcome item id from its shortname.
422 * @throws Exception
423 * @param string $shortname
424 * @return int
425 */
426 protected function get_outcome_id($shortname) {
427 global $DB;
428
429 if (!$id = $DB->get_field('grade_outcomes', 'id', array('shortname' => $shortname))) {
430 throw new Exception('The specified outcome with shortname "' . $shortname . '" does not exist');
431 }
432 return $id;
433 }
434
435 /**
436 * Get the id of a named scale.
437 * @param string $name the name of the scale.
438 * @return int the scale id.
439 */
440 protected function get_scale_id($name) {
441 global $DB;
442
443 if (!$id = $DB->get_field('scale', 'id', array('name' => $name))) {
444 throw new Exception('The specified scale with name "' . $name . '" does not exist');
445 }
446 return $id;
447 }
448
449 /**
450 * Get the id of a named question category (must be globally unique).
451 * Note that 'Top' is a special value, used when setting the parent of another
452 * category, meaning top-level.
453 *
454 * @param string $name the question category name.
455 * @return int the question category id.
456 */
457 protected function get_questioncategory_id($name) {
458 global $DB;
459
460 if ($name == 'Top') {
461 return 0;
462 }
463
464 if (!$id = $DB->get_field('question_categories', 'id', array('name' => $name))) {
465 throw new Exception('The specified question category with name "' . $name . '" does not exist');
466 }
467 return $id;
468 }
469
470 /**
471 * Gets the internal context id from the context reference.
472 *
473 * The context reference changes depending on the context
474 * level, it can be the system, a user, a category, a course or
475 * a module.
476 *
477 * @throws Exception
478 * @param string $levelname The context level string introduced by the test writer
479 * @param string $contextref The context reference introduced by the test writer
480 * @return context
481 */
482 protected function get_context($levelname, $contextref) {
483 global $DB;
484
485 // Getting context levels and names (we will be using the English ones as it is the test site language).
486 $contextlevels = context_helper::get_all_levels();
487 $contextnames = array();
488 foreach ($contextlevels as $level => $classname) {
489 $contextnames[context_helper::get_level_name($level)] = $level;
490 }
491
492 if (empty($contextnames[$levelname])) {
493 throw new Exception('The specified "' . $levelname . '" context level does not exist');
494 }
495 $contextlevel = $contextnames[$levelname];
496
497 // Return it, we don't need to look for other internal ids.
498 if ($contextlevel == CONTEXT_SYSTEM) {
499 return context_system::instance();
500 }
501
502 switch ($contextlevel) {
503
504 case CONTEXT_USER:
505 $instanceid = $DB->get_field('user', 'id', array('username' => $contextref));
506 break;
507
508 case CONTEXT_COURSECAT:
509 $instanceid = $DB->get_field('course_categories', 'id', array('idnumber' => $contextref));
510 break;
511
512 case CONTEXT_COURSE:
513 $instanceid = $DB->get_field('course', 'id', array('shortname' => $contextref));
514 break;
515
516 case CONTEXT_MODULE:
517 $instanceid = $DB->get_field('course_modules', 'id', array('idnumber' => $contextref));
518 break;
519
520 default:
521 break;
522 }
523
524 $contextclass = $contextlevels[$contextlevel];
525 if (!$context = $contextclass::instance($instanceid, IGNORE_MISSING)) {
526 throw new Exception('The specified "' . $contextref . '" context reference does not exist');
527 }
528
529 return $context;
530 }
531
532 /**
533 * Gets the contact id from it's username.
534 * @throws Exception
535 * @param string $username
536 * @return int
537 */
538 protected function get_contact_id($username) {
539 global $DB;
540
541 if (!$id = $DB->get_field('user', 'id', array('username' => $username))) {
542 throw new Exception('The specified user with username "' . $username . '" does not exist');
543 }
544 return $id;
545 }
15a00bea
TT
546
547 /**
548 * Gets the external backpack id from it's backpackweburl.
549 * @param string $backpackweburl
550 * @return mixed
551 * @throws dml_exception
552 */
553 protected function get_externalbackpack_id($backpackweburl) {
554 global $DB;
555 if (!$id = $DB->get_field('badge_external_backpack', 'id', ['backpackweburl' => $backpackweburl])) {
556 throw new Exception('The specified external backpack with backpackweburl "' . $username . '" does not exist');
557 }
558 return $id;
559 }
285c7036 560}