Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
16
 
17
namespace tool_generator\local\testscenario;
18
 
1441 ariadna 19
use behat_admin;
1 efrain 20
use behat_data_generators;
1441 ariadna 21
use behat_base;
22
use behat_course;
23
use behat_general;
24
use behat_user;
25
use core\attribute_helper;
1 efrain 26
use Behat\Gherkin\Parser;
27
use Behat\Gherkin\Lexer;
28
use Behat\Gherkin\Keywords\ArrayKeywords;
1441 ariadna 29
use Behat\Gherkin\Node\OutlineNode;
1 efrain 30
use ReflectionClass;
31
use ReflectionMethod;
1441 ariadna 32
use stdClass;
1 efrain 33
 
34
/**
35
 * Class to process a scenario generator file.
36
 *
37
 * @package    tool_generator
38
 * @copyright  2023 Ferran Recio <ferran@moodle.com>
39
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40
 */
41
class runner {
42
 
43
    /** @var behat_data_generators the behat data generator instance. */
44
    private behat_data_generators $generator;
45
 
46
    /** @var array of valid steps indexed by given expression tag. */
47
    private array $validsteps;
48
 
49
    /**
50
     * Initi all composer, behat libraries and load the valid steps.
51
     */
52
    public function init() {
53
        $this->include_composer_libraries();
54
        $this->include_behat_libraries();
55
        $this->load_generator();
1441 ariadna 56
        $this->load_cleanup();
1 efrain 57
    }
58
 
59
    /**
60
     * Include composer autload.
61
     */
62
    public function include_composer_libraries() {
63
        global $CFG;
64
        if (!file_exists($CFG->dirroot . '/vendor/autoload.php')) {
65
            throw new \moodle_exception('Missing composer.');
66
        }
67
        require_once($CFG->dirroot . '/vendor/autoload.php');
68
        return true;
69
    }
70
 
71
    /**
72
     * Include all necessary behat libraries.
73
     */
74
    public function include_behat_libraries() {
75
        global $CFG;
76
        if (!class_exists('Behat\Gherkin\Lexer')) {
77
            throw new \moodle_exception('Missing behat classes.');
78
        }
1441 ariadna 79
 
80
        // Behat constant.
81
        if (!defined('BEHAT_TEST')) {
82
            define('BEHAT_TEST', 1);
83
        }
84
 
1 efrain 85
        // Behat utilities.
86
        require_once($CFG->libdir . '/behat/classes/util.php');
87
        require_once($CFG->libdir . '/behat/classes/behat_command.php');
88
        require_once($CFG->libdir . '/behat/behat_base.php');
89
        require_once("{$CFG->libdir}/tests/behat/behat_data_generators.php");
1441 ariadna 90
        require_once("{$CFG->dirroot}/admin/tests/behat/behat_admin.php");
91
        require_once("{$CFG->dirroot}/course/lib.php");
92
        require_once("{$CFG->dirroot}/course/tests/behat/behat_course.php");
93
        require_once("{$CFG->dirroot}/lib/tests/behat/behat_general.php");
94
        require_once("{$CFG->dirroot}/user/tests/behat/behat_user.php");
1 efrain 95
        return true;
96
    }
97
 
98
    /**
99
     * Load all generators.
100
     */
101
    private function load_generator() {
102
        $this->generator = new behat_data_generators();
103
        $this->validsteps = $this->scan_generator($this->generator);
1441 ariadna 104
 
105
        // Add some extra steps from other classes.
106
        $extrasteps = [
107
            [behat_admin::class, 'the_following_config_values_are_set_as_admin'],
108
            [behat_general::class, 'i_enable_plugin'],
109
            [behat_general::class, 'i_disable_plugin'],
110
        ];
111
        foreach ($extrasteps as $callable) {
112
            $classname = $callable[0];
113
            $method = $callable[1];
114
            $extra = $this->scan_method(
115
                new ReflectionMethod($classname, $method),
116
                new $classname(),
117
            );
118
            if ($extra) {
119
                $this->validsteps[$extra->given] = $extra;
120
            }
121
        }
1 efrain 122
    }
123
 
124
    /**
1441 ariadna 125
     * Load all cleanup steps.
126
     */
127
    private function load_cleanup() {
128
        $extra = $this->scan_method(
129
            new ReflectionMethod(behat_course::class, 'the_course_is_deleted'),
130
            new behat_course(),
131
        );
132
        if ($extra) {
133
            $this->validsteps[$extra->given] = $extra;
134
        }
135
 
136
        $extra = $this->scan_method(
137
            new ReflectionMethod(behat_user::class, 'the_user_is_deleted'),
138
            new behat_user(),
139
        );
140
        if ($extra) {
141
            $this->validsteps[$extra->given] = $extra;
142
        }
143
    }
144
 
145
    /**
146
     * Get all valid steps.
147
     * @return array the valid steps.
148
     */
149
    public function get_valid_steps(): array {
150
        return array_values($this->validsteps);
151
    }
152
 
153
    /**
1 efrain 154
     * Scan a generator to get all valid steps.
155
     * @param behat_data_generators $generator the generator to scan.
156
     * @return array the valid steps.
157
     */
158
    private function scan_generator(behat_data_generators $generator): array {
159
        $result = [];
160
        $class = new ReflectionClass($generator);
161
        $methods = $class->getMethods(ReflectionMethod::IS_PUBLIC);
162
        foreach ($methods as $method) {
1441 ariadna 163
            $scan = $this->scan_method($method, $generator);
164
            if ($scan) {
165
                $result[$scan->given] = $scan;
1 efrain 166
            }
167
        }
168
        return $result;
169
    }
170
 
171
    /**
1441 ariadna 172
     * Scan a method to get the given expression tag.
173
     * @param ReflectionMethod $method the method to scan.
174
     * @param behat_base $behatclass the behat class instance to use.
175
     * @return stdClass|null the method data (given, name, class).
176
     */
177
    private function scan_method(ReflectionMethod $method, behat_base $behatclass): ?stdClass {
178
        $given = $this->get_method_given($method);
179
        if (!$given) {
180
            return null;
181
        }
182
        $result = (object)[
183
            'given' => $given,
184
            'name' => $method->getName(),
185
            'generator' => $behatclass,
186
            'example' => null,
187
        ];
188
        $reference = $method->getDeclaringClass()->getName() . '::' . $method->getName();
189
        if ($attribute = attribute_helper::instance($reference, \core\attribute\example::class)) {
190
            $result->example = (string) $attribute->example;
191
        }
192
        return $result;
193
    }
194
 
195
    /**
1 efrain 196
     * Get the given expression tag of a method.
197
     *
198
     * @param ReflectionMethod $method the method to get the given expression tag.
199
     * @return string|null the given expression tag or null if not found.
200
     */
201
    private function get_method_given(ReflectionMethod $method): ?string {
202
        $doccomment = $method->getDocComment();
203
        $doccomment = str_replace("\r\n", "\n", $doccomment);
204
        $doccomment = str_replace("\r", "\n", $doccomment);
205
        $doccomment = explode("\n", $doccomment);
206
        foreach ($doccomment as $line) {
207
            $matches = [];
208
            if (preg_match('/.*\@(given|when|then)\s+(.+)$/i', $line, $matches)) {
209
                return $matches[2];
210
            }
211
        }
212
        return null;
213
    }
214
 
215
    /**
216
     * Parse a feature file.
217
     * @param string $content the feature file content.
218
     * @return parsedfeature
219
     */
220
    public function parse_feature(string $content): parsedfeature {
1441 ariadna 221
        return $this->parse_selected_scenarios($content);
222
    }
223
 
224
    /**
225
     * Parse all feature file scenarios.
226
     *
227
     * Note: if no filter is passed, it will execute only the scenarios that are not tagged.
228
     *
229
     * @param string $content the feature file content.
230
     * @param string $filtertag the tag to filter the scenarios.
231
     * @return parsedfeature
232
     */
233
    private function parse_selected_scenarios(string $content, ?string $filtertag = null): parsedfeature {
1 efrain 234
        $result = new parsedfeature();
235
 
236
        $parser = $this->get_parser();
237
        $feature = $parser->parse($content);
238
 
239
        // No need for background in testing scenarios because scenarios can only contain generators.
240
        // In the future the background can be used to define clean up steps (when clean up methods
241
        // are implemented).
242
        if ($feature->hasScenarios()) {
243
            $scenarios = $feature->getScenarios();
244
            foreach ($scenarios as $scenario) {
1441 ariadna 245
                // By default, we only execute scenaros that are not tagged.
246
                if (empty($filtertag) && !empty($scenario->getTags())) {
247
                    continue;
248
                }
249
                if ($filtertag && !in_array($filtertag, $scenario->getTags())) {
250
                    continue;
251
                }
1 efrain 252
                if ($scenario->getNodeType() == 'Outline') {
1441 ariadna 253
                    $this->parse_scenario_outline($scenario, $result);
1 efrain 254
                    continue;
255
                }
256
                $result->add_scenario($scenario->getNodeType(), $scenario->getTitle());
257
                $steps = $scenario->getSteps();
258
                foreach ($steps as $step) {
1441 ariadna 259
                    $result->add_step(new steprunner(null, $this->validsteps, $step));
1 efrain 260
                }
261
            }
262
        }
263
        return $result;
264
    }
265
 
266
    /**
1441 ariadna 267
     * Parse a feature file using only the scenarios with cleanup tag.
268
     * @param string $content the feature file content.
269
     * @return parsedfeature
270
     */
271
    public function parse_cleanup(string $content): parsedfeature {
272
        return $this->parse_selected_scenarios($content, 'cleanup');
273
    }
274
 
275
    /**
276
     * Parse a scenario outline.
277
     * @param OutlineNode $scenario the scenario outline to parse.
278
     * @param parsedfeature $result the parsed feature to add the scenario.
279
     */
280
    private function parse_scenario_outline(OutlineNode $scenario, parsedfeature $result) {
281
        $count = 1;
282
        foreach ($scenario->getExamples() as $example) {
283
            $result->add_scenario($example->getNodeType(), $example->getOutlineTitle() . " ($count)");
284
            $steps = $example->getSteps();
285
            foreach ($steps as $step) {
286
                $result->add_step(new steprunner(null, $this->validsteps, $step));
287
            }
288
            $count++;
289
        }
290
    }
291
 
292
    /**
1 efrain 293
     * Get the parser.
294
     * @return Parser
295
     */
296
    private function get_parser(): Parser {
297
        $keywords = new ArrayKeywords([
298
            'en' => [
299
                'feature' => 'Feature',
300
                'background' => 'Background',
301
                'scenario' => 'Scenario',
302
                'scenario_outline' => 'Scenario Outline|Scenario Template',
303
                'examples' => 'Examples|Scenarios',
304
                'given' => 'Given',
305
                'when' => 'When',
306
                'then' => 'Then',
307
                'and' => 'And',
308
                'but' => 'But',
309
            ],
310
        ]);
311
        $lexer = new Lexer($keywords);
312
        $parser = new Parser($lexer);
313
        return $parser;
314
    }
315
 
316
    /**
317
     * Execute a parsed feature.
318
     * @param parsedfeature $parsedfeature the parsed feature to execute.
319
     * @return bool true if all steps were executed successfully.
320
     */
321
    public function execute(parsedfeature $parsedfeature): bool {
322
        if (!$parsedfeature->is_valid()) {
323
            return false;
324
        }
325
        $result = true;
326
        $steps = $parsedfeature->get_all_steps();
327
        foreach ($steps as $step) {
328
            $result = $step->execute() && $result;
329
        }
330
        return $result;
331
    }
332
}