b9d12b1e09ea840cf20998c154bb91437b4abc80
[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         if (!CLI_SCRIPT) {
67             confirm_sesskey();
68         }
69         self::check_behat_setup();
71         // Priority to the one specified as argument.
72         if (!$filter) {
73             $filter = optional_param('filter', false, PARAM_ALPHANUMEXT);
74         }
76         if ($filter) {
77             $filteroption = ' -d ' . $filter;
78         } else {
79             $filteroption = ' -di';
80         }
82         $currentcwd = getcwd();
83         chdir($CFG->behatpath);
84         exec('bin/behat --ansi ' . $filteroption, $steps, $code);
85         chdir($currentcwd);
87         // Outputing steps.
89         $content = '';
90         if ($steps) {
91             foreach ($steps as $line) {
93                 // Skipping the step definition context.
94                 if (strpos($line, '#') == 0) {
95                     if (CLI_SCRIPT) {
96                         $content .= $line . PHP_EOL;
97                     } else {
98                         $content .= htmlentities($line) . '<br/>';
99                     }
101                 }
102             }
103         }
105         if ($content === '') {
106             $content = get_string('nostepsdefinitions', 'tool_behat');
107         }
109         if (!CLI_SCRIPT) {
110             $html = self::get_header();
111             $html .= self::get_steps_definitions_form($filter);
112             $html .= html_writer::tag('div', $content, array('id' => 'steps-definitions'));
113             $html .= self::get_footer();
114             echo $html;
115         } else {
116             echo $content;
117         }
118     }
120     /**
121      * Switches from and to the regular environment to the testing environment
122      * @param string $testenvironment enable|disable
123      */
124     public static function switchenvironment($testenvironment = false) {
125         global $CFG;
127         if (!CLI_SCRIPT) {
128             confirm_sesskey();
129         }
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         }
142         if (!CLI_SCRIPT) {
143             redirect(get_login_url());
144         }
145     }
147     /**
148      * Runs the acceptance tests
149      * @param string $tags Restricts the executed tests to the ones that matches the tags
150      * @param string $extra Extra CLI behat options
151      */
152     public static function runtests($tags = false, $extra = false) {
153         global $CFG;
155         if (!CLI_SCRIPT) {
156             confirm_sesskey();
157         }
158         self::check_behat_setup();
160         self::update_config_file();
162         @set_time_limit(0);
164         // Priority to the one specified as argument.
165         if (!$tags) {
166             $tags = optional_param('tags', false, PARAM_ALPHANUMEXT);
167         }
168         // $extra only passed as CLI option (to simplify web runner usage).
170         $tagsoption = '';
171         if ($tags) {
172             $tagsoption = ' --tags ' . $tags;
173         }
175         if (!$extra && !CLI_SCRIPT) {
176             $extra = ' --format html';
177         } else if(!$extra && CLI_SCRIPT) {
178             $extra = '';
179         }
181         // Switching database and dataroot to test environment.
182         self::enable_test_environment();
183         $currentcwd = getcwd();
185         chdir($CFG->behatpath);
186         ob_start();
187         passthru('bin/behat --ansi ' . $tagsoption . ' ' .$extra, $code);
188         $output = ob_get_contents();
189         ob_end_clean();
191         // Switching back to regular environment.
192         self::disable_test_environment();
193         chdir($currentcwd);
195         // Output.
196         echo self::get_header();
197         if (!CLI_SCRIPT) {
198             echo self::get_run_tests_form($tags);
199         }
200         echo $output;
202         // Dirty hack to avoid behat bad HTML structure when test execution throws an exception and there are skipped steps.
203         if (strstr($output, 'class="skipped"') != false) {
204             echo '</ol></div></div></div></body></html>';
205         }
207         echo self::get_footer();
208     }
210     /**
211      * Updates the config file
212      * @throws file_exception
213      */
214     private static function update_config_file() {
216         $contents = '';
218         // Gets all the components with features.
219         $components = tests_finder::get_components_with_tests('features');
220         if ($components) {
221             $featurespaths[] = '';
222             foreach ($components as $componentname => $path) {
223                 $path = self::clean_path($path) . self::$behat_tests_path;
224                 if (empty($featurespaths[$path]) && file_exists($path)) {
225                     $featurespaths[$path] = $path;
226                 }
227             }
228             $contents = 'features:' . implode(PHP_EOL . '  - ', $featurespaths) . PHP_EOL;
229         }
231         // Gets all the components with steps definitions.
232         $components = tests_finder::get_components_with_tests('stepsdefinitions');
233         if ($components) {
234             $contents .= 'steps_definitions:' . PHP_EOL;
235             foreach ($components as $componentname => $filepath) {
236                 $filepath = self::clean_path($path) . self::$behat_tests_path . '/behat_' . $componentname . '.php';
237                 $contents .= '  ' . $componentname . ': ' . $filepath . PHP_EOL;
238             }
239         }
241         // Stores the file.
242         $fullfilepath = self::get_behat_dir() . '/config.yml';
243         if (!file_put_contents($fullfilepath, $contents)) {
244             throw new file_exception('cannotcreatefile', $fullfilepath);
245         }
247     }
250     /**
251      * Cleans the path returned by get_components_with_tests() to standarize it
252      *
253      * {@see tests_finder::get_all_directories_with_tests()} it returns the path including /tests/
254      * @param string $path
255      * @return string The string without the last /tests part
256      */
257     private static function clean_path($path) {
259         $path = rtrim($path, '/');
261         $parttoremove = '/tests';
263         $substr = substr($path, strlen($path) - strlen($parttoremove));
264         if ($substr == $parttoremove) {
265             $path = substr($path, 0, strlen($path) - strlen($parttoremove));
266         }
268         return rtrim($path, '/');
269     }
271     /**
272      * Checks whether the test database and dataroot is ready
273      * Stops execution if something went wrong
274      */
275     private static function test_environment_problem() {
276         global $CFG;
278         // phpunit --diag returns nothing if the test environment is set up correctly.
279         $currentcwd = getcwd();
280         chdir($CFG->dirroot . '/' . $CFG->admin . '/tool/phpunit/cli');
281         exec("php util.php --diag", $output, $code);
282         chdir($currentcwd);
284         // If something is not ready stop execution and display the CLI command output.
285         if ($code != 0) {
286             notice(implode(' ', $output));
287         }
288     }
290     /**
291      * Checks the behat setup
292      */
293     private static function check_behat_setup() {
294         global $CFG;
296         // Moodle setting.
297         if (empty($CFG->behatpath)) {
298             $msg = get_string('nobehatpath', 'tool_behat');
299             $url = $CFG->wwwroot . '/' . $CFG->admin . '/settings.php?section=systempaths';
301             if (!CLI_SCRIPT) {
302                 $msg .= ' ' . html_writer::tag('a', get_string('systempaths', 'admin'), array('href' => $url));
303             }
304             notice($msg);
305         }
307         // Behat test command.
308         $currentcwd = getcwd();
309         chdir($CFG->behatpath);
310         exec('bin/behat --help', $output, $code);
311         chdir($currentcwd);
313         if ($code != 0) {
314             notice(get_string('wrongbehatsetup', 'tool_behat'));
315         }
316     }
318     /**
319      * Enables test mode checking the test environment setup
320      *
321      * Stores a file in dataroot/behat to allow Moodle switch to
322      * test database and dataroot before the initial set up
323      *
324      * @throws file_exception
325      */
326     private static function enable_test_environment() {
327         global $CFG;
329         if (self::is_test_environment_enabled()) {
330             debugging('Test environment was already enabled');
331             return;
332         }
334         // Check that PHPUnit test environment is correctly set up.
335         self::test_environment_problem();
337         $behatdir = self::get_behat_dir();
339         $contents = '$CFG->phpunit_prefix and $CFG->phpunit_dataroot are currently used as $CFG->prefix and $CFG->dataroot';
340         $filepath = $behatdir . '/test_environment_enabled.txt';
341         if (!file_put_contents($filepath, $contents)) {
342             throw new file_exception('cannotcreatefile', $filepath);
343         }
344         chmod($filepath, $CFG->directorypermissions);
345     }
347     /**
348      * Disables test mode
349      */
350     private static function disable_test_environment() {
352         $testenvfile = self::get_test_filepath();
354         if (!self::is_test_environment_enabled()) {
355             debugging('Test environment was already disabled');
356         } else {
357             if (!unlink($testenvfile)) {
358                 throw new file_exception('cannotdeletetestenvironmentfile');
359             }
360         }
361     }
363     /**
364      * Checks whether test environment is enabled or disabled
365      *
366      * It does not return if the current script is running
367      * in test environment {@see tool_behat::is_test_environment_running()}
368      *
369      * @return bool
370      */
371     private static function is_test_environment_enabled() {
373         $testenvfile = self::get_test_filepath();
374         if (file_exists($testenvfile)) {
375             return true;
376         }
378         return false;
379     }
381     /**
382      * Returns true if Moodle is currently running with the test database and dataroot
383      * @return bool
384      */
385     private static function is_test_environment_running() {
386         global $CFG;
388         if (!empty($CFG->originaldataroot)) {
389             return true;
390         }
392         return false;
393     }
395     /**
396      * Returns the path to the file which specifies if test environment is enabled
397      * @return string
398      */
399     private static function get_test_filepath() {
400         global $CFG;
402         if (self::is_test_environment_running()) {
403             $testenvfile = $CFG->originaldataroot . '/behat/test_environment_enabled.txt';
404         } else {
405             $testenvfile = $CFG->dataroot . '/behat/test_environment_enabled.txt';
406         }
408         return $testenvfile;
409     }
412     /**
413      * Ensures the behat dir exists in moodledata
414      * @throws file_exception
415      * @return string Full path
416      */
417     private static function get_behat_dir() {
418         global $CFG;
420         $behatdir = $CFG->dataroot . '/behat';
422         if (!is_dir($behatdir)) {
423             if (!mkdir($behatdir, $CFG->directorypermissions, true)) {
424                 throw new file_exception('storedfilecannotcreatefiledirs');
425             }
426         }
428         if (!is_writable($behatdir)) {
429             throw new file_exception('storedfilecannotcreatefiledirs');
430         }
432         return $behatdir;
433     }
435     /**
436      * Returns header output
437      * @return string
438      */
439     private static function get_header() {
440         global $OUTPUT;
442         $action = optional_param('action', 'info', PARAM_ALPHAEXT);
444         if (CLI_SCRIPT) {
445             return '';
446         }
448         $title = get_string('pluginname', 'tool_behat') . ' - ' . get_string('command' . $action, 'tool_behat');
449         $html = $OUTPUT->header();
450         $html .= $OUTPUT->heading($title);
452         return $html;
453     }
455     /**
456      * Returns footer output
457      * @return string
458      */
459     private static function get_footer() {
460         global $OUTPUT;
462         if (CLI_SCRIPT) {
463             return '';
464         }
466         return $OUTPUT->footer();
467     }
469     /**
470      * Returns a message and a button to continue if web execution
471      * @param string $html
472      * @param string $url
473      * @return string
474      */
475     private static function output_success($html, $url = false) {
476         global $CFG, $OUTPUT;
478         if (!$url) {
479             $url = $CFG->wwwroot . '/' . $CFG->admin . '/tool/behat/index.php';
480         }
482         if (!CLI_SCRIPT) {
483             $html = $OUTPUT->box($html, 'generalbox', 'notice');
484             $html .= $OUTPUT->continue_button($url);
485         }
487         return $html;
488     }
490     /**
491      * Returns the installation instructions
492      *
493      * (hardcoded in English)
494      *
495      * @return string
496      */
497     private static function get_info() {
498         global $OUTPUT;
500         $url = 'http://docs.moodle.org/dev/Acceptance_testing';
502         $html = $OUTPUT->box_start();
503         $html .= html_writer::tag('h1', 'Info');
504         $html .= html_writer::tag('div', 'Follow <a href="' . $url . '" target="_blank">' . $url . '</a> instructions for info about installation and tests execution');
505         $html .= $OUTPUT->box_end();
507         return $html;
508     }
510     /**
511      * Returns the steps definitions form
512      * @param string $filter To filter the steps definitions list by keyword
513      * @return string
514      */
515     private static function get_steps_definitions_form($filter = false) {
516         global $OUTPUT;
518         if ($filter === false) {
519             $filter = '';
520         } else {
521             $filter = s($filter);
522         }
524         $html = $OUTPUT->box_start();
525         $html .= '<form method="get" action="index.php">';
526         $html .= '<fieldset class="invisiblefieldset">';
527         $html .= '<label for="id_filter">Steps definitions which contains</label> ';
528         $html .= '<input type="text" id="id_filter" value="' . $filter . '" name="filter"/> (all steps definitions if empty)';
529         $html .= '<p></p>';
530         $html .= '<input type="submit" value="View available steps definitions" />';
531         $html .= '<input type="hidden" name="action" value="stepsdefinitions" />';
532         $html .= '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
533         $html .= '</fieldset>';
534         $html .= '</form>';
535         $html .= $OUTPUT->box_end();
537         return $html;
538     }