7c93619bff9eb6076b01c6b9f2e00cd099cf6ab0
[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.
242             if (empty($config[$key])) {
243                 $config[$key] = $value;
245             // Merge branches if the key exists in both branches.
246             } else {
247                 $config[$key] = self::merge_config($config[$key], $localconfig[$key]);
248             }
249         }
251         return $config;
252     }
254     /**
255      * Gets the list of Moodle steps definitions
256      *
257      * Class name as a key and the filepath as value
258      *
259      * Externalized from update_config_file() to use
260      * it from the steps definitions web interface
261      *
262      * @return array
263      */
264     public static function get_components_steps_definitions() {
266         $components = tests_finder::get_components_with_tests('stepsdefinitions');
267         if (!$components) {
268             return false;
269         }
271         $stepsdefinitions = array();
272         foreach ($components as $componentname => $componentpath) {
273             $componentpath = self::clean_path($componentpath);
274             $diriterator = new DirectoryIterator($componentpath . self::get_behat_tests_path());
275             $regite = new RegexIterator($diriterator, '|behat_.*\.php$|');
277             // All behat_*.php inside self::get_behat_tests_path() are added as steps definitions files.
278             foreach ($regite as $file) {
279                 $key = $file->getBasename('.php');
280                 $stepsdefinitions[$key] = $file->getPathname();
281             }
282         }
284         return $stepsdefinitions;
285     }
287     /**
288      * Checks if $CFG->behat_wwwroot is available
289      *
290      * @return boolean
291      */
292     public static function is_server_running() {
293         global $CFG;
295         $request = new curl();
296         $request->get($CFG->behat_wwwroot);
297         return (true && !$request->get_errno());
298     }
300     /**
301      * Cleans the path returned by get_components_with_tests() to standarize it
302      *
303      * {@see tests_finder::get_all_directories_with_tests()} it returns the path including /tests/
304      * @param string $path
305      * @return string The string without the last /tests part
306      */
307     protected static function clean_path($path) {
309         $path = rtrim($path, DIRECTORY_SEPARATOR);
311         $parttoremove = DIRECTORY_SEPARATOR . 'tests';
313         $substr = substr($path, strlen($path) - strlen($parttoremove));
314         if ($substr == $parttoremove) {
315             $path = substr($path, 0, strlen($path) - strlen($parttoremove));
316         }
318         return rtrim($path, DIRECTORY_SEPARATOR);
319     }
321     /**
322      * Checks whether the test database and dataroot is ready
323      * Stops execution if something went wrong
324      */
325     protected static function test_environment_problem() {
326         global $CFG;
328         // PHPUnit --diag returns nothing if the test environment is set up correctly.
329         exec('php ' . $CFG->dirroot . '/' . $CFG->admin . '/tool/phpunit/cli/util.php --diag', $output, $code);
331         // If something is not ready stop execution and display the CLI command output.
332         if ($code != 0) {
333             notice(get_string('phpunitenvproblem', 'tool_behat') . ': ' . implode(' ', $output));
334         }
335     }
337     /**
338      * Checks if behat is set up and working
339      *
340      * It checks behat dependencies have been installed and runs
341      * the behat help command to ensure it works as expected
342      * @param boolean $checkphp Extra check for the PHP version
343      */
344     protected static function check_behat_setup($checkphp = false) {
345         global $CFG;
347         // We don't check the PHP version if $CFG->behat_switchcompletely has been enabled.
348         if (empty($CFG->behat_switchcompletely) && $checkphp && version_compare(PHP_VERSION, '5.4.0', '<')) {
349             throw new Exception(get_string('wrongphpversion', 'tool_behat'));
350         }
352         // Moodle setting.
353         if (!tool_behat::are_behat_dependencies_installed()) {
355             $msg = get_string('wrongbehatsetup', 'tool_behat');
357             // With HTML.
358             $docslink = tool_behat::$docsurl . '#Installation';
359             if (!CLI_SCRIPT) {
360                 $docslink = html_writer::tag('a', $docslink, array('href' => $docslink, 'target' => '_blank'));
361             }
362             $msg .= '. ' . get_string('moreinfoin', 'tool_behat') . ' ' . $docslink;
363             notice($msg);
364         }
366         // Behat test command.
367         $currentcwd = getcwd();
368         chdir($CFG->dirroot);
369         exec(self::get_behat_command() . ' --help', $output, $code);
370         chdir($currentcwd);
372         if ($code != 0) {
373             notice(get_string('wrongbehatsetup', 'tool_behat'));
374         }
375     }
377     /**
378      * Enables test mode
379      *
380      * Starts the test mode checking the composer installation and
381      * the phpunit test environment and updating the available
382      * features and steps definitions.
383      *
384      * Stores a file in dataroot/behat to allow Moodle to switch
385      * to the test environment when using cli-server (or $CFG->behat_switchcompletely)
386      *
387      * @throws file_exception
388      */
389     protected static function start_test_mode() {
390         global $CFG;
392         // Checks the behat set up and the PHP version.
393         self::check_behat_setup(true);
395         // Check that PHPUnit test environment is correctly set up.
396         self::test_environment_problem();
398         // Updates all the Moodle features and steps definitions.
399         self::update_config_file();
401         if (self::is_test_mode_enabled()) {
402             debugging('Test environment was already enabled');
403             return;
404         }
406         $behatdir = self::get_behat_dir();
408         $contents = '$CFG->behat_wwwroot, $CFG->phpunit_prefix and $CFG->phpunit_dataroot' .
409             ' are currently used as $CFG->wwwroot, $CFG->prefix and $CFG->dataroot';
410         $filepath = $behatdir . '/test_environment_enabled.txt';
411         if (!file_put_contents($filepath, $contents)) {
412             throw new file_exception('cannotcreatefile', $filepath);
413         }
414         chmod($filepath, $CFG->directorypermissions);
415     }
417     /**
418      * Disables test mode
419      * @throws file_exception
420      */
421     protected static function stop_test_mode() {
423         $testenvfile = self::get_test_filepath();
425         if (!self::is_test_mode_enabled()) {
426             debugging('Test environment was already disabled');
427         } else {
428             if (!unlink($testenvfile)) {
429                 throw new file_exception('cannotdeletetestenvironmentfile');
430             }
431         }
432     }
434     /**
435      * Checks whether test environment is enabled or disabled
436      *
437      * To check is the current script is running in the test
438      * environment {@see tool_behat::is_test_environment_running()}
439      *
440      * @return bool
441      */
442     public static function is_test_mode_enabled() {
444         $testenvfile = self::get_test_filepath();
445         if (file_exists($testenvfile)) {
446             return true;
447         }
449         return false;
450     }
452     /**
453      * Returns true if Moodle is currently running with the test database and dataroot
454      * @return bool
455      */
456     public static function is_test_environment_running() {
457         global $CFG;
459         if (!empty($CFG->originaldataroot)) {
460             return true;
461         }
463         return false;
464     }
466     /**
467      * Has the site installed composer with --dev option
468      * @return boolean
469      */
470     public static function are_behat_dependencies_installed() {
471         if (!is_dir(__DIR__ . '/../../../vendor/behat')) {
472             return false;
473         }
474         return true;
475     }
477     /**
478      * Returns the path to the file which specifies if test environment is enabled
479      *
480      * The file is in dataroot/behat/ but we need to
481      * know if test mode is running because then we swap
482      * it to phpunit_dataroot and we need the original value
483      *
484      * @return string
485      */
486     protected static function get_test_filepath() {
487         global $CFG;
489         if (self::is_test_environment_running()) {
490             $prefix = $CFG->originaldataroot;
491         } else {
492             $prefix = $CFG->dataroot;
493         }
495         return $prefix . '/behat/test_environment_enabled.txt';
496     }
499     /**
500      * The relative path where components stores their behat tests
501      *
502      * @return string
503      */
504     protected static function get_behat_tests_path() {
505         return DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'behat';
506     }
508     /**
509      * Ensures the behat dir exists in moodledata
510      * @throws file_exception
511      * @return string Full path
512      */
513     protected static function get_behat_dir() {
514         global $CFG;
516         $behatdir = $CFG->dataroot . '/behat';
518         if (!is_dir($behatdir)) {
519             if (!mkdir($behatdir, $CFG->directorypermissions, true)) {
520                 throw new file_exception('storedfilecannotcreatefiledirs');
521             }
522         }
524         if (!is_writable($behatdir)) {
525             throw new file_exception('storedfilecannotcreatefiledirs');
526         }
528         return $behatdir;
529     }
531     /**
532      * Returns the executable path
533      * @return string
534      */
535     protected static function get_behat_command() {
536         return 'vendor' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'behat';
537     }
539     /**
540      * Returns the behat config file path used by the steps definition list
541      * @return string
542      */
543     protected static function get_steps_list_config_filepath() {
544         return self::get_behat_dir() . '/behat.yml';
545     }