Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
    abstract protected 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
}