MDL-37046 behat: Using single config file on moodle side
[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 require_once($CFG->libdir . '/filestorage/file_exceptions.php');
26 require_once($CFG->libdir . '/phpunit/bootstraplib.php');
27 require_once($CFG->libdir . '/phpunit/classes/tests_finder.php');
29 /**
30  * Behat commands manager
31  *
32  * CLI + web execution
33  *
34  * @package    tool_behat
35  * @copyright  2012 David Monllaó
36  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
38 class tool_behat {
40     private static $behat_tests_path = '/tests/behat';
42     /**
43      * Displays basic info about acceptance tests
44      */
45     public static function info() {
47         $html = self::get_header();
48         $html .= self::get_info();
50         if (!self::is_test_environment_running()) {
51             $html .= self::get_steps_definitions_form();
52         }
54         $html .= self::get_footer();
56         echo $html;
57     }
59     /**
60      * Lists the available steps definitions
61      * @param string $filter Keyword to filter the list of steps definitions availables
62      */
63     public static function stepsdefinitions($filter = false) {
64         global $CFG;
66         self::check_behat_setup();
68         self::update_config_file();
70         // Priority to the one specified as argument.
71         if (!$filter) {
72             $filter = optional_param('filter', false, PARAM_ALPHANUMEXT);
73         }
75         if ($filter) {
76             $filteroption = ' -d ' . $filter;
77         } else {
78             $filteroption = ' -di';
79         }
81         $color = '';
82         if (CLI_SCRIPT) {
83             $color = '--ansi ';
84         }
86         $currentcwd = getcwd();
87         chdir($CFG->behatpath);
88         exec('bin/behat ' . $color . ' --config="' . self::get_behat_config_filepath() . '" ' . $filteroption, $steps, $code);
89         chdir($currentcwd);
91         // Outputing steps.
93         $content = '';
94         if ($steps) {
95             foreach ($steps as $line) {
97                 // Skipping the step definition context.
98                 if (strpos($line, '#') == 0) {
99                     if (CLI_SCRIPT) {
100                         $content .= $line . PHP_EOL;
101                     } else {
102                         $content .= htmlentities($line) . '<br/>';
103                     }
105                 }
106             }
107         }
109         if ($content === '') {
110             $content = get_string('nostepsdefinitions', 'tool_behat');
111         }
113         if (!CLI_SCRIPT) {
114             $html = self::get_header();
115             $html .= self::get_steps_definitions_form($filter);
116             $html .= html_writer::tag('div', $content, array('id' => 'steps-definitions'));
117             $html .= self::get_footer();
118             echo $html;
119         } else {
120             echo $content;
121         }
122     }
124     /**
125      * Switches from and to the regular environment to the testing environment
126      * @param string $testenvironment enable|disable
127      */
128     public static function switchenvironment($testenvironment = false) {
129         global $CFG;
131         // Priority to the one specified as argument.
132         if (!$testenvironment) {
133             $testenvironment = optional_param('testenvironment', 'enable', PARAM_ALPHA);
134         }
136         if ($testenvironment == 'enable') {
137             self::enable_test_environment();
138         } else if ($testenvironment == 'disable') {
139             self::disable_test_environment();
140         }
141     }
143     /**
144      * Runs the acceptance tests
145      * @param string $tags Restricts the executed tests to the ones that matches the tags
146      * @param string $extra Extra CLI behat options
147      */
148     public static function runtests($tags = false, $extra = false) {
149         global $CFG;
151         self::check_behat_setup();
153         self::update_config_file();
155         @set_time_limit(0);
157         // Priority to the one specified as argument.
158         if (!$tags) {
159             $tags = optional_param('tags', false, PARAM_ALPHANUMEXT);
160         }
162         $tagsoption = '';
163         if ($tags) {
164             $tagsoption = ' --tags ' . $tags;
165         }
167         if (!$extra) {
168             $extra = '';
169         }
171         // Switching database and dataroot to test environment.
172         self::enable_test_environment();
173         $currentcwd = getcwd();
175         chdir($CFG->behatpath);
176         ob_start();
177         passthru('bin/behat --ansi --config="' . self::get_behat_config_filepath() .'" ' . $tagsoption . ' ' .$extra, $code);
178         $output = ob_get_contents();
179         ob_end_clean();
181         // Switching back to regular environment.
182         self::disable_test_environment();
183         chdir($currentcwd);
185         // Output.
186         echo self::get_header();
187         echo $output;
189         // Dirty hack to avoid behat bad HTML structure when test execution throws an exception and there are skipped steps.
190         if (strstr($output, 'class="skipped"') != false) {
191             echo '</ol></div></div></div></body></html>';
192         }
194         echo self::get_footer();
195     }
197     /**
198      * Updates the config file
199      * @throws file_exception
200      */
201     private static function update_config_file() {
202         global $CFG;
204         $behatpath = rtrim($CFG->behatpath, '/');
206         // Basic behat dependencies.
207         $contents = 'default:
208   paths:
209     features: ' . $behatpath . '/features
210     bootstrap: ' . $behatpath . '/features/bootstrap
211   extensions:
212     Behat\MinkExtension\Extension:
213       base_url: ' . $CFG->wwwroot . '
214       goutte: ~
215       selenium2: ~
216     Sanpi\Behatch\Extension:
217       contexts:
218         browser: ~
219         system: ~
220         json: ~
221         table: ~
222         rest: ~
223     ' . $behatpath . '/vendor/moodlehq/behat-extension/init.php:
224 ';
226         // Gets all the components with features.
227         $components = tests_finder::get_components_with_tests('features');
228         if ($components) {
229             $featurespaths = array('');
230             foreach ($components as $componentname => $path) {
231                 $path = self::clean_path($path) . self::$behat_tests_path;
232                 if (empty($featurespaths[$path]) && file_exists($path)) {
233                     $featurespaths[$path] = $path;
234                 }
235             }
236             $contents .= '      features:' . implode(PHP_EOL . '        - ', $featurespaths) . PHP_EOL;
237         }
239         // Gets all the components with steps definitions.
240         $components = tests_finder::get_components_with_tests('stepsdefinitions');
241         if ($components) {
242             $stepsdefinitions = array('');
243             foreach ($components as $componentname => $componentpath) {
244                 $componentpath = self::clean_path($componentpath);
245                 $diriterator = new DirectoryIterator($componentpath . self::$behat_tests_path);
246                 $regite = new RegexIterator($diriterator, '|behat_.*\.php$|');
248                 // All behat_*.php inside self::$behat_tests_path are added as steps definitions files
249                 foreach ($regite as $file) {
250                     $key = $file->getBasename('.php');
251                     $stepsdefinitions[$key] = $key . ': ' . $file->getPathname();
252                 }
253             }
254             $contents .= '      steps_definitions:' . implode(PHP_EOL . '        ', $stepsdefinitions) . PHP_EOL;
255         }
257         // Stores the file.
258         if (!file_put_contents(self::get_behat_config_filepath(), $contents)) {
259             throw new file_exception('cannotcreatefile', self::get_behat_config_filepath());
260         }
262     }
265     /**
266      * Cleans the path returned by get_components_with_tests() to standarize it
267      *
268      * {@see tests_finder::get_all_directories_with_tests()} it returns the path including /tests/
269      * @param string $path
270      * @return string The string without the last /tests part
271      */
272     private static function clean_path($path) {
274         $path = rtrim($path, '/');
276         $parttoremove = '/tests';
278         $substr = substr($path, strlen($path) - strlen($parttoremove));
279         if ($substr == $parttoremove) {
280             $path = substr($path, 0, strlen($path) - strlen($parttoremove));
281         }
283         return rtrim($path, '/');
284     }
286     /**
287      * Checks whether the test database and dataroot is ready
288      * Stops execution if something went wrong
289      */
290     private static function test_environment_problem() {
291         global $CFG;
293         // phpunit --diag returns nothing if the test environment is set up correctly.
294         $currentcwd = getcwd();
295         chdir($CFG->dirroot . '/' . $CFG->admin . '/tool/phpunit/cli');
296         exec("php util.php --diag", $output, $code);
297         chdir($currentcwd);
299         // If something is not ready stop execution and display the CLI command output.
300         if ($code != 0) {
301             notice(implode(' ', $output));
302         }
303     }
305     /**
306      * Checks the behat setup
307      */
308     private static function check_behat_setup() {
309         global $CFG;
311         // Moodle setting.
312         if (empty($CFG->behatpath)) {
313             $msg = get_string('nobehatpath', 'tool_behat');
314             $url = $CFG->wwwroot . '/' . $CFG->admin . '/settings.php?section=systempaths';
316             if (!CLI_SCRIPT) {
317                 $msg .= ' ' . html_writer::tag('a', get_string('systempaths', 'admin'), array('href' => $url));
318             }
319             notice($msg);
320         }
322         // Behat test command.
323         $currentcwd = getcwd();
324         chdir($CFG->behatpath);
325         exec('bin/behat --help', $output, $code);
326         chdir($currentcwd);
328         if ($code != 0) {
329             notice(get_string('wrongbehatsetup', 'tool_behat'));
330         }
331     }
333     /**
334      * Enables test mode checking the test environment setup
335      *
336      * Stores a file in dataroot/behat to allow Moodle switch to
337      * test database and dataroot before the initial set up
338      *
339      * @throws file_exception
340      */
341     private static function enable_test_environment() {
342         global $CFG;
344         if (self::is_test_environment_enabled()) {
345             debugging('Test environment was already enabled');
346             return;
347         }
349         // Check that PHPUnit test environment is correctly set up.
350         self::test_environment_problem();
352         $behatdir = self::get_behat_dir();
354         $contents = '$CFG->phpunit_prefix and $CFG->phpunit_dataroot are currently used as $CFG->prefix and $CFG->dataroot';
355         $filepath = $behatdir . '/test_environment_enabled.txt';
356         if (!file_put_contents($filepath, $contents)) {
357             throw new file_exception('cannotcreatefile', $filepath);
358         }
359         chmod($filepath, $CFG->directorypermissions);
360     }
362     /**
363      * Disables test mode
364      */
365     private static function disable_test_environment() {
367         $testenvfile = self::get_test_filepath();
369         if (!self::is_test_environment_enabled()) {
370             debugging('Test environment was already disabled');
371         } else {
372             if (!unlink($testenvfile)) {
373                 throw new file_exception('cannotdeletetestenvironmentfile');
374             }
375         }
376     }
378     /**
379      * Checks whether test environment is enabled or disabled
380      *
381      * It does not return if the current script is running
382      * in test environment {@see tool_behat::is_test_environment_running()}
383      *
384      * @return bool
385      */
386     private static function is_test_environment_enabled() {
388         $testenvfile = self::get_test_filepath();
389         if (file_exists($testenvfile)) {
390             return true;
391         }
393         return false;
394     }
396     /**
397      * Returns true if Moodle is currently running with the test database and dataroot
398      * @return bool
399      */
400     private static function is_test_environment_running() {
401         global $CFG;
403         if (!empty($CFG->originaldataroot)) {
404             return true;
405         }
407         return false;
408     }
410     /**
411      * Returns the path to the file which specifies if test environment is enabled
412      * @return string
413      */
414     private static function get_test_filepath() {
415         global $CFG;
417         if (self::is_test_environment_running()) {
418             $testenvfile = $CFG->originaldataroot . '/behat/test_environment_enabled.txt';
419         } else {
420             $testenvfile = $CFG->dataroot . '/behat/test_environment_enabled.txt';
421         }
423         return $testenvfile;
424     }
427     /**
428      * Ensures the behat dir exists in moodledata
429      * @throws file_exception
430      * @return string Full path
431      */
432     private static function get_behat_dir() {
433         global $CFG;
435         $behatdir = $CFG->dataroot . '/behat';
437         if (!is_dir($behatdir)) {
438             if (!mkdir($behatdir, $CFG->directorypermissions, true)) {
439                 throw new file_exception('storedfilecannotcreatefiledirs');
440             }
441         }
443         if (!is_writable($behatdir)) {
444             throw new file_exception('storedfilecannotcreatefiledirs');
445         }
447         return $behatdir;
448     }
450     /**
451      * Returns the behat config file path
452      * @return string
453      */
454     private static function get_behat_config_filepath() {
455         return self::get_behat_dir() . '/behat.yml';
456     }
458     /**
459      * Returns header output
460      * @return string
461      */
462     private static function get_header() {
463         global $OUTPUT;
465         $action = optional_param('action', 'info', PARAM_ALPHAEXT);
467         if (CLI_SCRIPT) {
468             return '';
469         }
471         $title = get_string('pluginname', 'tool_behat') . ' - ' . get_string('command' . $action, 'tool_behat');
472         $html = $OUTPUT->header();
473         $html .= $OUTPUT->heading($title);
475         return $html;
476     }
478     /**
479      * Returns footer output
480      * @return string
481      */
482     private static function get_footer() {
483         global $OUTPUT;
485         if (CLI_SCRIPT) {
486             return '';
487         }
489         return $OUTPUT->footer();
490     }
492     /**
493      * Returns a message and a button to continue if web execution
494      * @param string $html
495      * @param string $url
496      * @return string
497      */
498     private static function output_success($html, $url = false) {
499         global $CFG, $OUTPUT;
501         if (!$url) {
502             $url = $CFG->wwwroot . '/' . $CFG->admin . '/tool/behat/index.php';
503         }
505         if (!CLI_SCRIPT) {
506             $html = $OUTPUT->box($html, 'generalbox', 'notice');
507             $html .= $OUTPUT->continue_button($url);
508         }
510         return $html;
511     }
513     /**
514      * Returns the installation instructions
515      *
516      * (hardcoded in English)
517      *
518      * @return string
519      */
520     private static function get_info() {
521         global $OUTPUT;
523         $url = 'http://docs.moodle.org/dev/Acceptance_testing';
525         $html = $OUTPUT->box_start();
526         $html .= html_writer::tag('h1', 'Info');
527         $html .= html_writer::tag('div', 'Follow <a href="' . $url . '" target="_blank">' . $url . '</a> instructions for info about installation and tests execution');
528         $html .= $OUTPUT->box_end();
530         return $html;
531     }
533     /**
534      * Returns the steps definitions form
535      * @param string $filter To filter the steps definitions list by keyword
536      * @return string
537      */
538     private static function get_steps_definitions_form($filter = false) {
539         global $OUTPUT;
541         if ($filter === false) {
542             $filter = '';
543         } else {
544             $filter = s($filter);
545         }
547         $html = $OUTPUT->box_start();
548         $html .= '<form method="get" action="index.php">';
549         $html .= '<fieldset class="invisiblefieldset">';
550         $html .= '<label for="id_filter">' . get_string('stepsdefinitions', 'tool_behat') . '</label> ';
551         $html .= '<input type="text" id="id_filter" value="' . $filter . '" name="filter"/> (' . get_string('stepsdefinitionsemptyfilter', 'tool_behat') . ')';
552         $html .= '<p></p>';
553         $html .= '<input type="submit" value="' . get_string('viewsteps', 'tool_behat') . '" />';
554         $html .= '<input type="hidden" name="action" value="stepsdefinitions" />';
555         $html .= '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
556         $html .= '</fieldset>';
557         $html .= '</form>';
558         $html .= $OUTPUT->box_end();
560         return $html;
561     }