Rev 1 | AutorÃa | Comparar con el anterior | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - https://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <https://www.gnu.org/licenses/>.namespace tool_generator\local\testscenario;use behat_admin;use behat_data_generators;use behat_base;use behat_course;use behat_general;use behat_user;use core\attribute_helper;use Behat\Gherkin\Parser;use Behat\Gherkin\Lexer;use Behat\Gherkin\Keywords\ArrayKeywords;use Behat\Gherkin\Node\OutlineNode;use ReflectionClass;use ReflectionMethod;use stdClass;/*** Class to process a scenario generator file.** @package tool_generator* @copyright 2023 Ferran Recio <ferran@moodle.com>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class runner {/** @var behat_data_generators the behat data generator instance. */private behat_data_generators $generator;/** @var array of valid steps indexed by given expression tag. */private array $validsteps;/*** Initi all composer, behat libraries and load the valid steps.*/public function init() {$this->include_composer_libraries();$this->include_behat_libraries();$this->load_generator();$this->load_cleanup();}/*** Include composer autload.*/public function include_composer_libraries() {global $CFG;if (!file_exists($CFG->dirroot . '/vendor/autoload.php')) {throw new \moodle_exception('Missing composer.');}require_once($CFG->dirroot . '/vendor/autoload.php');return true;}/*** Include all necessary behat libraries.*/public function include_behat_libraries() {global $CFG;if (!class_exists('Behat\Gherkin\Lexer')) {throw new \moodle_exception('Missing behat classes.');}// Behat constant.if (!defined('BEHAT_TEST')) {define('BEHAT_TEST', 1);}// Behat utilities.require_once($CFG->libdir . '/behat/classes/util.php');require_once($CFG->libdir . '/behat/classes/behat_command.php');require_once($CFG->libdir . '/behat/behat_base.php');require_once("{$CFG->libdir}/tests/behat/behat_data_generators.php");require_once("{$CFG->dirroot}/admin/tests/behat/behat_admin.php");require_once("{$CFG->dirroot}/course/lib.php");require_once("{$CFG->dirroot}/course/tests/behat/behat_course.php");require_once("{$CFG->dirroot}/lib/tests/behat/behat_general.php");require_once("{$CFG->dirroot}/user/tests/behat/behat_user.php");return true;}/*** Load all generators.*/private function load_generator() {$this->generator = new behat_data_generators();$this->validsteps = $this->scan_generator($this->generator);// Add some extra steps from other classes.$extrasteps = [[behat_admin::class, 'the_following_config_values_are_set_as_admin'],[behat_general::class, 'i_enable_plugin'],[behat_general::class, 'i_disable_plugin'],];foreach ($extrasteps as $callable) {$classname = $callable[0];$method = $callable[1];$extra = $this->scan_method(new ReflectionMethod($classname, $method),new $classname(),);if ($extra) {$this->validsteps[$extra->given] = $extra;}}}/*** Load all cleanup steps.*/private function load_cleanup() {$extra = $this->scan_method(new ReflectionMethod(behat_course::class, 'the_course_is_deleted'),new behat_course(),);if ($extra) {$this->validsteps[$extra->given] = $extra;}$extra = $this->scan_method(new ReflectionMethod(behat_user::class, 'the_user_is_deleted'),new behat_user(),);if ($extra) {$this->validsteps[$extra->given] = $extra;}}/*** Get all valid steps.* @return array the valid steps.*/public function get_valid_steps(): array {return array_values($this->validsteps);}/*** Scan a generator to get all valid steps.* @param behat_data_generators $generator the generator to scan.* @return array the valid steps.*/private function scan_generator(behat_data_generators $generator): array {$result = [];$class = new ReflectionClass($generator);$methods = $class->getMethods(ReflectionMethod::IS_PUBLIC);foreach ($methods as $method) {$scan = $this->scan_method($method, $generator);if ($scan) {$result[$scan->given] = $scan;}}return $result;}/*** Scan a method to get the given expression tag.* @param ReflectionMethod $method the method to scan.* @param behat_base $behatclass the behat class instance to use.* @return stdClass|null the method data (given, name, class).*/private function scan_method(ReflectionMethod $method, behat_base $behatclass): ?stdClass {$given = $this->get_method_given($method);if (!$given) {return null;}$result = (object)['given' => $given,'name' => $method->getName(),'generator' => $behatclass,'example' => null,];$reference = $method->getDeclaringClass()->getName() . '::' . $method->getName();if ($attribute = attribute_helper::instance($reference, \core\attribute\example::class)) {$result->example = (string) $attribute->example;}return $result;}/*** Get the given expression tag of a method.** @param ReflectionMethod $method the method to get the given expression tag.* @return string|null the given expression tag or null if not found.*/private function get_method_given(ReflectionMethod $method): ?string {$doccomment = $method->getDocComment();$doccomment = str_replace("\r\n", "\n", $doccomment);$doccomment = str_replace("\r", "\n", $doccomment);$doccomment = explode("\n", $doccomment);foreach ($doccomment as $line) {$matches = [];if (preg_match('/.*\@(given|when|then)\s+(.+)$/i', $line, $matches)) {return $matches[2];}}return null;}/*** Parse a feature file.* @param string $content the feature file content.* @return parsedfeature*/public function parse_feature(string $content): parsedfeature {return $this->parse_selected_scenarios($content);}/*** Parse all feature file scenarios.** Note: if no filter is passed, it will execute only the scenarios that are not tagged.** @param string $content the feature file content.* @param string $filtertag the tag to filter the scenarios.* @return parsedfeature*/private function parse_selected_scenarios(string $content, ?string $filtertag = null): parsedfeature {$result = new parsedfeature();$parser = $this->get_parser();$feature = $parser->parse($content);// No need for background in testing scenarios because scenarios can only contain generators.// In the future the background can be used to define clean up steps (when clean up methods// are implemented).if ($feature->hasScenarios()) {$scenarios = $feature->getScenarios();foreach ($scenarios as $scenario) {// By default, we only execute scenaros that are not tagged.if (empty($filtertag) && !empty($scenario->getTags())) {continue;}if ($filtertag && !in_array($filtertag, $scenario->getTags())) {continue;}if ($scenario->getNodeType() == 'Outline') {$this->parse_scenario_outline($scenario, $result);continue;}$result->add_scenario($scenario->getNodeType(), $scenario->getTitle());$steps = $scenario->getSteps();foreach ($steps as $step) {$result->add_step(new steprunner(null, $this->validsteps, $step));}}}return $result;}/*** Parse a feature file using only the scenarios with cleanup tag.* @param string $content the feature file content.* @return parsedfeature*/public function parse_cleanup(string $content): parsedfeature {return $this->parse_selected_scenarios($content, 'cleanup');}/*** Parse a scenario outline.* @param OutlineNode $scenario the scenario outline to parse.* @param parsedfeature $result the parsed feature to add the scenario.*/private function parse_scenario_outline(OutlineNode $scenario, parsedfeature $result) {$count = 1;foreach ($scenario->getExamples() as $example) {$result->add_scenario($example->getNodeType(), $example->getOutlineTitle() . " ($count)");$steps = $example->getSteps();foreach ($steps as $step) {$result->add_step(new steprunner(null, $this->validsteps, $step));}$count++;}}/*** Get the parser.* @return Parser*/private function get_parser(): Parser {$keywords = new ArrayKeywords(['en' => ['feature' => 'Feature','background' => 'Background','scenario' => 'Scenario','scenario_outline' => 'Scenario Outline|Scenario Template','examples' => 'Examples|Scenarios','given' => 'Given','when' => 'When','then' => 'Then','and' => 'And','but' => 'But',],]);$lexer = new Lexer($keywords);$parser = new Parser($lexer);return $parser;}/*** Execute a parsed feature.* @param parsedfeature $parsedfeature the parsed feature to execute.* @return bool true if all steps were executed successfully.*/public function execute(parsedfeature $parsedfeature): bool {if (!$parsedfeature->is_valid()) {return false;}$result = true;$steps = $parsedfeature->get_all_steps();foreach ($steps as $step) {$result = $step->execute() && $result;}return $result;}}