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
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');
30 * Behat commands manager
35 * @copyright 2012 David Monllaó
36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 private static $behat_tests_path = '/tests/behat';
43 * Displays basic info about acceptance tests
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();
54 $html .= self::get_footer();
60 * Lists the available steps definitions
61 * @param string $filter Keyword to filter the list of steps definitions availables
63 public static function stepsdefinitions($filter = false) {
66 self::check_behat_setup();
68 self::update_config_file();
70 // Priority to the one specified as argument.
72 $filter = optional_param('filter', false, PARAM_ALPHANUMEXT);
76 $filteroption = ' -d ' . $filter;
78 $filteroption = ' -di';
86 $currentcwd = getcwd();
87 chdir($CFG->behatpath);
88 exec('bin/behat ' . $color . ' --config="' . self::get_behat_config_filepath() . '" ' . $filteroption, $steps, $code);
95 foreach ($steps as $line) {
97 // Skipping the step definition context.
98 if (strpos($line, '#') == 0) {
100 $content .= $line . PHP_EOL;
102 $content .= htmlentities($line) . '<br/>';
109 if ($content === '') {
110 $content = get_string('nostepsdefinitions', 'tool_behat');
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();
125 * Switches from and to the regular environment to the testing environment
126 * @param string $testenvironment enable|disable
128 public static function switchenvironment($testenvironment = false) {
131 // Priority to the one specified as argument.
132 if (!$testenvironment) {
133 $testenvironment = optional_param('testenvironment', 'enable', PARAM_ALPHA);
136 if ($testenvironment == 'enable') {
137 self::enable_test_environment();
138 } else if ($testenvironment == 'disable') {
139 self::disable_test_environment();
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
148 public static function runtests($tags = false, $extra = false) {
151 self::check_behat_setup();
153 self::update_config_file();
157 // Priority to the one specified as argument.
159 $tags = optional_param('tags', false, PARAM_ALPHANUMEXT);
164 $tagsoption = ' --tags ' . $tags;
171 // Switching database and dataroot to test environment.
172 self::enable_test_environment();
173 $currentcwd = getcwd();
175 chdir($CFG->behatpath);
177 passthru('bin/behat --ansi --config="' . self::get_behat_config_filepath() .'" ' . $tagsoption . ' ' .$extra, $code);
178 $output = ob_get_contents();
181 // Switching back to regular environment.
182 self::disable_test_environment();
186 echo self::get_header();
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>';
194 echo self::get_footer();
198 * Updates the config file
199 * @throws file_exception
201 private static function update_config_file() {
204 $behatpath = rtrim($CFG->behatpath, '/');
206 // Basic behat dependencies.
207 $contents = 'default:
209 features: ' . $behatpath . '/features
210 bootstrap: ' . $behatpath . '/features/bootstrap
212 Behat\MinkExtension\Extension:
213 base_url: ' . $CFG->wwwroot . '
216 Sanpi\Behatch\Extension:
223 ' . $behatpath . '/vendor/moodlehq/behat-extension/init.php:
226 // Gets all the components with features.
227 $components = tests_finder::get_components_with_tests('features');
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;
236 $contents .= ' features:' . implode(PHP_EOL . ' - ', $featurespaths) . PHP_EOL;
239 // Gets all the components with steps definitions.
240 $components = tests_finder::get_components_with_tests('stepsdefinitions');
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();
254 $contents .= ' steps_definitions:' . implode(PHP_EOL . ' ', $stepsdefinitions) . PHP_EOL;
258 if (!file_put_contents(self::get_behat_config_filepath(), $contents)) {
259 throw new file_exception('cannotcreatefile', self::get_behat_config_filepath());
266 * Cleans the path returned by get_components_with_tests() to standarize it
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
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));
283 return rtrim($path, '/');
287 * Checks whether the test database and dataroot is ready
288 * Stops execution if something went wrong
290 private static function test_environment_problem() {
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);
299 // If something is not ready stop execution and display the CLI command output.
301 notice(implode(' ', $output));
306 * Checks the behat setup
308 private static function check_behat_setup() {
312 if (empty($CFG->behatpath)) {
313 $msg = get_string('nobehatpath', 'tool_behat');
314 $url = $CFG->wwwroot . '/' . $CFG->admin . '/settings.php?section=systempaths';
317 $msg .= ' ' . html_writer::tag('a', get_string('systempaths', 'admin'), array('href' => $url));
322 // Behat test command.
323 $currentcwd = getcwd();
324 chdir($CFG->behatpath);
325 exec('bin/behat --help', $output, $code);
329 notice(get_string('wrongbehatsetup', 'tool_behat'));
334 * Enables test mode checking the test environment setup
336 * Stores a file in dataroot/behat to allow Moodle switch to
337 * test database and dataroot before the initial set up
339 * @throws file_exception
341 private static function enable_test_environment() {
344 if (self::is_test_environment_enabled()) {
345 debugging('Test environment was already enabled');
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);
359 chmod($filepath, $CFG->directorypermissions);
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');
372 if (!unlink($testenvfile)) {
373 throw new file_exception('cannotdeletetestenvironmentfile');
379 * Checks whether test environment is enabled or disabled
381 * It does not return if the current script is running
382 * in test environment {@see tool_behat::is_test_environment_running()}
386 private static function is_test_environment_enabled() {
388 $testenvfile = self::get_test_filepath();
389 if (file_exists($testenvfile)) {
397 * Returns true if Moodle is currently running with the test database and dataroot
400 private static function is_test_environment_running() {
403 if (!empty($CFG->originaldataroot)) {
411 * Returns the path to the file which specifies if test environment is enabled
414 private static function get_test_filepath() {
417 if (self::is_test_environment_running()) {
418 $testenvfile = $CFG->originaldataroot . '/behat/test_environment_enabled.txt';
420 $testenvfile = $CFG->dataroot . '/behat/test_environment_enabled.txt';
428 * Ensures the behat dir exists in moodledata
429 * @throws file_exception
430 * @return string Full path
432 private static function get_behat_dir() {
435 $behatdir = $CFG->dataroot . '/behat';
437 if (!is_dir($behatdir)) {
438 if (!mkdir($behatdir, $CFG->directorypermissions, true)) {
439 throw new file_exception('storedfilecannotcreatefiledirs');
443 if (!is_writable($behatdir)) {
444 throw new file_exception('storedfilecannotcreatefiledirs');
451 * Returns the behat config file path
454 private static function get_behat_config_filepath() {
455 return self::get_behat_dir() . '/behat.yml';
459 * Returns header output
462 private static function get_header() {
465 $action = optional_param('action', 'info', PARAM_ALPHAEXT);
471 $title = get_string('pluginname', 'tool_behat') . ' - ' . get_string('command' . $action, 'tool_behat');
472 $html = $OUTPUT->header();
473 $html .= $OUTPUT->heading($title);
479 * Returns footer output
482 private static function get_footer() {
489 return $OUTPUT->footer();
493 * Returns a message and a button to continue if web execution
494 * @param string $html
498 private static function output_success($html, $url = false) {
499 global $CFG, $OUTPUT;
502 $url = $CFG->wwwroot . '/' . $CFG->admin . '/tool/behat/index.php';
506 $html = $OUTPUT->box($html, 'generalbox', 'notice');
507 $html .= $OUTPUT->continue_button($url);
514 * Returns the installation instructions
516 * (hardcoded in English)
520 private static function get_info() {
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();
534 * Returns the steps definitions form
535 * @param string $filter To filter the steps definitions list by keyword
538 private static function get_steps_definitions_form($filter = false) {
541 if ($filter === false) {
544 $filter = s($filter);
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') . ')';
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>';
558 $html .= $OUTPUT->box_end();