MDL-37046 behat: Added to standard plugins list
[moodle.git] / admin / tool / behat / locallib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Behat commands
19  *
20  * @package    tool_behat
21  * @copyright  2012 David MonllaĆ³
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 global $CFG;
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');
32 /**
33  * Behat commands manager
34  *
35  * @package    tool_behat
36  * @copyright  2012 David MonllaĆ³
37  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class tool_behat {
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';
47     /**
48      * Lists the available steps definitions
49      *
50      * @param string $type
51      * @param string $component
52      * @param string $filter
53      * @return string
54      */
55     public static function stepsdefinitions($type, $component, $filter) {
56         global $CFG;
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.
64         if ($type) {
65             $filter .= '&&' . $type;
66         }
68         if ($filter) {
69             $filteroption = ' -d "' . $filter . '"';
70         } else {
71             $filteroption = ' -di';
72         }
74         $currentcwd = getcwd();
75         chdir($CFG->dirroot);
76         exec(self::get_behat_command() . ' --config="'.self::get_steps_list_config_filepath(). '" '.$filteroption, $steps, $code);
77         chdir($currentcwd);
79         if ($steps) {
80             $stepshtml = implode('', $steps);
81         }
83         if (!isset($stepshtml) || $stepshtml == '') {
84             $stepshtml = get_string('nostepsdefinitions', 'tool_behat');
85         }
87         return $stepshtml;
88     }
90     /**
91      * Allows / disables the test environment to be accessed through the built-in server
92      *
93      * Built-in server must be started separately
94      *
95      * @param string $testenvironment enable|disable
96      */
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();
102         }
103     }
105     /**
106      * Updates a config file
107      *
108      * The tests runner and the steps definitions list uses different
109      * config files to avoid problems with concurrent executions.
110      *
111      * The steps definitions list can be filtered by component so it's
112      * behat.yml can be different from the dirroot one.
113      *
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
117      */
118     protected static function update_config_file($component = '', $testsrunner = true) {
119         global $CFG;
121         // Behat must run with the whole set of features and steps definitions.
122         if ($testsrunner === true) {
123             $prefix = '';
124             $configfilepath = $CFG->dirroot . '/behat.yml';
126             // Alternative for steps definitions filtering.
127         } else {
128             $configfilepath = self::get_steps_list_config_filepath();
129             $prefix = $CFG->dirroot .'/';
130         }
132         // Gets all the components with features.
133         $features = array();
134         $components = tests_finder::get_components_with_tests('features');
135         if ($components) {
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;
141                 }
142             }
143             $features = array_values($featurespaths);
144         }
146         // Gets all the components with steps definitions.
147         $stepsdefinitions = array();
148         $steps = self::get_components_steps_definitions();
149         if ($steps) {
150             foreach ($steps as $key => $filepath) {
151                 if ($component == '' || $component === $key) {
152                     $stepsdefinitions[$key] = $filepath;
153                 }
154             }
155         }
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);
161         // Stores the file.
162         if (!file_put_contents($configfilepath, $contents)) {
163             throw new file_exception('cannotcreatefile', $configfilepath);
164         }
166     }
168     /**
169      * Behat config file specifing the main context class,
170      * the required Behat extensions and Moodle test wwwroot.
171      *
172      * @param string $prefix The filesystem prefix
173      * @param array $features The system feature files
174      * @param array $stepsdefinitions The system steps definitions
175      * @return string
176      */
177     protected static function get_config_file_contents($prefix, $features, $stepsdefinitions) {
178         global $CFG;
180         // We require here when we are sure behat dependencies are available.
181         require_once($CFG->dirroot . '/vendor/autoload.php');
183         $config = array(
184             'default' => array(
185                 'paths' => array(
186                     'features' => $prefix . 'lib/behat/features',
187                     'bootstrap' => $prefix . 'lib/behat/features/bootstrap',
188                 ),
189                 'context' => array(
190                     'class' => 'behat_init_context'
191                 ),
192                 'extensions' => array(
193                     'Behat\MinkExtension\Extension' => array(
194                         'base_url' => $CFG->behat_wwwroot,
195                         'goutte' => null,
196                         'selenium2' => null
197                     ),
198                     'Moodle\BehatExtension\Extension' => array(
199                         'features' => $features,
200                         'steps_definitions' => $stepsdefinitions
201                     )
202                 )
203             )
204         );
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);
209         }
211         return Symfony\Component\Yaml\Yaml::dump($config, 10, 2);
212     }
214     /**
215      * Overrides default config with local config values
216      *
217      * array_merge does not merge completely the array's values
218      *
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
222      */
223     protected static function merge_config($config, $localconfig) {
225         if (!is_array($config) && !is_array($localconfig)) {
226             return $localconfig;
227         }
229         // Local overrides also deeper default values.
230         if (is_array($config) && !is_array($localconfig)) {
231             return $localconfig;
232         }
234         foreach ($localconfig as $key => $value) {
236             // If defaults are not as deep as local values let locals override.
237             if (!is_array($config)) {
238                 unset($config);
239             }
241             // Add the param if it doesn't exists or merge branches.
242             if (empty($config[$key])) {
243                 $config[$key] = $value;
244             } else {
245                 $config[$key] = self::merge_config($config[$key], $localconfig[$key]);
246             }
247         }
249         return $config;
250     }
252     /**
253      * Gets the list of Moodle steps definitions
254      *
255      * Class name as a key and the filepath as value
256      *
257      * Externalized from update_config_file() to use
258      * it from the steps definitions web interface
259      *
260      * @return array
261      */
262     public static function get_components_steps_definitions() {
264         $components = tests_finder::get_components_with_tests('stepsdefinitions');
265         if (!$components) {
266             return false;
267         }
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();
279             }
280         }
282         return $stepsdefinitions;
283     }
285     /**
286      * Checks if $CFG->behat_wwwroot is available
287      *
288      * @return boolean
289      */
290     public static function is_server_running() {
291         global $CFG;
293         $request = new curl();
294         $request->get($CFG->behat_wwwroot);
295         return (true && !$request->get_errno());
296     }
298     /**
299      * Cleans the path returned by get_components_with_tests() to standarize it
300      *
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
304      */
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));
314         }
316         return rtrim($path, DIRECTORY_SEPARATOR);
317     }
319     /**
320      * Checks whether the test database and dataroot is ready
321      * Stops execution if something went wrong
322      */
323     protected static function test_environment_problem() {
324         global $CFG;
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.
330         if ($code != 0) {
331             notice(get_string('phpunitenvproblem', 'tool_behat') . ': ' . implode(' ', $output));
332         }
333     }
335     /**
336      * Checks if behat is set up and working
337      *
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
341      */
342     protected static function check_behat_setup($checkphp = false) {
343         global $CFG;
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'));
348         }
350         // Moodle setting.
351         if (!self::are_behat_dependencies_installed()) {
353             $msg = get_string('wrongbehatsetup', 'tool_behat');
355             // With HTML.
356             $docslink = self::$docsurl . '#Installation';
357             if (!CLI_SCRIPT) {
358                 $docslink = html_writer::tag('a', $docslink, array('href' => $docslink, 'target' => '_blank'));
359             }
360             $msg .= '. ' . get_string('moreinfoin', 'tool_behat') . ' ' . $docslink;
361             notice($msg);
362         }
364         // Behat test command.
365         $currentcwd = getcwd();
366         chdir($CFG->dirroot);
367         exec(self::get_behat_command() . ' --help', $output, $code);
368         chdir($currentcwd);
370         if ($code != 0) {
371             notice(get_string('wrongbehatsetup', 'tool_behat'));
372         }
373     }
375     /**
376      * Enables test mode
377      *
378      * Starts the test mode checking the composer installation and
379      * the phpunit test environment and updating the available
380      * features and steps definitions.
381      *
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)
384      *
385      * @throws file_exception
386      */
387     protected static function start_test_mode() {
388         global $CFG;
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');
401             return;
402         }
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);
411         }
412         chmod($filepath, $CFG->directorypermissions);
413     }
415     /**
416      * Disables test mode
417      * @throws file_exception
418      */
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');
425         } else {
426             if (!unlink($testenvfile)) {
427                 throw new file_exception('cannotdeletetestenvironmentfile');
428             }
429         }
430     }
432     /**
433      * Checks whether test environment is enabled or disabled
434      *
435      * To check is the current script is running in the test
436      * environment {@see tool_behat::is_test_environment_running()}
437      *
438      * @return bool
439      */
440     public static function is_test_mode_enabled() {
442         $testenvfile = self::get_test_filepath();
443         if (file_exists($testenvfile)) {
444             return true;
445         }
447         return false;
448     }
450     /**
451      * Returns true if Moodle is currently running with the test database and dataroot
452      * @return bool
453      */
454     public static function is_test_environment_running() {
455         global $CFG;
457         if (!empty($CFG->originaldataroot)) {
458             return true;
459         }
461         return false;
462     }
464     /**
465      * Has the site installed composer with --dev option
466      * @return boolean
467      */
468     public static function are_behat_dependencies_installed() {
469         if (!is_dir(__DIR__ . '/../../../vendor/behat')) {
470             return false;
471         }
472         return true;
473     }
475     /**
476      * Returns the path to the file which specifies if test environment is enabled
477      *
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
481      *
482      * @return string
483      */
484     protected static function get_test_filepath() {
485         global $CFG;
487         if (self::is_test_environment_running()) {
488             $prefix = $CFG->originaldataroot;
489         } else {
490             $prefix = $CFG->dataroot;
491         }
493         return $prefix . '/behat/test_environment_enabled.txt';
494     }
497     /**
498      * The relative path where components stores their behat tests
499      *
500      * @return string
501      */
502     protected static function get_behat_tests_path() {
503         return DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'behat';
504     }
506     /**
507      * Ensures the behat dir exists in moodledata
508      * @throws file_exception
509      * @return string Full path
510      */
511     protected static function get_behat_dir() {
512         global $CFG;
514         $behatdir = $CFG->dataroot . '/behat';
516         if (!is_dir($behatdir)) {
517             if (!mkdir($behatdir, $CFG->directorypermissions, true)) {
518                 throw new file_exception('storedfilecannotcreatefiledirs');
519             }
520         }
522         if (!is_writable($behatdir)) {
523             throw new file_exception('storedfilecannotcreatefiledirs');
524         }
526         return $behatdir;
527     }
529     /**
530      * Returns the executable path
531      * @return string
532      */
533     protected static function get_behat_command() {
534         return 'vendor' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'behat';
535     }
537     /**
538      * Returns the behat config file path used by the steps definition list
539      * @return string
540      */
541     protected static function get_steps_list_config_filepath() {
542         return self::get_behat_dir() . '/behat.yml';
543     }