2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
21 * @copyright 2012 David Monllaó
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 require_once($CFG->libdir . '/filestorage/file_exceptions.php');
27 require_once($CFG->libdir . '/phpunit/bootstraplib.php');
28 require_once($CFG->libdir . '/phpunit/classes/tests_finder.php');
30 require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/behat/steps_definitions_form.php');
33 * Behat commands manager
36 * @copyright 2012 David Monllaó
37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41 /** @var array Steps types */
42 private static $steps_types = array('given', 'when', 'then');
44 /** @var string Docu url */
45 public static $docsurl = 'http://docs.moodle.org/dev/Acceptance_testing';
48 * Lists the available steps definitions
51 * @param string $component
52 * @param string $filter
55 public static function stepsdefinitions($type, $component, $filter) {
58 self::check_behat_setup();
60 // The loaded steps depends on the component specified.
61 self::update_config_file($component, false);
63 // The Moodle\BehatExtension\HelpPrinter\MoodleDefinitionsPrinter will parse this search format.
65 $filter .= '&&' . $type;
69 $filteroption = ' -d "' . $filter . '"';
71 $filteroption = ' -di';
74 $currentcwd = getcwd();
76 exec(self::get_behat_command() . ' --config="'.self::get_steps_list_config_filepath(). '" '.$filteroption, $steps, $code);
80 $stepshtml = implode('', $steps);
83 if (!isset($stepshtml) || $stepshtml == '') {
84 $stepshtml = get_string('nostepsdefinitions', 'tool_behat');
91 * Allows / disables the test environment to be accessed through the built-in server
93 * Built-in server must be started separately
95 * @param string $testenvironment enable|disable
97 public static function switchenvironment($testenvironment) {
98 if ($testenvironment == 'enable') {
99 self::start_test_mode();
100 } else if ($testenvironment == 'disable') {
101 self::stop_test_mode();
106 * Updates a config file
108 * The tests runner and the steps definitions list uses different
109 * config files to avoid problems with concurrent executions.
111 * The steps definitions list can be filtered by component so it's
112 * behat.yml can be different from the dirroot one.
114 * @param string $component Restricts the obtained steps definitions to the specified component
115 * @param string $testsrunner If the config file will be used to run tests
116 * @throws file_exception
118 protected static function update_config_file($component = '', $testsrunner = true) {
121 // Behat must run with the whole set of features and steps definitions.
122 if ($testsrunner === true) {
124 $configfilepath = $CFG->dirroot . '/behat.yml';
126 // Alternative for steps definitions filtering.
128 $configfilepath = self::get_steps_list_config_filepath();
129 $prefix = $CFG->dirroot .'/';
132 // Gets all the components with features.
134 $components = tests_finder::get_components_with_tests('features');
136 foreach ($components as $componentname => $path) {
137 $path = self::clean_path($path) . self::get_behat_tests_path();
138 if (empty($featurespaths[$path]) && file_exists($path)) {
139 $uniquekey = str_replace('\\', '/', $path);
140 $featurespaths[$uniquekey] = $path;
143 $features = array_values($featurespaths);
146 // Gets all the components with steps definitions.
147 $stepsdefinitions = array();
148 $steps = self::get_components_steps_definitions();
150 foreach ($steps as $key => $filepath) {
151 if ($component == '' || $component === $key) {
152 $stepsdefinitions[$key] = $filepath;
157 // Behat config file specifing the main context class,
158 // the required Behat extensions and Moodle test wwwroot.
159 $contents = self::get_config_file_contents($prefix, $features, $stepsdefinitions);
162 if (!file_put_contents($configfilepath, $contents)) {
163 throw new file_exception('cannotcreatefile', $configfilepath);
169 * Behat config file specifing the main context class,
170 * the required Behat extensions and Moodle test wwwroot.
172 * @param string $prefix The filesystem prefix
173 * @param array $features The system feature files
174 * @param array $stepsdefinitions The system steps definitions
177 protected static function get_config_file_contents($prefix, $features, $stepsdefinitions) {
180 // We require here when we are sure behat dependencies are available.
181 require_once($CFG->dirroot . '/vendor/autoload.php');
186 'features' => $prefix . 'lib/behat/features',
187 'bootstrap' => $prefix . 'lib/behat/features/bootstrap',
190 'class' => 'behat_init_context'
192 'extensions' => array(
193 'Behat\MinkExtension\Extension' => array(
194 'base_url' => $CFG->behat_wwwroot,
198 'Moodle\BehatExtension\Extension' => array(
199 'features' => $features,
200 'steps_definitions' => $stepsdefinitions
206 // In case user defined overrides respect them over our default ones.
207 if (!empty($CFG->behat_config)) {
208 $config = self::merge_config($config, $CFG->behat_config);
211 return Symfony\Component\Yaml\Yaml::dump($config, 10, 2);
215 * Overrides default config with local config values
217 * array_merge does not merge completely the array's values
219 * @param mixed $config The node of the default config
220 * @param mixed $localconfig The node of the local config
221 * @return mixed The merge result
223 protected static function merge_config($config, $localconfig) {
225 if (!is_array($config) && !is_array($localconfig)) {
229 // Local overrides also deeper default values.
230 if (is_array($config) && !is_array($localconfig)) {
234 foreach ($localconfig as $key => $value) {
236 // If defaults are not as deep as local values let locals override.
237 if (!is_array($config)) {
241 // Add the param if it doesn't exists or merge branches.
242 if (empty($config[$key])) {
243 $config[$key] = $value;
245 $config[$key] = self::merge_config($config[$key], $localconfig[$key]);
253 * Gets the list of Moodle steps definitions
255 * Class name as a key and the filepath as value
257 * Externalized from update_config_file() to use
258 * it from the steps definitions web interface
262 public static function get_components_steps_definitions() {
264 $components = tests_finder::get_components_with_tests('stepsdefinitions');
269 $stepsdefinitions = array();
270 foreach ($components as $componentname => $componentpath) {
271 $componentpath = self::clean_path($componentpath);
272 $diriterator = new DirectoryIterator($componentpath . self::get_behat_tests_path());
273 $regite = new RegexIterator($diriterator, '|behat_.*\.php$|');
275 // All behat_*.php inside self::get_behat_tests_path() are added as steps definitions files.
276 foreach ($regite as $file) {
277 $key = $file->getBasename('.php');
278 $stepsdefinitions[$key] = $file->getPathname();
282 return $stepsdefinitions;
286 * Checks if $CFG->behat_wwwroot is available
290 public static function is_server_running() {
293 $request = new curl();
294 $request->get($CFG->behat_wwwroot);
295 return (true && !$request->get_errno());
299 * Cleans the path returned by get_components_with_tests() to standarize it
301 * {@see tests_finder::get_all_directories_with_tests()} it returns the path including /tests/
302 * @param string $path
303 * @return string The string without the last /tests part
305 protected static function clean_path($path) {
307 $path = rtrim($path, DIRECTORY_SEPARATOR);
309 $parttoremove = DIRECTORY_SEPARATOR . 'tests';
311 $substr = substr($path, strlen($path) - strlen($parttoremove));
312 if ($substr == $parttoremove) {
313 $path = substr($path, 0, strlen($path) - strlen($parttoremove));
316 return rtrim($path, DIRECTORY_SEPARATOR);
320 * Checks whether the test database and dataroot is ready
321 * Stops execution if something went wrong
323 protected static function test_environment_problem() {
326 // PHPUnit --diag returns nothing if the test environment is set up correctly.
327 exec('php ' . $CFG->dirroot . '/' . $CFG->admin . '/tool/phpunit/cli/util.php --diag', $output, $code);
329 // If something is not ready stop execution and display the CLI command output.
331 notice(get_string('phpunitenvproblem', 'tool_behat') . ': ' . implode(' ', $output));
336 * Checks if behat is set up and working
338 * It checks behat dependencies have been installed and runs
339 * the behat help command to ensure it works as expected
340 * @param boolean $checkphp Extra check for the PHP version
342 protected static function check_behat_setup($checkphp = false) {
345 // We don't check the PHP version if $CFG->behat_switchcompletely has been enabled.
346 if (empty($CFG->behat_switchcompletely) && $checkphp && version_compare(PHP_VERSION, '5.4.0', '<')) {
347 throw new Exception(get_string('wrongphpversion', 'tool_behat'));
351 if (!self::are_behat_dependencies_installed()) {
353 $msg = get_string('wrongbehatsetup', 'tool_behat');
356 $docslink = self::$docsurl . '#Installation';
358 $docslink = html_writer::tag('a', $docslink, array('href' => $docslink, 'target' => '_blank'));
360 $msg .= '. ' . get_string('moreinfoin', 'tool_behat') . ' ' . $docslink;
364 // Behat test command.
365 $currentcwd = getcwd();
366 chdir($CFG->dirroot);
367 exec(self::get_behat_command() . ' --help', $output, $code);
371 notice(get_string('wrongbehatsetup', 'tool_behat'));
378 * Starts the test mode checking the composer installation and
379 * the phpunit test environment and updating the available
380 * features and steps definitions.
382 * Stores a file in dataroot/behat to allow Moodle to switch
383 * to the test environment when using cli-server (or $CFG->behat_switchcompletely)
385 * @throws file_exception
387 protected static function start_test_mode() {
390 // Checks the behat set up and the PHP version.
391 self::check_behat_setup(true);
393 // Check that PHPUnit test environment is correctly set up.
394 self::test_environment_problem();
396 // Updates all the Moodle features and steps definitions.
397 self::update_config_file();
399 if (self::is_test_mode_enabled()) {
400 debugging('Test environment was already enabled');
404 $behatdir = self::get_behat_dir();
406 $contents = '$CFG->behat_wwwroot, $CFG->phpunit_prefix and $CFG->phpunit_dataroot' .
407 ' are currently used as $CFG->wwwroot, $CFG->prefix and $CFG->dataroot';
408 $filepath = $behatdir . '/test_environment_enabled.txt';
409 if (!file_put_contents($filepath, $contents)) {
410 throw new file_exception('cannotcreatefile', $filepath);
412 chmod($filepath, $CFG->directorypermissions);
417 * @throws file_exception
419 protected static function stop_test_mode() {
421 $testenvfile = self::get_test_filepath();
423 if (!self::is_test_mode_enabled()) {
424 debugging('Test environment was already disabled');
426 if (!unlink($testenvfile)) {
427 throw new file_exception('cannotdeletetestenvironmentfile');
433 * Checks whether test environment is enabled or disabled
435 * To check is the current script is running in the test
436 * environment {@see tool_behat::is_test_environment_running()}
440 public static function is_test_mode_enabled() {
442 $testenvfile = self::get_test_filepath();
443 if (file_exists($testenvfile)) {
451 * Returns true if Moodle is currently running with the test database and dataroot
454 public static function is_test_environment_running() {
457 if (!empty($CFG->originaldataroot)) {
465 * Has the site installed composer with --dev option
468 public static function are_behat_dependencies_installed() {
469 if (!is_dir(__DIR__ . '/../../../vendor/behat')) {
476 * Returns the path to the file which specifies if test environment is enabled
478 * The file is in dataroot/behat/ but we need to
479 * know if test mode is running because then we swap
480 * it to phpunit_dataroot and we need the original value
484 protected static function get_test_filepath() {
487 if (self::is_test_environment_running()) {
488 $prefix = $CFG->originaldataroot;
490 $prefix = $CFG->dataroot;
493 return $prefix . '/behat/test_environment_enabled.txt';
498 * The relative path where components stores their behat tests
502 protected static function get_behat_tests_path() {
503 return DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'behat';
507 * Ensures the behat dir exists in moodledata
508 * @throws file_exception
509 * @return string Full path
511 protected static function get_behat_dir() {
514 $behatdir = $CFG->dataroot . '/behat';
516 if (!is_dir($behatdir)) {
517 if (!mkdir($behatdir, $CFG->directorypermissions, true)) {
518 throw new file_exception('storedfilecannotcreatefiledirs');
522 if (!is_writable($behatdir)) {
523 throw new file_exception('storedfilecannotcreatefiledirs');
530 * Returns the executable path
533 protected static function get_behat_command() {
534 return 'vendor' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'behat';
538 * Returns the behat config file path used by the steps definition list
541 protected static function get_steps_list_config_filepath() {
542 return self::get_behat_dir() . '/behat.yml';