From 2e95fabd955f03855172b6f353fa1914f2ba2646 Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Tue, 7 Dec 2021 13:57:01 +1300 Subject: [PATCH] MDL-73272 behat: move behat extension to core --- composer.json | 14 +- composer.lock | 191 ++++------- .../Context/ContextClass/ClassResolver.php | 83 +++++ .../Initializer/MoodleAwareInitializer.php | 41 +++ .../BehatExtension/Context/MoodleContext.php | 31 ++ .../Context/Step/ChainedStep.php | 64 ++++ .../BehatExtension/Context/Step/Given.php | 38 +++ .../BehatExtension/Context/Step/Then.php | 38 +++ .../BehatExtension/Context/Step/When.php | 37 +++ .../Cli/AvailableDefinitionsController.php | 123 +++++++ .../ConsoleDefinitionInformationPrinter.php | 101 ++++++ .../BehatExtension/Driver/WebDriver.php | 59 ++++ .../Driver/WebDriverFactory.php | 67 ++++ .../Tester/ChainedStepTester.php | 263 +++++++++++++++ .../MoodleEventDispatchingStepTester.php | 130 ++++++++ .../Exception/SkippedException.php | 10 + .../FilesystemSkipPassedListLocator.php | 101 ++++++ .../Output/Formatter/MoodleListFormatter.php | 149 +++++++++ .../MoodleProgressFormatterFactory.php | 207 ++++++++++++ .../Formatter/MoodleScreenshotFormatter.php | 299 +++++++++++++++++ .../Formatter/MoodleStepcountFormatter.php | 159 +++++++++ .../Output/Printer/MoodleProgressPrinter.php | 127 ++++++++ .../ServiceContainer/BehatExtension.php | 302 ++++++++++++++++++ .../ServiceContainer/services/core.xml | 26 ++ .../Tester/Cli/SkipPassedController.php | 199 ++++++++++++ lib/behat/extension/readme_moodle.txt | 5 + lib/thirdpartylibs.xml | 7 + 27 files changed, 2744 insertions(+), 127 deletions(-) create mode 100644 lib/behat/extension/Moodle/BehatExtension/Context/ContextClass/ClassResolver.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Context/Initializer/MoodleAwareInitializer.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Context/MoodleContext.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Context/Step/ChainedStep.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Context/Step/Given.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Context/Step/Then.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Context/Step/When.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Definition/Cli/AvailableDefinitionsController.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Definition/Printer/ConsoleDefinitionInformationPrinter.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Driver/WebDriver.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Driver/WebDriverFactory.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/EventDispatcher/Tester/ChainedStepTester.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/EventDispatcher/Tester/MoodleEventDispatchingStepTester.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Exception/SkippedException.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Locator/FilesystemSkipPassedListLocator.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleListFormatter.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleProgressFormatterFactory.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleScreenshotFormatter.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleStepcountFormatter.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/Output/Printer/MoodleProgressPrinter.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/ServiceContainer/BehatExtension.php create mode 100644 lib/behat/extension/Moodle/BehatExtension/ServiceContainer/services/core.xml create mode 100644 lib/behat/extension/Moodle/BehatExtension/Tester/Cli/SkipPassedController.php create mode 100644 lib/behat/extension/readme_moodle.txt diff --git a/composer.json b/composer.json index 681f3e536bb..69d3e4f481c 100644 --- a/composer.json +++ b/composer.json @@ -12,8 +12,18 @@ ], "require-dev": { "phpunit/phpunit": "9.5.*", - "moodlehq/behat-extension": "3.400.5", - "mikey179/vfsstream": "^1.6" + "mikey179/vfsstream": "^1.6", + "behat/mink": "~1.8", + "friends-of-behat/mink-extension": "dev-master", + "behat/mink-goutte-driver": "~1.2", + "symfony/process": "^4.0 || ^5.0", + "behat/behat": "3.8.*", + "oleg-andreyev/mink-phpwebdriver": "^1.0" + }, + "autoload-dev": { + "psr-0": { + "Moodle\\BehatExtension": "lib/behat/extension/" + } }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/composer.lock b/composer.lock index 8abab3f3882..ea519f61ed5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "227721c148ea49756e15abcea9e31c12", + "content-hash": "ca8049d85aba78a5fb3ba7975e9c29c7", "packages": [], "packages-dev": [ { @@ -899,64 +899,6 @@ }, "time": "2021-09-25T08:05:01+00:00" }, - { - "name": "moodlehq/behat-extension", - "version": "v3.400.5", - "source": { - "type": "git", - "url": "https://github.com/moodlehq/moodle-behat-extension.git", - "reference": "f6c3d6e657f4a84449949a6fbf58a39e3f62d2d3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/f6c3d6e657f4a84449949a6fbf58a39e3f62d2d3", - "reference": "f6c3d6e657f4a84449949a6fbf58a39e3f62d2d3", - "shasum": "" - }, - "require": { - "behat/behat": "3.8.*", - "behat/mink": "~1.8", - "behat/mink-goutte-driver": "~1.2", - "friends-of-behat/mink-extension": "dev-master", - "oleg-andreyev/mink-phpwebdriver": "^1.0", - "php": ">=7.3.0", - "symfony/process": "^4.0 || ^5.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Moodle\\BehatExtension": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-3.0-or-later" - ], - "authors": [ - { - "name": "David Monllaó", - "email": "david.monllao@gmail.com", - "homepage": "http://moodle.com", - "role": "Developer" - }, - { - "name": "Andrew Nicols", - "email": "andrew@nicols.co.uk", - "homepage": "http://moodle.com", - "role": "Developer" - } - ], - "description": "Moodle behat extension", - "keywords": [ - "BDD", - "Behat", - "moodle" - ], - "support": { - "source": "https://github.com/moodlehq/moodle-behat-extension/tree/v3.400.5" - }, - "time": "2021-05-19T23:29:45+00:00" - }, { "name": "myclabs/deep-copy", "version": "1.10.2", @@ -974,9 +916,6 @@ "require": { "php": "^7.1 || ^8.0" }, - "replace": { - "myclabs/deep-copy": "self.version" - }, "require-dev": { "doctrine/collections": "^1.0", "doctrine/common": "^2.6", @@ -1858,16 +1797,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.10", + "version": "9.5.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c814a05837f2edb0d1471d6e3f4ab3501ca3899a" + "reference": "2406855036db1102126125537adb1406f7242fdd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c814a05837f2edb0d1471d6e3f4ab3501ca3899a", - "reference": "c814a05837f2edb0d1471d6e3f4ab3501ca3899a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2406855036db1102126125537adb1406f7242fdd", + "reference": "2406855036db1102126125537adb1406f7242fdd", "shasum": "" }, "require": { @@ -1945,11 +1884,11 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.10" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.11" }, "funding": [ { - "url": "https://phpunit.de/donate.html", + "url": "https://phpunit.de/sponsors.html", "type": "custom" }, { @@ -1957,7 +1896,7 @@ "type": "github" } ], - "time": "2021-09-25T07:38:51+00:00" + "time": "2021-12-25T07:07:57+00:00" }, { "name": "psr/container", @@ -3192,16 +3131,16 @@ }, { "name": "symfony/config", - "version": "v5.4.0", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "e39cf688c80fd79ab0a6a2d05a9facac9b2d534b" + "reference": "2e082dae50da563c639119b7b52347a2a3db4ba5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/e39cf688c80fd79ab0a6a2d05a9facac9b2d534b", - "reference": "e39cf688c80fd79ab0a6a2d05a9facac9b2d534b", + "url": "https://api.github.com/repos/symfony/config/zipball/2e082dae50da563c639119b7b52347a2a3db4ba5", + "reference": "2e082dae50da563c639119b7b52347a2a3db4ba5", "shasum": "" }, "require": { @@ -3251,7 +3190,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v5.4.0" + "source": "https://github.com/symfony/config/tree/v5.4.2" }, "funding": [ { @@ -3267,20 +3206,20 @@ "type": "tidelift" } ], - "time": "2021-11-28T15:25:38+00:00" + "time": "2021-12-15T11:06:13+00:00" }, { "name": "symfony/console", - "version": "v5.4.1", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "9130e1a0fc93cb0faadca4ee917171bd2ca9e5f4" + "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/9130e1a0fc93cb0faadca4ee917171bd2ca9e5f4", - "reference": "9130e1a0fc93cb0faadca4ee917171bd2ca9e5f4", + "url": "https://api.github.com/repos/symfony/console/zipball/a2c6b7ced2eb7799a35375fb9022519282b5405e", + "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e", "shasum": "" }, "require": { @@ -3350,7 +3289,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.1" + "source": "https://github.com/symfony/console/tree/v5.4.2" }, "funding": [ { @@ -3366,20 +3305,20 @@ "type": "tidelift" } ], - "time": "2021-12-09T11:22:43+00:00" + "time": "2021-12-20T16:11:12+00:00" }, { "name": "symfony/css-selector", - "version": "v5.4.0", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "44b933f98bb4b5220d10bed9ce5662f8c2d13dcc" + "reference": "cfcbee910e159df402603502fe387e8b677c22fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/44b933f98bb4b5220d10bed9ce5662f8c2d13dcc", - "reference": "44b933f98bb4b5220d10bed9ce5662f8c2d13dcc", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/cfcbee910e159df402603502fe387e8b677c22fd", + "reference": "cfcbee910e159df402603502fe387e8b677c22fd", "shasum": "" }, "require": { @@ -3416,7 +3355,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v5.4.0" + "source": "https://github.com/symfony/css-selector/tree/v5.4.2" }, "funding": [ { @@ -3432,20 +3371,20 @@ "type": "tidelift" } ], - "time": "2021-09-09T08:06:01+00:00" + "time": "2021-12-16T21:58:21+00:00" }, { "name": "symfony/dependency-injection", - "version": "v5.4.1", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "9bd1ef389a2fe05fea7306b6155403e8a960d73d" + "reference": "ba94559be9738d77cd29e24b5d81cf3b89b7d628" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/9bd1ef389a2fe05fea7306b6155403e8a960d73d", - "reference": "9bd1ef389a2fe05fea7306b6155403e8a960d73d", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/ba94559be9738d77cd29e24b5d81cf3b89b7d628", + "reference": "ba94559be9738d77cd29e24b5d81cf3b89b7d628", "shasum": "" }, "require": { @@ -3505,7 +3444,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v5.4.1" + "source": "https://github.com/symfony/dependency-injection/tree/v5.4.2" }, "funding": [ { @@ -3521,7 +3460,7 @@ "type": "tidelift" } ], - "time": "2021-12-01T16:25:34+00:00" + "time": "2021-12-29T10:10:35+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3592,16 +3531,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v4.4.30", + "version": "v4.4.36", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "4632ae3567746c7e915c33c67a2fb6ab746090c4" + "reference": "42de12bee3b5e594977209bcdf58ec4fef8dde39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/4632ae3567746c7e915c33c67a2fb6ab746090c4", - "reference": "4632ae3567746c7e915c33c67a2fb6ab746090c4", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/42de12bee3b5e594977209bcdf58ec4fef8dde39", + "reference": "42de12bee3b5e594977209bcdf58ec4fef8dde39", "shasum": "" }, "require": { @@ -3646,7 +3585,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v4.4.30" + "source": "https://github.com/symfony/dom-crawler/tree/v4.4.36" }, "funding": [ { @@ -3662,7 +3601,7 @@ "type": "tidelift" } ], - "time": "2021-08-28T15:40:01+00:00" + "time": "2021-12-28T14:48:02+00:00" }, { "name": "symfony/event-dispatcher", @@ -4622,16 +4561,16 @@ }, { "name": "symfony/process", - "version": "v5.4.0", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "5be20b3830f726e019162b26223110c8f47cf274" + "reference": "2b3ba8722c4aaf3e88011be5e7f48710088fb5e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/5be20b3830f726e019162b26223110c8f47cf274", - "reference": "5be20b3830f726e019162b26223110c8f47cf274", + "url": "https://api.github.com/repos/symfony/process/zipball/2b3ba8722c4aaf3e88011be5e7f48710088fb5e4", + "reference": "2b3ba8722c4aaf3e88011be5e7f48710088fb5e4", "shasum": "" }, "require": { @@ -4664,7 +4603,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.0" + "source": "https://github.com/symfony/process/tree/v5.4.2" }, "funding": [ { @@ -4680,7 +4619,7 @@ "type": "tidelift" } ], - "time": "2021-11-28T15:25:38+00:00" + "time": "2021-12-27T21:01:00+00:00" }, { "name": "symfony/service-contracts", @@ -4767,16 +4706,16 @@ }, { "name": "symfony/string", - "version": "v5.4.0", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "9ffaaba53c61ba75a3c7a3a779051d1e9ec4fd2d" + "reference": "e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/9ffaaba53c61ba75a3c7a3a779051d1e9ec4fd2d", - "reference": "9ffaaba53c61ba75a3c7a3a779051d1e9ec4fd2d", + "url": "https://api.github.com/repos/symfony/string/zipball/e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d", + "reference": "e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d", "shasum": "" }, "require": { @@ -4833,7 +4772,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.0" + "source": "https://github.com/symfony/string/tree/v5.4.2" }, "funding": [ { @@ -4849,20 +4788,20 @@ "type": "tidelift" } ], - "time": "2021-11-24T10:02:00+00:00" + "time": "2021-12-16T21:52:00+00:00" }, { "name": "symfony/translation", - "version": "v5.4.1", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "8c82cd35ed861236138d5ae1c78c0c7ebcd62107" + "reference": "ff8bb2107b6a549dc3c5dd9c498dcc82c9c098ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/8c82cd35ed861236138d5ae1c78c0c7ebcd62107", - "reference": "8c82cd35ed861236138d5ae1c78c0c7ebcd62107", + "url": "https://api.github.com/repos/symfony/translation/zipball/ff8bb2107b6a549dc3c5dd9c498dcc82c9c098ca", + "reference": "ff8bb2107b6a549dc3c5dd9c498dcc82c9c098ca", "shasum": "" }, "require": { @@ -4930,7 +4869,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v5.4.1" + "source": "https://github.com/symfony/translation/tree/v5.4.2" }, "funding": [ { @@ -4946,7 +4885,7 @@ "type": "tidelift" } ], - "time": "2021-12-05T20:33:52+00:00" + "time": "2021-12-25T19:45:36+00:00" }, { "name": "symfony/translation-contracts", @@ -5028,16 +4967,16 @@ }, { "name": "symfony/yaml", - "version": "v5.4.0", + "version": "v5.4.2", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "034ccc0994f1ae3f7499fa5b1f2e75d5e7a94efc" + "reference": "b9eb163846a61bb32dfc147f7859e274fab38b58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/034ccc0994f1ae3f7499fa5b1f2e75d5e7a94efc", - "reference": "034ccc0994f1ae3f7499fa5b1f2e75d5e7a94efc", + "url": "https://api.github.com/repos/symfony/yaml/zipball/b9eb163846a61bb32dfc147f7859e274fab38b58", + "reference": "b9eb163846a61bb32dfc147f7859e274fab38b58", "shasum": "" }, "require": { @@ -5083,7 +5022,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.4.0" + "source": "https://github.com/symfony/yaml/tree/v5.4.2" }, "funding": [ { @@ -5099,7 +5038,7 @@ "type": "tidelift" } ], - "time": "2021-11-28T15:25:38+00:00" + "time": "2021-12-16T21:58:21+00:00" }, { "name": "theseer/tokenizer", @@ -5212,7 +5151,9 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": { + "friends-of-behat/mink-extension": 20 + }, "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -5237,5 +5178,5 @@ "ext-fileinfo": "*" }, "platform-dev": [], - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.2.0" } diff --git a/lib/behat/extension/Moodle/BehatExtension/Context/ContextClass/ClassResolver.php b/lib/behat/extension/Moodle/BehatExtension/Context/ContextClass/ClassResolver.php new file mode 100644 index 00000000000..358325f5dd7 --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Context/ContextClass/ClassResolver.php @@ -0,0 +1,83 @@ +. + +/** + * Moodle behat context class resolver. + * + * @package behat + * @copyright 2104 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace Moodle\BehatExtension\Context\ContextClass; + +use Behat\Behat\Context\Environment\Handler\ContextEnvironmentHandler; +use Behat\Behat\Context\ContextClass\ClassResolver as Resolver; + +/** + * Resolves arbitrary context strings into a context classes. + * + * @see ContextEnvironmentHandler + * + * @author Konstantin Kudryashov + */ +final class ClassResolver implements Resolver { + + /** + * @var array keep list of all behat contexts in moodle. + */ + private $moodlebehatcontexts = null; + + /** + * @param $parameters array list of params provided to moodle. + */ + public function __construct($parameters) { + $this->moodlebehatcontexts = $parameters['steps_definitions']; + } + /** + * Checks if resolvers supports provided class. + * Moodle behat context class starts with behat_ + * + * @param string $contextString + * + * @return Boolean + */ + public function supportsClass($contextString) { + return (strpos($contextString, 'behat_') === 0); + } + + /** + * Resolves context class. + * + * @param string $contexclass + * + * @return string context class. + */ + public function resolveClass($contextclass) { + if (!is_array($this->moodlebehatcontexts)) { + throw new \RuntimeException('There are no Moodle context with steps definitions'); + } + + // Using the key as context identifier load context class. + if (!empty($this->moodlebehatcontexts[$contextclass]) && + (file_exists($this->moodlebehatcontexts[$contextclass]))) { + require_once($this->moodlebehatcontexts[$contextclass]); + } else { + throw new \RuntimeException('Moodle behat context "'.$contextclass.'" not found'); + } + return $contextclass; + } +} diff --git a/lib/behat/extension/Moodle/BehatExtension/Context/Initializer/MoodleAwareInitializer.php b/lib/behat/extension/Moodle/BehatExtension/Context/Initializer/MoodleAwareInitializer.php new file mode 100644 index 00000000000..a8b0ce8c5e8 --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Context/Initializer/MoodleAwareInitializer.php @@ -0,0 +1,41 @@ + + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class MoodleAwareInitializer implements ContextInitializer +{ + private $parameters; + + + /** + * Initializes initializer. + * + * @param Mink $mink + * @param array $parameters + */ + public function __construct(array $parameters) { + $this->parameters = $parameters; + } + + /** + * Initializes provided context. + * + * @param Context $context + */ + public function initializeContext(Context $context) { + if (method_exists($context, 'setMoodleConfig')) { + $context->setMoodleConfig($this->parameters); + } + } +} diff --git a/lib/behat/extension/Moodle/BehatExtension/Context/MoodleContext.php b/lib/behat/extension/Moodle/BehatExtension/Context/MoodleContext.php new file mode 100644 index 00000000000..5f8ee7d367b --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Context/MoodleContext.php @@ -0,0 +1,31 @@ +moodleConfig = $parameters; + } +} diff --git a/lib/behat/extension/Moodle/BehatExtension/Context/Step/ChainedStep.php b/lib/behat/extension/Moodle/BehatExtension/Context/Step/ChainedStep.php new file mode 100644 index 00000000000..9a67f8e7760 --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Context/Step/ChainedStep.php @@ -0,0 +1,64 @@ +. + +/** + * Override step tester to ensure chained steps gets executed. + * + * @package behat + * @copyright 2016 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace Moodle\BehatExtension\Context\Step; + +use Behat\Gherkin\Node\StepNode; +/** + * Base ChainedStep class. + */ +abstract class ChainedStep extends StepNode { + /** + * @var string + */ + private $language; + + /** + * Initializes ChainedStep. + * + * @param string $type + * @param string $text + * @param array $arguments + */ + public function __construct($keyword, $text, array $arguments, $line = 0, $keywordType = 'Given') { + parent::__construct($keyword, $text, $arguments, $line, $keywordType); + } + + /** + * Sets language. + * + * @param string $language + */ + public function setLanguage($language) { + $this->language = $language; + } + + /** + * Returns language. + * + * @return string + */ + public function getLanguage() { + return $this->language; + } +} \ No newline at end of file diff --git a/lib/behat/extension/Moodle/BehatExtension/Context/Step/Given.php b/lib/behat/extension/Moodle/BehatExtension/Context/Step/Given.php new file mode 100644 index 00000000000..db6dc2a624e --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Context/Step/Given.php @@ -0,0 +1,38 @@ +. + +/** + * Override step tester to ensure chained steps gets executed. + * + * @package behat + * @copyright 2016 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace Moodle\BehatExtension\Context\Step; +/** + * Given sub-step. + */ +class Given extends ChainedStep { + /** + * Initializes `Given` sub-step. + */ + public function __construct() { + $arguments = func_get_args(); + $text = array_shift($arguments); + parent::__construct('Given', $text, $arguments); + } +} \ No newline at end of file diff --git a/lib/behat/extension/Moodle/BehatExtension/Context/Step/Then.php b/lib/behat/extension/Moodle/BehatExtension/Context/Step/Then.php new file mode 100644 index 00000000000..ad715b71ba8 --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Context/Step/Then.php @@ -0,0 +1,38 @@ +. + +/** + * Override step tester to ensure chained steps gets executed. + * + * @package behat + * @copyright 2016 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace Moodle\BehatExtension\Context\Step; + +/** + * `Then` ChainedStep. + */ +class Then extends ChainedStep { + /** + * Initializes `Then` sub-step. + */ + public function __construct() { + $arguments = func_get_args(); + $text = array_shift($arguments); + parent::__construct('Then', $text, $arguments); + } +} \ No newline at end of file diff --git a/lib/behat/extension/Moodle/BehatExtension/Context/Step/When.php b/lib/behat/extension/Moodle/BehatExtension/Context/Step/When.php new file mode 100644 index 00000000000..52f8df593ae --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Context/Step/When.php @@ -0,0 +1,37 @@ +. + +/** + * Override step tester to ensure chained steps gets executed. + * + * @package behat + * @copyright 2016 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace Moodle\BehatExtension\Context\Step; +/** + * `When` ChainedStep. + */ +class When extends ChainedStep { + /** + * Initializes `When` sub-step. + */ + public function __construct() { + $arguments = func_get_args(); + $text = array_shift($arguments); + parent::__construct('When', $text, $arguments); + } +} \ No newline at end of file diff --git a/lib/behat/extension/Moodle/BehatExtension/Definition/Cli/AvailableDefinitionsController.php b/lib/behat/extension/Moodle/BehatExtension/Definition/Cli/AvailableDefinitionsController.php new file mode 100644 index 00000000000..4014d7f3ddd --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Definition/Cli/AvailableDefinitionsController.php @@ -0,0 +1,123 @@ +. + +namespace Moodle\BehatExtension\Definition\Cli; + +use Behat\Behat\Definition\DefinitionWriter; +use Moodle\BehatExtension\Definition\Printer\ConsoleDefinitionInformationPrinter; +use Behat\Behat\Definition\Printer\ConsoleDefinitionListPrinter; +use Behat\Behat\Definition\Printer\DefinitionPrinter; +use Behat\Testwork\Cli\Controller; +use Behat\Testwork\Suite\SuiteRepository; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Available definition controller, for calling moodle information printer. + * + * @package behat + * @copyright 2016 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class AvailableDefinitionsController implements Controller { + /** + * @var SuiteRepository + */ + private $suiteRepository; + /** + * @var DefinitionWriter + */ + private $writer; + /** + * @var ConsoleDefinitionListPrinter + */ + private $listPrinter; + /** + * @var ConsoleDefinitionInformationPrinter + */ + private $infoPrinter; + + /** + * Initializes controller. + * + * @param SuiteRepository $suiteRepository + * @param DefinitionWriter $writer + * @param ConsoleDefinitionListPrinter $listPrinter + * @param ConsoleDefinitionInformationPrinter $infoPrinter + */ + public function __construct( + SuiteRepository $suiteRepository, + DefinitionWriter $writer, + ConsoleDefinitionListPrinter $listPrinter, + ConsoleDefinitionInformationPrinter $infoPrinter + ) { + $this->suiteRepository = $suiteRepository; + $this->writer = $writer; + $this->listPrinter = $listPrinter; + $this->infoPrinter = $infoPrinter; + } + + /** + * {@inheritdoc} + */ + public function configure(Command $command) { + $command->addOption('--definitions', '-d', InputOption::VALUE_REQUIRED, + "Print all available step definitions:" . PHP_EOL . + "- use --definitions l to just list definition expressions." . PHP_EOL . + "- use --definitions i to show definitions with extended info." . PHP_EOL . + "- use --definitions 'needle' to find specific definitions." . PHP_EOL . + "Use --lang to see definitions in specific language." + ); + } + + /** + * {@inheritdoc} + */ + public function execute(InputInterface $input, OutputInterface $output) { + if (null === $argument = $input->getOption('definitions')) { + return null; + } + + $printer = $this->getDefinitionPrinter($argument); + foreach ($this->suiteRepository->getSuites() as $suite) { + $this->writer->printSuiteDefinitions($printer, $suite); + } + + return 0; + } + + /** + * Returns definition printer for provided option argument. + * + * @param string $argument + * + * @return DefinitionPrinter + */ + private function getDefinitionPrinter($argument) { + if ('l' === $argument) { + return $this->listPrinter; + } + + if ('i' !== $argument) { + $this->infoPrinter->setSearchCriterion($argument); + } + + return $this->infoPrinter; + } +} diff --git a/lib/behat/extension/Moodle/BehatExtension/Definition/Printer/ConsoleDefinitionInformationPrinter.php b/lib/behat/extension/Moodle/BehatExtension/Definition/Printer/ConsoleDefinitionInformationPrinter.php new file mode 100644 index 00000000000..d8f6659a79b --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Definition/Printer/ConsoleDefinitionInformationPrinter.php @@ -0,0 +1,101 @@ +. + +namespace Moodle\BehatExtension\Definition\Printer; + +use Behat\Behat\Definition\Definition; +use Behat\Testwork\Suite\Suite; +use Behat\Behat\Definition\Printer\ConsoleDefinitionPrinter; + +/** + * Moodle console definition information printer. + * Used in moodle for definition printing. + * + * @package behat + * @copyright 2016 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class ConsoleDefinitionInformationPrinter extends ConsoleDefinitionPrinter { + /** + * @var null|string + */ + private $searchCriterion; + + /** + * Sets search criterion. + * + * @param string $criterion + */ + public function setSearchCriterion($criterion) { + $this->searchCriterion = $criterion; + } + + /** + * {@inheritdoc} + */ + public function printDefinitions(Suite $suite, $definitions) { + $template = <<
{description}
+
{type}{regex}
+
{apipath}
+ +TPL; + + $search = $this->searchCriterion; + + // If there is a specific type (given, when or then) required. + if (strpos($search, '&&') !== false) { + list($search, $type) = explode('&&', $search); + } + + foreach ($definitions as $definition) { + $definition = $this->translateDefinition($suite, $definition); + + if (!empty($type) && strtolower($definition->getType()) != $type) { + continue; + } + + $pattern = $definition->getPattern(); + + if ($search && !preg_match('/'.str_replace(' ', '.*', preg_quote($search, '/').'/'), $pattern)) { + continue; + } + + $description = $definition->getDescription(); + + // Removing beginning and end. + $pattern = substr($pattern, 2, strlen($pattern) - 4); + + // Replacing inline regex for expected info string. + $pattern = preg_replace_callback( + '/"\(\?P<([^>]*)>(.*?)"( |$)/', + function ($matches) { + return '"' . strtoupper($matches[1]) . '" '; + }, $pattern); + + $definitiontoprint[] = strtr($template, array( + '{regex}' => $pattern, + '{type}' => str_pad($definition->getType(), 5, ' ', STR_PAD_LEFT), + '{description}' => $description ? $description : '', + '{apipath}' => $definition->getPath() + )); + + $this->write(implode("\n", $definitiontoprint)); + unset($definitiontoprint); + } + } +} diff --git a/lib/behat/extension/Moodle/BehatExtension/Driver/WebDriver.php b/lib/behat/extension/Moodle/BehatExtension/Driver/WebDriver.php new file mode 100644 index 00000000000..df31cb68e32 --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Driver/WebDriver.php @@ -0,0 +1,59 @@ +. + +/** + * Driver factory for the Moodle WebDriver. + * + * @package behat + * @copyright 2020 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace Moodle\BehatExtension\Driver; + +use Behat\MinkExtension\ServiceContainer\Driver\DriverFactory; +use OAndreyev\Mink\Driver\WebDriverFactory as UpstreamFactory; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\DependencyInjection\Definition; + +class WebDriverFactory extends UpstreamFactory implements DriverFactory { + /** + * {@inheritdoc} + */ + public function buildDriver(array $config) + { + // Merge capabilities + $extraCapabilities = $config['capabilities']['extra_capabilities']; + unset($config['capabilities']['extra_capabilities']); + + // Ensure that the capabilites.browser is set correctly. + $config['capabilities']['browser'] = $config['browser']; + + $capabilities = array_replace($this->guessCapabilities(), $extraCapabilities, $config['capabilities']); + + // Build driver definition + return new Definition(WebDriver::class, [ + $config['browser'], + $capabilities, + $config['wd_host'], + ]); + } + + /** + * {@inheritdoc} + */ + protected function getCapabilitiesNode() + { + $node = parent::getCapabilitiesNode(); + + // Specify chrome as the default browser. + $node->find('browser')->defaultValue('chrome'); + + return $node; + } +} diff --git a/lib/behat/extension/Moodle/BehatExtension/EventDispatcher/Tester/ChainedStepTester.php b/lib/behat/extension/Moodle/BehatExtension/EventDispatcher/Tester/ChainedStepTester.php new file mode 100644 index 00000000000..e480e001dda --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/EventDispatcher/Tester/ChainedStepTester.php @@ -0,0 +1,263 @@ +. + +/** + * Override step tester to ensure chained steps gets executed. + * + * @package behat + * @copyright 2016 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace Moodle\BehatExtension\EventDispatcher\Tester; + +use Behat\Behat\Tester\Result\ExecutedStepResult; +use Behat\Behat\Tester\Result\SkippedStepResult; +use Behat\Behat\Tester\Result\StepResult; +use Behat\Behat\Tester\StepTester; +use Behat\Behat\Tester\Result\UndefinedStepResult; +use Moodle\BehatExtension\Context\Step\Given; +use Moodle\BehatExtension\Context\Step\ChainedStep; +use Behat\Gherkin\Node\FeatureNode; +use Behat\Gherkin\Node\StepNode; +use Behat\Testwork\Call\CallResult; +use Behat\Testwork\Environment\Environment; +use Behat\Testwork\EventDispatcher\TestworkEventDispatcher; +use Behat\Behat\EventDispatcher\Event\AfterStepSetup; +use Behat\Behat\EventDispatcher\Event\AfterStepTested; +use Behat\Behat\EventDispatcher\Event\BeforeStepTeardown; +use Behat\Behat\EventDispatcher\Event\BeforeStepTested; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Moodle\BehatExtension\Exception\SkippedException; + +/** + * Override step tester to ensure chained steps gets executed. + * + * @package behat + * @copyright 2016 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class ChainedStepTester implements StepTester { + /** + * The text of the step to look for exceptions / debugging messages. + */ + const EXCEPTIONS_STEP_TEXT = 'I look for exceptions'; + + /** + * @var StepTester Base step tester. + */ + private $singlesteptester; + + /** + * @var EventDispatcher keep step event dispatcher. + */ + private $eventDispatcher; + + /** + * Keep status of chained steps if used. + * @var bool + */ + protected static $chainedstepused = false; + + /** + * Constructor. + * + * @param StepTester $steptester single step tester. + */ + public function __construct(StepTester $steptester) { + $this->singlesteptester = $steptester; + } + + /** + * Set event dispatcher to use for events. + * + * @param EventDispatcherInterface $eventDispatcher + */ + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher) { + $this->eventDispatcher = $eventDispatcher; + } + + /** + * {@inheritdoc} + */ + public function setUp(Environment $env, FeatureNode $feature, StepNode $step, $skip) { + return $this->singlesteptester->setUp($env, $feature, $step, $skip); + } + + /** + * {@inheritdoc} + */ + public function test(Environment $env, FeatureNode $feature, StepNode $step, $skip) { + $result = $this->singlesteptester->test($env, $feature, $step, $skip); + + if (!($result instanceof ExecutedStepResult) || !$this->supportsResult($result->getCallResult())) { + $result = $this->checkSkipResult($result); + + // If undefined step then don't continue chained steps. + if ($result instanceof UndefinedStepResult) { + return $result; + } + + // If exception caught, then don't continue chained steps. + if (($result instanceof ExecutedStepResult) && $result->hasException()) { + return $result; + } + + // If step is skipped, then return. no need to continue chain steps. + if ($result instanceof SkippedStepResult) { + return $result; + } + + // Check for exceptions. + // Extra step, looking for a moodle exception, a debugging() message or a PHP debug message. + $checkingStep = new StepNode('Given', self::EXCEPTIONS_STEP_TEXT, array(), $step->getLine()); + $afterExceptionCheckingEvent = $this->singlesteptester->test($env, $feature, $checkingStep, $skip); + return $this->checkSkipResult($afterExceptionCheckingEvent); + } + + return $this->runChainedSteps($env, $feature, $result, $skip); + } + + /** + * {@inheritdoc} + */ + public function tearDown(Environment $env, FeatureNode $feature, StepNode $step, $skip, StepResult $result) { + return $this->singlesteptester->tearDown($env, $feature, $step, $skip, $result); + } + + /** + * Check if results supported. + * + * @param CallResult $result + * @return bool + */ + private function supportsResult(CallResult $result) { + $return = $result->getReturn(); + if ($return instanceof ChainedStep) { + return true; + } + if (!is_array($return) || empty($return)) { + return false; + } + foreach ($return as $value) { + if (!$value instanceof ChainedStep) { + return false; + } + } + return true; + } + + /** + * Run chained steps. + * + * @param Environment $env + * @param FeatureNode $feature + * @param ExecutedStepResult $result + * @param $skip + * + * @return ExecutedStepResult|StepResult + */ + private function runChainedSteps(Environment $env, FeatureNode $feature, ExecutedStepResult $result, $skip) { + // Set chained setp is used, so it can be used by formatter to o/p. + self::$chainedstepused = true; + + $callResult = $result->getCallResult(); + $steps = $callResult->getReturn(); + + if (!is_array($steps)) { + // Test it, no need to dispatch events for single chain. + $stepResult = $this->test($env, $feature, $steps, $skip); + return $this->checkSkipResult($stepResult); + } + + // Test all steps. + foreach ($steps as $step) { + // Setup new step. + $event = new BeforeStepTested($env, $feature, $step); + if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) { + // Symfony 4.3 and up. + $this->eventDispatcher->dispatch($event, $event::BEFORE); + } else { + // TODO: Remove when our min supported version is >= 4.3. + $this->eventDispatcher->dispatch($event::BEFORE, $event); + } + + $setup = $this->setUp($env, $feature, $step, $skip); + + $event = new AfterStepSetup($env, $feature, $step, $setup); + if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) { + // Symfony 4.3 and up. + $this->eventDispatcher->dispatch($event, $event::AFTER_SETUP); + } else { + // TODO: Remove when our min supported version is >= 4.3. + $this->eventDispatcher->dispatch($event::AFTER_SETUP, $event); + } + + // Test it. + $stepResult = $this->test($env, $feature, $step, $skip); + + // Tear down. + $event = new BeforeStepTeardown($env, $feature, $step, $result); + if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) { + // Symfony 4.3 and up. + $this->eventDispatcher->dispatch($event, $event::BEFORE_TEARDOWN); + } else { + // TODO: Remove when our min supported version is >= 4.3. + $this->eventDispatcher->dispatch($event::BEFORE_TEARDOWN, $event); + } + + $teardown = $this->tearDown($env, $feature, $step, $skip, $result); + + $event = new AfterStepTested($env, $feature, $step, $result, $teardown); + if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) { + // Symfony 4.3 and up. + $this->eventDispatcher->dispatch($event, $event::AFTER); + } else { + // TODO: Remove when our min supported version is >= 4.3. + $this->eventDispatcher->dispatch($event::AFTER, $event); + } + + // + if (!$stepResult->isPassed()) { + return $this->checkSkipResult($stepResult); + } + } + return $this->checkSkipResult($stepResult); + } + + /** + * Handle skip exception. + * + * @param StepResult $result + * + * @return ExecutedStepResult|SkippedStepResult + */ + private function checkSkipResult(StepResult $result) { + if ((method_exists($result, 'getException')) && ($result->getException() instanceof SkippedException)) { + return new SkippedStepResult($result->getSearchResult()); + } else { + return $result; + } + } + + /** + * Returns if cahined steps are used. + * @return bool. + */ + public static function is_chained_step_used() { + return self::$chainedstepused; + } +} diff --git a/lib/behat/extension/Moodle/BehatExtension/EventDispatcher/Tester/MoodleEventDispatchingStepTester.php b/lib/behat/extension/Moodle/BehatExtension/EventDispatcher/Tester/MoodleEventDispatchingStepTester.php new file mode 100644 index 00000000000..0add0b8151f --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/EventDispatcher/Tester/MoodleEventDispatchingStepTester.php @@ -0,0 +1,130 @@ +. + +/** + * Override step tester to ensure chained steps gets executed. + * + * @package behat + * @copyright 2016 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace Moodle\BehatExtension\EventDispatcher\Tester; + +use Behat\Behat\EventDispatcher\Event\AfterStepSetup; +use Behat\Behat\EventDispatcher\Event\AfterStepTested; +use Behat\Behat\EventDispatcher\Event\BeforeStepTeardown; +use Behat\Behat\EventDispatcher\Event\BeforeStepTested; +use Behat\Behat\Tester\Result\StepResult; +use Behat\Behat\Tester\StepTester; +use Behat\Gherkin\Node\FeatureNode; +use Behat\Gherkin\Node\StepNode; +use Behat\Testwork\Environment\Environment; +use Behat\Testwork\EventDispatcher\TestworkEventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; + +/** + * Step tester dispatching BEFORE/AFTER events during tests. + * + * @package behat + * @copyright 2016 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class MoodleEventDispatchingStepTester implements StepTester +{ + /** + * @var StepTester + */ + private $baseTester; + /** + * @var EventDispatcherInterface + */ + private $eventDispatcher; + + /** + * Initializes tester. + * + * @param StepTester $baseTester + * @param EventDispatcherInterface $eventDispatcher + */ + public function __construct(StepTester $baseTester, EventDispatcherInterface $eventDispatcher) { + $this->baseTester = $baseTester; + $this->eventDispatcher = $eventDispatcher; + } + + /** + * {@inheritdoc} + */ + public function setUp(Environment $env, FeatureNode $feature, StepNode $step, $skip) { + $event = new BeforeStepTested($env, $feature, $step); + if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) { + // Symfony 4.3 and up. + $this->eventDispatcher->dispatch($event, $event::BEFORE); + } else { + // TODO: Remove when our min supported version is >= 4.3. + $this->eventDispatcher->dispatch($event::BEFORE, $event); + } + + $setup = $this->baseTester->setUp($env, $feature, $step, $skip); + $this->baseTester->setEventDispatcher($this->eventDispatcher); + + $event = new AfterStepSetup($env, $feature, $step, $setup); + if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) { + // Symfony 4.3 and up. + $this->eventDispatcher->dispatch($event, $event::AFTER_SETUP); + } else { + // TODO: Remove when our min supported version is >= 4.3. + $this->eventDispatcher->dispatch($event::AFTER_SETUP, $event); + } + + return $setup; + } + + /** + * {@inheritdoc} + */ + public function test(Environment $env, FeatureNode $feature, StepNode $step, $skip) { + return $this->baseTester->test($env, $feature, $step, $skip); + } + + /** + * {@inheritdoc} + */ + public function tearDown(Environment $env, FeatureNode $feature, StepNode $step, $skip, StepResult $result) { + $event = new BeforeStepTeardown($env, $feature, $step, $result); + if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) { + // Symfony 4.3 and up. + $this->eventDispatcher->dispatch($event, $event::BEFORE_TEARDOWN); + } else { + // TODO: Remove when our min supported version is >= 4.3. + $this->eventDispatcher->dispatch($event::BEFORE_TEARDOWN, $event); + } + + $teardown = $this->baseTester->tearDown($env, $feature, $step, $skip, $result); + + $event = new AfterStepTested($env, $feature, $step, $result, $teardown); + if (TestworkEventDispatcher::DISPATCHER_VERSION === 2) { + // Symfony 4.3 and up. + $this->eventDispatcher->dispatch($event, $event::AFTER); + } else { + // TODO: Remove when our min supported version is >= 4.3. + $this->eventDispatcher->dispatch($event::AFTER, $event); + } + + return $teardown; + } +} diff --git a/lib/behat/extension/Moodle/BehatExtension/Exception/SkippedException.php b/lib/behat/extension/Moodle/BehatExtension/Exception/SkippedException.php new file mode 100644 index 00000000000..0b292b782c8 --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Exception/SkippedException.php @@ -0,0 +1,10 @@ +. + +/** + * Skips gherkin features using a file with the list of scenarios. + * + * @copyright 2016 onwards Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace Moodle\BehatExtension\Locator; + +use Behat\Behat\Gherkin\Specification\LazyFeatureIterator; +use Behat\Gherkin\Gherkin; +use Behat\Testwork\Specification\Locator\SpecificationLocator; +use Behat\Testwork\Specification\NoSpecificationsIterator; +use Behat\Testwork\Suite\Suite; + +/** + * Skips gherkin features using a file with the list of scenarios. + * + * @copyright 2016 onwards Rajesh Taneja + */ +final class FilesystemSkipPassedListLocator implements SpecificationLocator { + /** + * @var Gherkin + */ + private $gherkin; + + /** + * Initializes locator. + * + * @param Gherkin $gherkin + */ + public function __construct(Gherkin $gherkin) { + $this->gherkin = $gherkin; + } + + /** + * {@inheritdoc} + */ + public function getLocatorExamples() { + return array(); + } + + /** + * {@inheritdoc} + */ + public function locateSpecifications(Suite $suite, $locator) { + if (!is_file($locator) || 'passed' !== pathinfo($locator, PATHINFO_EXTENSION)) { + return new NoSpecificationsIterator($suite); + } + + $scenarios = json_decode(trim(file_get_contents($locator)), true); + if (empty($scenarios) || empty($scenarios[$suite->getName()])) { + return new NoSpecificationsIterator($suite); + } + + $suitepaths = $this->getSuitePaths($suite); + + $scenarios = array_diff($suitepaths, array_values($scenarios[$suite->getName()])); + + return new LazyFeatureIterator($suite, $this->gherkin, $scenarios); + } + + /** + * Returns array of feature paths configured for the provided suite. + * + * @param Suite $suite + * + * @return string[] + * + * @throws SuiteConfigurationException If `paths` setting is not an array + */ + private function getSuitePaths(Suite $suite) { + if (!is_array($suite->getSetting('paths'))) { + throw new SuiteConfigurationException( + sprintf('`paths` setting of the "%s" suite is expected to be an array, %s given.', + $suite->getName(), + gettype($suite->getSetting('paths')) + ), + $suite->getName() + ); + } + + return $suite->getSetting('paths'); + } +} diff --git a/lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleListFormatter.php b/lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleListFormatter.php new file mode 100644 index 00000000000..351a120b7e1 --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleListFormatter.php @@ -0,0 +1,149 @@ +. + +/** + * Feature step counter for distributing features between parallel runs. + * + * Use it with --dry-run (and any other selectors combination) to + * get the results quickly. + * + * @copyright 2015 onwards Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace Moodle\BehatExtension\Output\Formatter; + +use Behat\Behat\EventDispatcher\Event\AfterFeatureTested; +use Behat\Behat\EventDispatcher\Event\AfterOutlineTested; +use Behat\Behat\EventDispatcher\Event\AfterScenarioTested; +use Behat\Behat\EventDispatcher\Event\AfterStepTested; +use Behat\Behat\EventDispatcher\Event\BeforeFeatureTested; +use Behat\Behat\EventDispatcher\Event\BeforeOutlineTested; +use Behat\Behat\EventDispatcher\Event\BeforeScenarioTested; +use Behat\Behat\Tester\Result\ExecutedStepResult; +use Behat\Testwork\Counter\Memory; +use Behat\Testwork\Counter\Timer; +use Behat\Testwork\EventDispatcher\Event\AfterExerciseCompleted; +use Behat\Testwork\EventDispatcher\Event\AfterSuiteTested; +use Behat\Testwork\EventDispatcher\Event\BeforeExerciseCompleted; +use Behat\Testwork\EventDispatcher\Event\BeforeSuiteTested; +use Behat\Testwork\Output\Exception\BadOutputPathException; +use Behat\Testwork\Output\Formatter; +use Behat\Testwork\Output\Printer\OutputPrinter; + +class MoodleListFormatter implements Formatter { + + /** + * @var OutputPrinter + */ + private $printer; + /** + * @var array + */ + private $parameters; + /** + * @var string + */ + private $name; + /** + * @var string + */ + private $description; + + /** + * Initializes formatter. + * + * @param string $name + * @param string $description + * @param array $parameters + * @param OutputPrinter $printer + */ + public function __construct($name, $description, array $parameters, OutputPrinter $printer) { + $this->name = $name; + $this->description = $description; + $this->parameters = $parameters; + $this->printer = $printer; + } + + /** + * Returns an array of event names this subscriber wants to listen to. + * @return array The event names to listen to + */ + public static function getSubscribedEvents() { + return array( + + 'tester.scenario_tested.after' => 'afterScenario', + 'tester.outline_tested.after' => 'afterOutlineExample', + ); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->description; + } + + /** + * {@inheritdoc} + */ + public function getOutputPrinter() { + return $this->printer; + } + + /** + * {@inheritdoc} + */ + public function setParameter($name, $value) { + $this->parameters[$name] = $value; + } + + /** + * {@inheritdoc} + */ + public function getParameter($name) { + return isset($this->parameters[$name]) ? $this->parameters[$name] : null; + } + + /** + * Listens to "scenario.after" event. + * + * @param ScenarioEvent $event + */ + public function afterScenario(AfterScenarioTested $event) { + $scenario = $event->getScenario(); + $this->printer->writeln($event->getFeature()->getFile() . ':' . $scenario->getLine()); + } + + + /** + * Listens to "outline.example.after" event. + * + * @param OutlineExampleEvent $event + */ + public function afterOutlineExample(AfterOutlineTested $event) { + $outline = $event->getOutline(); + $line = $outline->getLine(); + $this->printer->writeln($event->getFeature()->getFile() . ':' . $line); + } +} diff --git a/lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleProgressFormatterFactory.php b/lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleProgressFormatterFactory.php new file mode 100644 index 00000000000..d1c394808ea --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleProgressFormatterFactory.php @@ -0,0 +1,207 @@ +. + +/** + * Moodle behat context class resolver. + * + * @package behat + * @copyright 2016 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace Moodle\BehatExtension\Output\Formatter; + +use Behat\Testwork\Exception\ServiceContainer\ExceptionExtension; +use Behat\Testwork\Output\ServiceContainer\OutputExtension; +use Behat\Testwork\ServiceContainer\ServiceProcessor; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; +use Behat\Behat\Output\ServiceContainer\Formatter\ProgressFormatterFactory; +use Behat\Behat\EventDispatcher\Event\OutlineTested; +use Behat\Testwork\Output\ServiceContainer\Formatter\FormatterFactory; +use Behat\Testwork\Translator\ServiceContainer\TranslatorExtension; + +class MoodleProgressFormatterFactory implements FormatterFactory { + /** + * @var ServiceProcessor + */ + private $processor; + + /* + * Available services + */ + const ROOT_LISTENER_ID_MOODLE = 'output.node.listener.moodleprogress'; + const RESULT_TO_STRING_CONVERTER_ID_MOODLE = 'output.node.printer.result_to_string'; + + /* + * Available extension points + */ + const ROOT_LISTENER_WRAPPER_TAG_MOODLE = 'output.node.listener.moodleprogress.wrapper'; + + /** + * Initializes extension. + * + * @param null|ServiceProcessor $processor + */ + public function __construct(ServiceProcessor $processor = null) { + $this->processor = $processor ? : new ServiceProcessor(); + } + + /** + * {@inheritdoc} + */ + public function buildFormatter(ContainerBuilder $container) { + $this->loadRootNodeListener($container); + $this->loadCorePrinters($container); + $this->loadPrinterHelpers($container); + $this->loadFormatter($container); + } + + /** + * {@inheritdoc} + */ + public function processFormatter(ContainerBuilder $container) { + $this->processListenerWrappers($container); + } + + /** + * Loads progress formatter node event listener. + * + * @param ContainerBuilder $container + */ + protected function loadRootNodeListener(ContainerBuilder $container) { + $definition = new Definition('Behat\Behat\Output\Node\EventListener\AST\StepListener', array( + new Reference('output.node.printer.moodleprogress.step') + )); + $container->setDefinition(self::ROOT_LISTENER_ID_MOODLE, $definition); + } + + /** + * Loads formatter itself. + * + * @param ContainerBuilder $container + */ + protected function loadFormatter(ContainerBuilder $container) { + + $definition = new Definition('Behat\Behat\Output\Statistics\TotalStatistics'); + $container->setDefinition('output.moodleprogress.statistics', $definition); + + $moodleconfig = $container->getParameter('behat.moodle.parameters'); + + $definition = new Definition('Moodle\BehatExtension\Output\Printer\MoodleProgressPrinter', + array($moodleconfig['moodledirroot'])); + $container->setDefinition('moodle.output.node.printer.moodleprogress.printer', $definition); + + $definition = new Definition('Behat\Testwork\Output\NodeEventListeningFormatter', array( + 'moodle_progress', + 'Prints information about then run followed by one character per step.', + array( + 'timer' => true + ), + $this->createOutputPrinterDefinition(), + new Definition('Behat\Testwork\Output\Node\EventListener\ChainEventListener', array( + array( + new Reference(self::ROOT_LISTENER_ID_MOODLE), + new Definition('Behat\Behat\Output\Node\EventListener\Statistics\StatisticsListener', array( + new Reference('output.moodleprogress.statistics'), + new Reference('output.node.printer.moodleprogress.statistics') + )), + new Definition('Behat\Behat\Output\Node\EventListener\Statistics\ScenarioStatsListener', array( + new Reference('output.moodleprogress.statistics') + )), + new Definition('Behat\Behat\Output\Node\EventListener\Statistics\StepStatsListener', array( + new Reference('output.moodleprogress.statistics'), + new Reference(ExceptionExtension::PRESENTER_ID) + )), + new Definition('Behat\Behat\Output\Node\EventListener\Statistics\HookStatsListener', array( + new Reference('output.moodleprogress.statistics'), + new Reference(ExceptionExtension::PRESENTER_ID) + )), + new Definition('Behat\Behat\Output\Node\EventListener\AST\SuiteListener', array( + new Reference('moodle.output.node.printer.moodleprogress.printer') + )) + ) + ) + ) + )); + $definition->addTag(OutputExtension::FORMATTER_TAG, array('priority' => 1)); + $container->setDefinition(OutputExtension::FORMATTER_TAG . '.moodleprogress', $definition); + } + + /** + * Loads printer helpers. + * + * @param ContainerBuilder $container + */ + protected function loadPrinterHelpers(ContainerBuilder $container) { + $definition = new Definition('Behat\Behat\Output\Node\Printer\Helper\ResultToStringConverter'); + $container->setDefinition(self::RESULT_TO_STRING_CONVERTER_ID_MOODLE, $definition); + } + + /** + * Loads feature, scenario and step printers. + * + * @param ContainerBuilder $container + */ + protected function loadCorePrinters(ContainerBuilder $container) { + $definition = new Definition('Behat\Behat\Output\Node\Printer\CounterPrinter', array( + new Reference(self::RESULT_TO_STRING_CONVERTER_ID_MOODLE), + new Reference(TranslatorExtension::TRANSLATOR_ID), + )); + $container->setDefinition('output.node.moodle.printer.counter', $definition); + + $definition = new Definition('Behat\Behat\Output\Node\Printer\ListPrinter', array( + new Reference(self::RESULT_TO_STRING_CONVERTER_ID_MOODLE), + new Reference(ExceptionExtension::PRESENTER_ID), + new Reference(TranslatorExtension::TRANSLATOR_ID), + '%paths.base%' + )); + $container->setDefinition('output.node.moodle.printer.list', $definition); + + $definition = new Definition('Behat\Behat\Output\Node\Printer\Progress\ProgressStepPrinter', array( + new Reference(self::RESULT_TO_STRING_CONVERTER_ID_MOODLE) + )); + $container->setDefinition('output.node.printer.moodleprogress.step', $definition); + + $definition = new Definition('Behat\Behat\Output\Node\Printer\Progress\ProgressStatisticsPrinter', array( + new Reference('output.node.moodle.printer.counter'), + new Reference('output.node.moodle.printer.list') + )); + $container->setDefinition('output.node.printer.moodleprogress.statistics', $definition); + } + + /** + * Creates output printer definition. + * + * @return Definition + */ + protected function createOutputPrinterDefinition() { + return new Definition('Behat\Testwork\Output\Printer\StreamOutputPrinter', array( + new Definition('Behat\Behat\Output\Printer\ConsoleOutputFactory'), + )); + } + + /** + * Processes all registered pretty formatter node listener wrappers. + * + * @param ContainerBuilder $container + */ + protected function processListenerWrappers(ContainerBuilder $container) { + $this->processor->processWrapperServices($container, self::ROOT_LISTENER_ID_MOODLE, self::ROOT_LISTENER_WRAPPER_TAG_MOODLE); + } +} diff --git a/lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleScreenshotFormatter.php b/lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleScreenshotFormatter.php new file mode 100644 index 00000000000..a71e4d9eadf --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleScreenshotFormatter.php @@ -0,0 +1,299 @@ +. + +/** + * Feature step counter for distributing features between parallel runs. + * + * Use it with --dry-run (and any other selectors combination) to + * get the results quickly. + * + * @copyright 2016 onwards Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace Moodle\BehatExtension\Output\Formatter; + +use Behat\Behat\EventDispatcher\Event\AfterFeatureTested; +use Behat\Behat\EventDispatcher\Event\AfterOutlineTested; +use Behat\Behat\EventDispatcher\Event\AfterScenarioTested; +use Behat\Behat\EventDispatcher\Event\AfterStepTested; +use Behat\Behat\EventDispatcher\Event\BeforeFeatureTested; +use Behat\Behat\EventDispatcher\Event\BeforeOutlineTested; +use Behat\Behat\EventDispatcher\Event\BeforeScenarioTested; +use Behat\Behat\EventDispatcher\Event\BeforeStepTested; +use Behat\Behat\Tester\Result\ExecutedStepResult; +use Behat\Testwork\Counter\Memory; +use Behat\Testwork\Counter\Timer; +use Behat\Testwork\EventDispatcher\Event\AfterExerciseCompleted; +use Behat\Testwork\EventDispatcher\Event\AfterSuiteTested; +use Behat\Testwork\EventDispatcher\Event\BeforeExerciseCompleted; +use Behat\Testwork\EventDispatcher\Event\BeforeSuiteTested; +use Behat\Testwork\Output\Exception\BadOutputPathException; +use Behat\Testwork\Output\Formatter; +use Behat\Testwork\Output\Printer\OutputPrinter; + +class MoodleScreenshotFormatter implements Formatter { + + /** + * @var OutputPrinter + */ + private $printer; + /** + * @var array + */ + private $parameters; + /** + * @var string + */ + private $name; + /** + * @var string + */ + private $description; + + /** + * @var int The scenario count. + */ + protected static $currentscenariocount = 0; + + /** + * @var int The step count within the current scenario. + */ + protected static $currentscenariostepcount = 0; + + /** + * If we are saving any kind of dump on failure we should use the same parent dir during a run. + * + * @var The parent dir name + */ + protected static $faildumpdirname = false; + + /** + * Initializes formatter. + * + * @param string $name + * @param string $description + * @param array $parameters + * @param OutputPrinter $printer + * @param EventListener $listener + */ + public function __construct($name, $description, array $parameters, OutputPrinter $printer) { + $this->name = $name; + $this->description = $description; + $this->parameters = $parameters; + $this->printer = $printer; + } + + /** + * Returns an array of event names this subscriber wants to listen to. + * @return array The event names to listen to + */ + public static function getSubscribedEvents() { + return array( + + 'tester.scenario_tested.before' => 'beforeScenario', + 'tester.step_tested.before' => 'beforeStep', + 'tester.step_tested.after' => 'afterStep', + ); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->description; + } + + /** + * {@inheritdoc} + */ + public function getOutputPrinter() { + return $this->printer; + } + + /** + * {@inheritdoc} + */ + public function setParameter($name, $value) { + $this->parameters[$name] = $value; + } + + /** + * {@inheritdoc} + */ + public function getParameter($name) { + return isset($this->parameters[$name]) ? $this->parameters[$name] : null; + } + + /** + * Reset currentscenariostepcount + * + * @param BeforeScenarioTested $event + */ + public function beforeScenario(BeforeScenarioTested $event) { + + self::$currentscenariostepcount = 0; + self::$currentscenariocount++; + } + + /** + * Increment currentscenariostepcount + * + * @param BeforeStepTested $event + */ + public function beforeStep(BeforeStepTested $event) { + self::$currentscenariostepcount++; + } + + /** + * Take screenshot after step is executed. Behat\Behat\Event\html + * + * @param AfterStepTested $event + */ + public function afterStep(AfterStepTested $event) { + $behathookcontext = $event->getEnvironment()->getContext('behat_hooks'); + + $formats = $this->getParameter('formats'); + $formats = explode(',', $formats); + + // Take screenshot. + if (in_array('image', $formats)) { + $this->take_screenshot($event, $behathookcontext); + } + + // Save html content. + if (in_array('html', $formats)) { + $this->take_contentdump($event, $behathookcontext); + } + } + + /** + * Return screenshot directory where all screenshots will be saved. + * + * @return string + */ + protected function get_run_screenshot_dir() { + global $CFG; + + if (self::$faildumpdirname) { + return self::$faildumpdirname; + } + + // If output_path is set then use output_path else use faildump_path. + if ($this->getOutputPrinter()->getOutputPath()) { + $screenshotpath = $this->getOutputPrinter()->getOutputPath(); + } else if ($CFG->behat_faildump_path) { + $screenshotpath = $CFG->behat_faildump_path; + } else { + // It should never reach here. + throw new FormatterException('You should specify --out "SOME/PATH" for moodle_screenshot format'); + } + + if ($this->getParameter('dir_permissions')) { + $dirpermissions = $this->getParameter('dir_permissions'); + } else { + $dirpermissions = 0777; + } + + // All the screenshot dumps should be in the same parent dir. + self::$faildumpdirname = $screenshotpath . DIRECTORY_SEPARATOR . date('Ymd_His'); + + if (!is_dir(self::$faildumpdirname) && !mkdir(self::$faildumpdirname, $dirpermissions, true)) { + // It shouldn't, we already checked that the directory is writable. + throw new FormatterException(sprintf( + 'No directories can be created inside %s, check the directory permissions.', $screenshotpath)); + } + + return self::$faildumpdirname; + } + + /** + * Take screenshot when a step fails. + * + * @throws Exception + * @param AfterStepTested $event + */ + protected function take_screenshot(AfterStepTested $event, $context) { + // Goutte can't save screenshots. + if ($context->getMink()->isSessionStarted($context->getMink()->getDefaultSessionName())) { + if (get_class($context->getMink()->getSession()->getDriver()) === 'Behat\Mink\Driver\GoutteDriver') { + return false; + } + list ($dir, $filename) = $this->get_faildump_filename($event, 'png'); + $context->saveScreenshot($filename, $dir); + } + } + + /** + * Take a dump of the page content when a step fails. + * + * @throws Exception + * @param AfterStepTested $event + */ + protected function take_contentdump(AfterStepTested $event, $context) { + list ($dir, $filename) = $this->get_faildump_filename($event, 'html'); + $fh = fopen($dir . DIRECTORY_SEPARATOR . $filename, 'w'); + fwrite($fh, $context->getMink()->getSession()->getPage()->getContent()); + fclose($fh); + } + + /** + * Determine the full pathname to store a failure-related dump. + * + * This is used for content such as the DOM, and screenshots. + * + * @param AfterStepTested $event + * @param String $filetype The file suffix to use. Limited to 4 chars. + */ + protected function get_faildump_filename(AfterStepTested $event, $filetype) { + // Make a directory for the scenario. + $featurename = $event->getFeature()->getTitle(); + $featurename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $featurename); + if ($this->getParameter('dir_permissions')) { + $dirpermissions = $this->getParameter('dir_permissions'); + } else { + $dirpermissions = 0777; + } + + $dir = $this->get_run_screenshot_dir(); + + // We want a i-am-the-scenario-title format. + $dir = $dir . DIRECTORY_SEPARATOR . self::$currentscenariocount . '-' . $featurename; + if (!is_dir($dir) && !mkdir($dir, $dirpermissions, true)) { + // We already checked that the directory is writable. This should not fail. + throw new FormatterException(sprintf( + 'No directories can be created inside %s, check the directory permissions.', $dir)); + } + + // The failed step text. + // We want a stepno-i-am-the-failed-step.$filetype format. + $filename = $event->getStep()->getText(); + $filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename); + $filename = self::$currentscenariostepcount . '-' . $filename; + + // File name limited to 255 characters. Leaving 4 chars for the file + // extension as we allow .png for images and .html for DOM contents. + $filename = substr($filename, 0, 250) . '.' . $filetype; + return array($dir, $filename); + } +} diff --git a/lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleStepcountFormatter.php b/lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleStepcountFormatter.php new file mode 100644 index 00000000000..21a94f2e82c --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Output/Formatter/MoodleStepcountFormatter.php @@ -0,0 +1,159 @@ +. + +/** + * Feature step counter for distributing features between parallel runs. + * + * Use it with --dry-run (and any other selectors combination) to + * get the results quickly. + * + * @copyright 2016 onwards Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace Moodle\BehatExtension\Output\Formatter; + +use Behat\Behat\EventDispatcher\Event\AfterFeatureTested; +use Behat\Behat\EventDispatcher\Event\AfterOutlineTested; +use Behat\Behat\EventDispatcher\Event\AfterScenarioTested; +use Behat\Behat\EventDispatcher\Event\AfterStepTested; +use Behat\Behat\EventDispatcher\Event\BeforeFeatureTested; +use Behat\Behat\EventDispatcher\Event\BeforeOutlineTested; +use Behat\Behat\EventDispatcher\Event\BeforeScenarioTested; +use Behat\Behat\Tester\Result\ExecutedStepResult; +use Behat\Testwork\Counter\Memory; +use Behat\Testwork\Counter\Timer; +use Behat\Testwork\EventDispatcher\Event\AfterExerciseCompleted; +use Behat\Testwork\EventDispatcher\Event\AfterSuiteTested; +use Behat\Testwork\EventDispatcher\Event\BeforeExerciseCompleted; +use Behat\Testwork\EventDispatcher\Event\BeforeSuiteTested; +use Behat\Testwork\Output\Exception\BadOutputPathException; +use Behat\Testwork\Output\Formatter; +use Behat\Testwork\Output\Printer\OutputPrinter; + +class MoodleStepcountFormatter implements Formatter { + + /** @var int Number of steps executed in feature file. */ + private static $stepcount = 0; + + /** + * @var OutputPrinter + */ + private $printer; + /** + * @var array + */ + private $parameters; + /** + * @var string + */ + private $name; + /** + * @var string + */ + private $description; + + /** + * Initializes formatter. + * + * @param string $name + * @param string $description + * @param array $parameters + * @param OutputPrinter $printer + * @param EventListener $listener + */ + public function __construct($name, $description, array $parameters, OutputPrinter $printer) { + $this->name = $name; + $this->description = $description; + $this->parameters = $parameters; + $this->printer = $printer; + } + + /** + * Returns an array of event names this subscriber wants to listen to. + * @return array The event names to listen to + */ + public static function getSubscribedEvents() { + return array( + + 'tester.feature_tested.before' => 'beforeFeature', + 'tester.feature_tested.after' => 'afterFeature', + 'tester.step_tested.after' => 'afterStep', + ); + } + + /** + * {@inheritdoc} + */ + public function getName() { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->description; + } + + /** + * {@inheritdoc} + */ + public function getOutputPrinter() { + return $this->printer; + } + + /** + * {@inheritdoc} + */ + public function setParameter($name, $value) { + $this->parameters[$name] = $value; + } + + /** + * {@inheritdoc} + */ + public function getParameter($name) { + return isset($this->parameters[$name]) ? $this->parameters[$name] : null; + } + + /** + * Listens to "feature.before" event. + * + * @param FeatureEvent $event + */ + public function beforeFeature(BeforeFeatureTested $event) { + self::$stepcount = 0; + } + + /** + * Listens to "feature.after" event. + * + * @param FeatureEvent $event + */ + public function afterFeature(AfterFeatureTested $event) { + $this->printer->writeln($event->getFeature()->getFile() . '::' . self::$stepcount); + } + + /** + * Listens to "step.after" event. + * + * @param StepEvent $event + */ + public function afterStep(AfterStepTested $event) { + self::$stepcount++; + } +} diff --git a/lib/behat/extension/Moodle/BehatExtension/Output/Printer/MoodleProgressPrinter.php b/lib/behat/extension/Moodle/BehatExtension/Output/Printer/MoodleProgressPrinter.php new file mode 100644 index 00000000000..5118307dff9 --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Output/Printer/MoodleProgressPrinter.php @@ -0,0 +1,127 @@ +. + +/** + * Moodle behat context class resolver. + * + * @package behat + * @copyright 2016 Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace Moodle\BehatExtension\Output\Printer; + +use Behat\Behat\Output\Node\Printer\SetupPrinter; +use Behat\Testwork\Output\Formatter; +use Behat\Testwork\Tester\Setup\Setup; +use Behat\Testwork\Tester\Setup\Teardown; +use Behat\Testwork\Call\CallResult; +use Behat\Testwork\Hook\Tester\Setup\HookedTeardown; +use Behat\Testwork\Output\Printer\OutputPrinter; +use Behat\Testwork\Tester\Result\TestResult; +use Moodle\BehatExtension\Driver\WebDriver; + +/** + * Prints hooks in a pretty fashion. + */ +final class MoodleProgressPrinter implements SetupPrinter { + + /** + * @var string Moodle directory root. + */ + private $moodledirroot; + + /** + * @var bool true if output is displayed. + */ + private static $outputdisplayed; + + /** + * Constructor. + * + * @param string $moodledirroot Moodle dir root. + */ + public function __construct($moodledirroot) { + $this->moodledirroot = $moodledirroot; + } + + /** + * {@inheritdoc} + */ + public function printSetup(Formatter $formatter, Setup $setup) { + if (empty(self::$outputdisplayed)) { + $this->printMoodleInfo($formatter->getOutputPrinter()); + self::$outputdisplayed = true; + } + } + + /** + * {@inheritdoc} + */ + public function printTeardown(Formatter $formatter, Teardown $teardown) { + if (!$teardown instanceof HookedTeardown) { + return; + } + + foreach ($teardown->getHookCallResults() as $callResult) { + $this->printTeardownHookCallResult($formatter->getOutputPrinter(), $callResult); + } + } + + /** + * We print the site info + driver used and OS. + * + * @param Printer $printer + * @return void + */ + public function printMoodleInfo($printer) { + require_once($this->moodledirroot . '/lib/behat/classes/util.php'); + + $browser = WebDriver::getBrowserName(); + + // Calling all directly from here as we avoid more behat framework extensions. + $runinfo = \behat_util::get_site_info(); + $runinfo .= 'Server OS "' . PHP_OS . '"' . ', Browser: "' . $browser . '"' . PHP_EOL; + $runinfo .= 'Started at ' . date('d-m-Y, H:i', time()); + + $printer->writeln($runinfo); + } + + /** + * Prints teardown hook call result. + * + * @param OutputPrinter $printer + * @param CallResult $callResult + */ + private function printTeardownHookCallResult(OutputPrinter $printer, CallResult $callResult) { + // Notify dev that chained step is being used. + if (\Moodle\BehatExtension\EventDispatcher\Tester\ChainedStepTester::is_chained_step_used()) { + $printer->writeln(); + $printer->write("{+failed}Chained steps are deprecated. See https://docs.moodle.org/dev/Acceptance_testing/Migrating_from_Behat_2.5_to_3.x_in_Moodle#Changes_required_in_context_file{-failed}"); + } + + if (!$callResult->hasStdOut() && !$callResult->hasException()) { + return; + } + + $hook = $callResult->getCall()->getCallee(); + $path = $hook->getPath(); + + $printer->writeln($hook); + $printer->writeln($path); + } +} diff --git a/lib/behat/extension/Moodle/BehatExtension/ServiceContainer/BehatExtension.php b/lib/behat/extension/Moodle/BehatExtension/ServiceContainer/BehatExtension.php new file mode 100644 index 00000000000..44009fca01e --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/ServiceContainer/BehatExtension.php @@ -0,0 +1,302 @@ +processor = $processor ? : new ServiceProcessor(); + } + + /** + * Loads moodle specific configuration. + * + * @param array $config Extension configuration hash (from behat.yml) + * @param ContainerBuilder $container ContainerBuilder instance + */ + public function load(ContainerBuilder $container, array $config) { + $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/services')); + $loader->load('core.xml'); + + // Getting the extension parameters. + $container->setParameter('behat.moodle.parameters', $config); + + // Load moodle progress formatter. + $moodleprogressformatter = new MoodleProgressFormatterFactory(); + $moodleprogressformatter->buildFormatter($container); + + // Load custom step tester event dispatcher. + $this->loadEventDispatchingStepTester($container); + + // Load chained step tester. + $this->loadChainedStepTester($container); + + // Load step count formatter. + $this->loadMoodleListFormatter($container); + + // Load step count formatter. + $this->loadMoodleStepcountFormatter($container); + + // Load screenshot formatter. + $this->loadMoodleScreenshotFormatter($container); + + // Load namespace alias. + $this->alias_old_namespaces(); + + // Load skip passed controller and list locator. + $this->loadSkipPassedController($container, $config['passed_cache']); + $this->loadFilesystemSkipPassedScenariosListLocator($container); + } + + /** + * Loads moodle List formatter. + * + * @param ContainerBuilder $container + */ + protected function loadMoodleListFormatter(ContainerBuilder $container) { + $definition = new Definition('Moodle\BehatExtension\Output\Formatter\MoodleListFormatter', array( + 'moodle_list', + 'List all scenarios. Use with --dry-run', + array('stepcount' => false), + $this->createOutputPrinterDefinition() + )); + $definition->addTag(OutputExtension::FORMATTER_TAG, array('priority' => 101)); + $container->setDefinition(OutputExtension::FORMATTER_TAG . '.moodle_list', $definition); + } + + /** + * Loads moodle Step count formatter. + * + * @param ContainerBuilder $container + */ + protected function loadMoodleStepcountFormatter(ContainerBuilder $container) { + $definition = new Definition('Moodle\BehatExtension\Output\Formatter\MoodleStepcountFormatter', array( + 'moodle_stepcount', + 'Count steps in feature files. Use with --dry-run', + array('stepcount' => false), + $this->createOutputPrinterDefinition() + )); + $definition->addTag(OutputExtension::FORMATTER_TAG, array('priority' => 101)); + $container->setDefinition(OutputExtension::FORMATTER_TAG . '.moodle_stepcount', $definition); + } + + /** + * Loads moodle screenshot formatter. + * + * @param ContainerBuilder $container + */ + protected function loadMoodleScreenshotFormatter(ContainerBuilder $container) { + $definition = new Definition('Moodle\BehatExtension\Output\Formatter\MoodleScreenshotFormatter', array( + 'moodle_screenshot', + 'Take screenshot of all steps. Use --format-settings \'{"formats": "html,image"}\' to get specific o/p type', + array('formats' => 'html,image'), + $this->createOutputPrinterDefinition() + )); + $definition->addTag(OutputExtension::FORMATTER_TAG, array('priority' => 102)); + $container->setDefinition(OutputExtension::FORMATTER_TAG . '.moodle_screenshot', $definition); + } + + /** + * Creates output printer definition. + * + * @return Definition + */ + protected function createOutputPrinterDefinition() { + return new Definition('Behat\Testwork\Output\Printer\StreamOutputPrinter', array( + new Definition('Behat\Behat\Output\Printer\ConsoleOutputFactory'), + )); + } + + /** + * Loads skip passed controller. + * + * @param ContainerBuilder $container + * @param null|string $cachePath + */ + protected function loadSkipPassedController(ContainerBuilder $container, $cachePath) { + $definition = new Definition('Moodle\BehatExtension\Tester\Cli\SkipPassedController', array( + new Reference(EventDispatcherExtension::DISPATCHER_ID), + $cachePath, + $container->getParameter('paths.base') + )); + $definition->addTag(CliExtension::CONTROLLER_TAG, array('priority' => 200)); + $container->setDefinition(CliExtension::CONTROLLER_TAG . '.passed', $definition); + } + + /** + * Loads filesystem passed scenarios list locator. + * + * @param ContainerBuilder $container + */ + private function loadFilesystemSkipPassedScenariosListLocator(ContainerBuilder $container) { + $definition = new Definition('Moodle\BehatExtension\Locator\FilesystemSkipPassedListLocator', array( + new Reference(self::GHERKIN_ID) + )); + $definition->addTag(SpecificationExtension::LOCATOR_TAG, array('priority' => 50)); + $container->setDefinition(SpecificationExtension::LOCATOR_TAG . '.filesystem_skip_passed_scenarios_list', $definition); + } + + /** + * Loads definition printers. + * + * @param ContainerBuilder $container + */ + private function loadDefinitionPrinters(ContainerBuilder $container) { + $definition = new Definition('Moodle\BehatExtension\Definition\Printer\ConsoleDefinitionInformationPrinter', array( + new Reference(CliExtension::OUTPUT_ID), + new Reference(DefinitionExtension::PATTERN_TRANSFORMER_ID), + new Reference(DefinitionExtension::DEFINITION_TRANSLATOR_ID), + new Reference(GherkinExtension::KEYWORDS_ID) + )); + $container->removeDefinition('definition.information_printer'); + $container->setDefinition('definition.information_printer', $definition); + + } + + /** + * Loads definition controller. + * + * @param ContainerBuilder $container + */ + private function loadController(ContainerBuilder $container) { + $definition = new Definition('Moodle\BehatExtension\Definition\Cli\AvailableDefinitionsController', array( + new Reference(SuiteExtension::REGISTRY_ID), + new Reference(DefinitionExtension::WRITER_ID), + new Reference('definition.list_printer'), + new Reference('definition.information_printer')) + ); + $container->removeDefinition(CliExtension::CONTROLLER_TAG . '.available_definitions'); + $container->setDefinition(CliExtension::CONTROLLER_TAG . '.available_definitions', $definition); + } + + /** + * Loads chained step tester. + * + * @param ContainerBuilder $container + */ + protected function loadChainedStepTester(ContainerBuilder $container) { + // Chained steps. + $definition = new Definition('Moodle\BehatExtension\EventDispatcher\Tester\ChainedStepTester', array( + new Reference(TesterExtension::STEP_TESTER_ID), + )); + $definition->addTag(TesterExtension::STEP_TESTER_WRAPPER_TAG, array('priority' => 100)); + $container->setDefinition(TesterExtension::STEP_TESTER_WRAPPER_TAG . '.substep', $definition); + } + + /** + * Loads event-dispatching step tester. + * + * @param ContainerBuilder $container + */ + protected function loadEventDispatchingStepTester(ContainerBuilder $container) { + $definition = new Definition('Moodle\BehatExtension\EventDispatcher\Tester\MoodleEventDispatchingStepTester', array( + new Reference(TesterExtension::STEP_TESTER_ID), + new Reference(EventDispatcherExtension::DISPATCHER_ID) + )); + $definition->addTag(TesterExtension::STEP_TESTER_WRAPPER_TAG, array('priority' => -9999)); + $container->setDefinition(TesterExtension::STEP_TESTER_WRAPPER_TAG . '.event_dispatching', $definition); + } + + /** + * Setups configuration for current extension. + * + * @param ArrayNodeDefinition $builder + */ + public function configure(ArrayNodeDefinition $builder) { + $builder-> + children()-> + arrayNode('capabilities')-> + useAttributeAsKey('key')-> + prototype('variable')->end()-> + end()-> + arrayNode('steps_definitions')-> + useAttributeAsKey('key')-> + prototype('variable')->end()-> + end()-> + scalarNode('moodledirroot')-> + defaultNull()-> + end()-> + scalarNode('passed_cache')-> + info('Sets the passed cache path')-> + defaultValue( + is_writable(sys_get_temp_dir()) + ? sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'behat_passed_cache' + : null)-> + end()-> + end()-> + end(); + } + + /** + * {@inheritDoc} + */ + public function getConfigKey() { + return self::MOODLE_ID; + } + + /** + * {@inheritdoc} + */ + public function initialize(ExtensionManager $extensionManager) { + if (null !== $minkExtension = $extensionManager->getExtension('mink')) { + $minkExtension->registerDriverFactory(new WebDriverFactory()); + } + } + + public function process(ContainerBuilder $container) { + // Load controller for definition printing. + $this->loadDefinitionPrinters($container); + $this->loadController($container); + } + + /** + * Alias old namespace of given. when and then for BC. + */ + private function alias_old_namespaces() { + class_alias('Moodle\\BehatExtension\\Context\\Step\\Given', 'Behat\\Behat\\Context\\Step\\Given', true); + class_alias('Moodle\\BehatExtension\\Context\\Step\\When', 'Behat\\Behat\\Context\\Step\\When', true); + class_alias('Moodle\\BehatExtension\\Context\\Step\\Then', 'Behat\\Behat\\Context\\Step\\Then', true); + } +} diff --git a/lib/behat/extension/Moodle/BehatExtension/ServiceContainer/services/core.xml b/lib/behat/extension/Moodle/BehatExtension/ServiceContainer/services/core.xml new file mode 100644 index 00000000000..47cda997b24 --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/ServiceContainer/services/core.xml @@ -0,0 +1,26 @@ + + + + + + Moodle\BehatExtension\Context\Initializer\MoodleAwareInitializer + Moodle\BehatExtension\Context\ContextClass\ClassResolver + Behat\Mink\Selector\SelectorsHandler + + + + + + %behat.moodle.parameters% + + + + + + %behat.moodle.parameters% + + + + diff --git a/lib/behat/extension/Moodle/BehatExtension/Tester/Cli/SkipPassedController.php b/lib/behat/extension/Moodle/BehatExtension/Tester/Cli/SkipPassedController.php new file mode 100644 index 00000000000..e2b57bbe8ef --- /dev/null +++ b/lib/behat/extension/Moodle/BehatExtension/Tester/Cli/SkipPassedController.php @@ -0,0 +1,199 @@ +. + +/** + * Caches passed scenarios and skip only them if `--skip-passed` option provided. + * + * @copyright 2016 onwards Rajesh Taneja + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace Moodle\BehatExtension\Tester\Cli; + +use Behat\Behat\EventDispatcher\Event\AfterFeatureTested; +use Behat\Behat\EventDispatcher\Event\AfterScenarioTested; +use Behat\Behat\EventDispatcher\Event\ExampleTested; +use Behat\Behat\EventDispatcher\Event\FeatureTested; +use Behat\Behat\EventDispatcher\Event\ScenarioTested; +use Behat\Testwork\Cli\Controller; +use Behat\Testwork\EventDispatcher\Event\ExerciseCompleted; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Behat\Testwork\Tester\Result\TestResult; + +/** + * Caches passed scenarios and skip only them if `--skip-passed` option provided. + * + * @copyright 2016 onwards Rajesh Taneja + */ +final class SkipPassedController implements Controller { + /** + * @var EventDispatcherInterface + */ + private $eventDispatcher; + + /** + * @var null|string + */ + private $cachePath; + + /** + * @var string + */ + private $key; + + /** + * @var string[] + */ + private $lines = array(); + + /** + * @var string + */ + private $basepath; + + /** + * Initializes controller. + * + * @param EventDispatcherInterface $eventDispatcher + * @param null|string $cachePath + * @param string $basepath + */ + public function __construct(EventDispatcherInterface $eventDispatcher, $cachePath, $basepath) { + $this->eventDispatcher = $eventDispatcher; + $this->cachePath = null !== $cachePath ? rtrim($cachePath, DIRECTORY_SEPARATOR) : null; + $this->basepath = $basepath; + } + + /** + * Configures command to be executable by the controller. + * + * @param Command $command + */ + public function configure(Command $command) { + $command->addOption('--skip-passed', null, InputOption::VALUE_NONE, + 'Skip scenarios that passed during last execution.' + ); + } + + /** + * Executes controller. + * + * @param InputInterface $input + * @param OutputInterface $output + * + * @return null|integer + */ + public function execute(InputInterface $input, OutputInterface $output) { + if (!$input->getOption('skip-passed')) { + // If no skip option is passed then remove any old file which we are saving. + if (!$this->getFileName()) { + return; + } + if (file_exists($this->getFileName())) { + unlink($this->getFileName()); + } + return; + } + + $this->eventDispatcher->addListener(ScenarioTested::AFTER, array($this, 'collectPassedScenario'), -50); + $this->eventDispatcher->addListener(ExampleTested::AFTER, array($this, 'collectPassedScenario'), -50); + $this->eventDispatcher->addListener(ExerciseCompleted::AFTER, array($this, 'writeCache'), -50); + $this->key = $this->generateKey($input); + + if (!$this->getFileName() || !file_exists($this->getFileName())) { + return; + } + $input->setArgument('paths', $this->getFileName()); + + $existing = json_decode(file_get_contents($this->getFileName()), true); + if (!empty($existing)) { + $this->lines = array_merge_recursive($existing, $this->lines); + } + } + + /** + * Records scenario if it is passed. + * + * @param AfterScenarioTested $event + */ + public function collectPassedScenario(AfterScenarioTested $event) { + if (!$this->getFileName()) { + return; + } + + $feature = $event->getFeature(); + $suitename = $event->getSuite()->getName(); + + if (($event->getTestResult()->getResultCode() !== TestResult::PASSED) && + ($event->getTestResult()->getResultCode() !== TestResult::SKIPPED)) { + unset($this->lines[$suitename][$feature->getFile()]); + return; + } + + $this->lines[$suitename][$feature->getFile()] = $feature->getFile(); + } + + /** + * Writes passed scenarios cache. + */ + public function writeCache() { + if (!$this->getFileName()) { + return; + } + if (0 === count($this->lines)) { + return; + } + file_put_contents($this->getFileName(), json_encode($this->lines)); + } + + /** + * Generates cache key. + * + * @param InputInterface $input + * + * @return string + */ + private function generateKey(InputInterface $input) { + return md5( + $input->getParameterOption(array('--profile', '-p')) . + $input->getOption('suite') . + implode(' ', $input->getOption('name')) . + implode(' ', $input->getOption('tags')) . + $input->getOption('role') . + $input->getArgument('paths') . + $this->basepath + ); + } + + /** + * Returns cache filename (if exists). + * + * @return null|string + */ + private function getFileName() { + if (null === $this->cachePath || null === $this->key) { + return null; + } + if (!is_dir($this->cachePath)) { + mkdir($this->cachePath, 0777); + } + return $this->cachePath . DIRECTORY_SEPARATOR . $this->key . '.passed'; + } +} diff --git a/lib/behat/extension/readme_moodle.txt b/lib/behat/extension/readme_moodle.txt new file mode 100644 index 00000000000..22c7aff2b53 --- /dev/null +++ b/lib/behat/extension/readme_moodle.txt @@ -0,0 +1,5 @@ +This directory is a copy of original Moodle behat extension +located at https://github.com/moodlehq/moodle-behat-extension + +The reason to move this code to Moodle core was to simplify +maintenance of behat integration. \ No newline at end of file diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index 7f9d7d3a24b..4426489bff8 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -14,6 +14,13 @@ 3.5.5 2.0 + + behat/extension + Moodle behat extension + GPL + 3.400.5 + 3.0+ + bennu Bennu