MDL-37046 behat: CLI option to add/remove javascript tests
[moodle.git] / admin / tool / behat / locallib.php
CommitLineData
7f541ea3
DM
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/>.
16
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 */
24
25require_once($CFG->libdir . '/filestorage/file_exceptions.php');
26require_once($CFG->libdir . '/phpunit/bootstraplib.php');
33005f68 27require_once($CFG->libdir . '/phpunit/classes/tests_finder.php');
7f541ea3 28
6d994c51
DM
29/**
30 * Behat commands manager
31 *
6d994c51
DM
32 * @package tool_behat
33 * @copyright 2012 David MonllaĆ³
34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35 */
7f541ea3
DM
36class tool_behat {
37
93245e95
DM
38 private static $behat_tests_path = '/tests/behat';
39
7f541ea3
DM
40 /**
41 * Displays basic info about acceptance tests
42 */
43 public static function info() {
44
33005f68
DM
45 $html = self::get_header();
46 $html .= self::get_info();
cca2c806 47 $html .= self::get_steps_definitions_form();
33005f68 48 $html .= self::get_footer();
7f541ea3
DM
49
50 echo $html;
51 }
52
7f541ea3
DM
53 /**
54 * Lists the available steps definitions
6d994c51 55 * @param string $filter Keyword to filter the list of steps definitions availables
7f541ea3 56 */
6d994c51 57 public static function stepsdefinitions($filter = false) {
7f541ea3
DM
58 global $CFG;
59
33005f68 60 self::check_behat_setup();
7f541ea3 61
df8dcf3e
DM
62 self::update_config_file();
63
6d994c51
DM
64 // Priority to the one specified as argument.
65 if (!$filter) {
66 $filter = optional_param('filter', false, PARAM_ALPHANUMEXT);
67 }
68
69 if ($filter) {
7f541ea3
DM
70 $filteroption = ' -d ' . $filter;
71 } else {
72 $filteroption = ' -di';
73 }
74
df8dcf3e
DM
75 $color = '';
76 if (CLI_SCRIPT) {
77 $color = '--ansi ';
78 }
79
7f541ea3
DM
80 $currentcwd = getcwd();
81 chdir($CFG->behatpath);
df8dcf3e 82 exec('bin/behat ' . $color . ' --config="' . self::get_behat_config_filepath() . '" ' . $filteroption, $steps, $code);
7f541ea3
DM
83 chdir($currentcwd);
84
85 // Outputing steps.
7f541ea3
DM
86
87 $content = '';
88 if ($steps) {
89 foreach ($steps as $line) {
90
91 // Skipping the step definition context.
92 if (strpos($line, '#') == 0) {
603c95dc
DM
93 if (CLI_SCRIPT) {
94 $content .= $line . PHP_EOL;
95 } else {
96 $content .= htmlentities($line) . '<br/>';
97 }
98
7f541ea3
DM
99 }
100 }
101 }
102
103 if ($content === '') {
104 $content = get_string('nostepsdefinitions', 'tool_behat');
105 }
106
603c95dc
DM
107 if (!CLI_SCRIPT) {
108 $html = self::get_header();
109 $html .= self::get_steps_definitions_form($filter);
110 $html .= html_writer::tag('div', $content, array('id' => 'steps-definitions'));
111 $html .= self::get_footer();
112 echo $html;
113 } else {
114 echo $content;
115 }
7f541ea3
DM
116 }
117
cc544646 118 /**
cca2c806
DM
119 * Allows / disables the test environment to be accesses through the built-in server
120 *
121 * Built-in server must be started separately
122 *
6d994c51 123 * @param string $testenvironment enable|disable
cc544646 124 */
6d994c51 125 public static function switchenvironment($testenvironment = false) {
cc544646
DM
126 global $CFG;
127
6d994c51
DM
128 // Priority to the one specified as argument.
129 if (!$testenvironment) {
130 $testenvironment = optional_param('testenvironment', 'enable', PARAM_ALPHA);
131 }
132
cc544646 133 if ($testenvironment == 'enable') {
cca2c806 134 self::start_test_mode();
cc544646 135 } else if ($testenvironment == 'disable') {
cca2c806 136 self::stop_test_mode();
cc544646 137 }
cc544646 138 }
7f541ea3 139
7f541ea3
DM
140 /**
141 * Runs the acceptance tests
cca2c806
DM
142 *
143 * It starts test mode and runs the built-in php
144 * CLI server and stops it all then it's done
145 *
6d994c51
DM
146 * @param string $tags Restricts the executed tests to the ones that matches the tags
147 * @param string $extra Extra CLI behat options
7f541ea3 148 */
25833ef8 149 public static function runtests($withjavascript = false, $tags = false, $extra = false) {
7f541ea3
DM
150 global $CFG;
151
cca2c806 152 // Checks that the behat reference is properly set up
33005f68 153 self::check_behat_setup();
7f541ea3 154
cca2c806
DM
155 // Check that PHPUnit test environment is correctly set up.
156 self::test_environment_problem();
157
8cdc0ce8
DM
158 self::update_config_file();
159
7f541ea3
DM
160 @set_time_limit(0);
161
25833ef8
DM
162 // No javascript by default
163 if (!$withjavascript && strstr($tags, 'javascript') == false) {
164 $jsstr = '~javascript';
6d994c51 165 }
6d994c51 166
25833ef8 167 // Adding javascript option to --tags.
7f541ea3 168 $tagsoption = '';
6d994c51 169 if ($tags) {
25833ef8
DM
170 if (!empty($jsstr)) {
171 $tags .= ',' . $jsstr;
172 }
7f541ea3 173 $tagsoption = ' --tags ' . $tags;
25833ef8
DM
174
175 // No javascript by default.
176 } else if (!empty($jsstr)) {
177 $tagsoption = ' --tags ' . $jsstr;
7f541ea3
DM
178 }
179
df8dcf3e 180 if (!$extra) {
6d994c51
DM
181 $extra = '';
182 }
183
cca2c806
DM
184 // Starts built-in server and inits test mode
185 self::start_test_mode();
186 $server = self::start_test_server();
7f541ea3 187
cca2c806
DM
188 // Runs the tests switching the current working directory to CFG->behatpath.
189 $currentcwd = getcwd();
7f541ea3 190 chdir($CFG->behatpath);
93245e95 191 ob_start();
df8dcf3e 192 passthru('bin/behat --ansi --config="' . self::get_behat_config_filepath() .'" ' . $tagsoption . ' ' .$extra, $code);
93245e95
DM
193 $output = ob_get_contents();
194 ob_end_clean();
7f541ea3 195 chdir($currentcwd);
cc544646 196
cca2c806
DM
197 // Stops built-in server and stops test mode
198 self::stop_test_server($server[0], $server[1]);
199 self::stop_test_mode();
200
93245e95
DM
201 // Output.
202 echo self::get_header();
93245e95 203 echo $output;
33005f68 204 echo self::get_footer();
7f541ea3
DM
205 }
206
8cdc0ce8
DM
207 /**
208 * Updates the config file
209 * @throws file_exception
210 */
211 private static function update_config_file() {
df8dcf3e 212 global $CFG;
8cdc0ce8 213
df8dcf3e
DM
214 $behatpath = rtrim($CFG->behatpath, '/');
215
216 // Basic behat dependencies.
217 $contents = 'default:
218 paths:
219 features: ' . $behatpath . '/features
220 bootstrap: ' . $behatpath . '/features/bootstrap
221 extensions:
222 Behat\MinkExtension\Extension:
cca2c806 223 base_url: ' . $CFG->test_wwwroot . '
df8dcf3e
DM
224 goutte: ~
225 selenium2: ~
226 Sanpi\Behatch\Extension:
227 contexts:
228 browser: ~
229 system: ~
230 json: ~
231 table: ~
df8dcf3e
DM
232 ' . $behatpath . '/vendor/moodlehq/behat-extension/init.php:
233';
93245e95
DM
234
235 // Gets all the components with features.
8cdc0ce8
DM
236 $components = tests_finder::get_components_with_tests('features');
237 if ($components) {
df8dcf3e 238 $featurespaths = array('');
8cdc0ce8 239 foreach ($components as $componentname => $path) {
93245e95 240 $path = self::clean_path($path) . self::$behat_tests_path;
603c95dc
DM
241 if (empty($featurespaths[$path]) && file_exists($path)) {
242 $featurespaths[$path] = $path;
243 }
8cdc0ce8 244 }
df8dcf3e 245 $contents .= ' features:' . implode(PHP_EOL . ' - ', $featurespaths) . PHP_EOL;
8cdc0ce8
DM
246 }
247
93245e95
DM
248 // Gets all the components with steps definitions.
249 $components = tests_finder::get_components_with_tests('stepsdefinitions');
250 if ($components) {
df8dcf3e
DM
251 $stepsdefinitions = array('');
252 foreach ($components as $componentname => $componentpath) {
253 $componentpath = self::clean_path($componentpath);
254 $diriterator = new DirectoryIterator($componentpath . self::$behat_tests_path);
255 $regite = new RegexIterator($diriterator, '|behat_.*\.php$|');
256
257 // All behat_*.php inside self::$behat_tests_path are added as steps definitions files
258 foreach ($regite as $file) {
259 $key = $file->getBasename('.php');
260 $stepsdefinitions[$key] = $key . ': ' . $file->getPathname();
261 }
93245e95 262 }
df8dcf3e 263 $contents .= ' steps_definitions:' . implode(PHP_EOL . ' ', $stepsdefinitions) . PHP_EOL;
93245e95 264 }
8cdc0ce8 265
93245e95 266 // Stores the file.
df8dcf3e
DM
267 if (!file_put_contents(self::get_behat_config_filepath(), $contents)) {
268 throw new file_exception('cannotcreatefile', self::get_behat_config_filepath());
8cdc0ce8 269 }
603c95dc 270
8cdc0ce8 271 }
7f541ea3 272
93245e95
DM
273
274 /**
275 * Cleans the path returned by get_components_with_tests() to standarize it
276 *
277 * {@see tests_finder::get_all_directories_with_tests()} it returns the path including /tests/
278 * @param string $path
279 * @return string The string without the last /tests part
280 */
281 private static function clean_path($path) {
282
283 $path = rtrim($path, '/');
284
285 $parttoremove = '/tests';
286
287 $substr = substr($path, strlen($path) - strlen($parttoremove));
288 if ($substr == $parttoremove) {
289 $path = substr($path, 0, strlen($path) - strlen($parttoremove));
290 }
291
292 return rtrim($path, '/');
293 }
294
7f541ea3
DM
295 /**
296 * Checks whether the test database and dataroot is ready
297 * Stops execution if something went wrong
298 */
299 private static function test_environment_problem() {
300 global $CFG;
301
302 // phpunit --diag returns nothing if the test environment is set up correctly.
303 $currentcwd = getcwd();
304 chdir($CFG->dirroot . '/' . $CFG->admin . '/tool/phpunit/cli');
305 exec("php util.php --diag", $output, $code);
306 chdir($currentcwd);
307
308 // If something is not ready stop execution and display the CLI command output.
309 if ($code != 0) {
310 notice(implode(' ', $output));
311 }
312 }
313
314 /**
cca2c806
DM
315 * Checks if behat is set up and working
316 *
317 * It checks the behatpath setting value and runs the
318 * behat help command to ensure it works as expected
7f541ea3
DM
319 */
320 private static function check_behat_setup() {
321 global $CFG;
322
323 // Moodle setting.
324 if (empty($CFG->behatpath)) {
325 $msg = get_string('nobehatpath', 'tool_behat');
326 $url = $CFG->wwwroot . '/' . $CFG->admin . '/settings.php?section=systempaths';
327
328 if (!CLI_SCRIPT) {
329 $msg .= ' ' . html_writer::tag('a', get_string('systempaths', 'admin'), array('href' => $url));
330 }
331 notice($msg);
332 }
333
334 // Behat test command.
335 $currentcwd = getcwd();
336 chdir($CFG->behatpath);
337 exec('bin/behat --help', $output, $code);
338 chdir($currentcwd);
339
340 if ($code != 0) {
341 notice(get_string('wrongbehatsetup', 'tool_behat'));
342 }
343 }
344
345 /**
cca2c806 346 * Enables test mode
7f541ea3 347 *
cca2c806
DM
348 * Stores a file in dataroot/behat to
349 * allow Moodle to switch to the test
350 * database and dataroot before the initial setup
7f541ea3
DM
351 *
352 * @throws file_exception
cca2c806 353 * @return array
7f541ea3 354 */
cca2c806 355 private static function start_test_mode() {
554820dd 356 global $CFG;
7f541ea3 357
cca2c806 358 if (self::is_test_mode_enabled()) {
7f541ea3
DM
359 debugging('Test environment was already enabled');
360 return;
361 }
362
8cdc0ce8 363 $behatdir = self::get_behat_dir();
7f541ea3 364
33005f68 365 $contents = '$CFG->phpunit_prefix and $CFG->phpunit_dataroot are currently used as $CFG->prefix and $CFG->dataroot';
7f541ea3 366 $filepath = $behatdir . '/test_environment_enabled.txt';
33005f68 367 if (!file_put_contents($filepath, $contents)) {
7f541ea3
DM
368 throw new file_exception('cannotcreatefile', $filepath);
369 }
554820dd 370 chmod($filepath, $CFG->directorypermissions);
7f541ea3
DM
371 }
372
cca2c806
DM
373 /**
374 * Runs the php built-in server
375 * @return array The process running the server and the pipes array
376 */
377 private static function start_test_server() {
378 global $CFG;
379
380 $descriptorspec = array(
381 array("pipe", "r"),
382 array("pipe", "w"),
383 array("pipe", "a")
384 );
385
386 $server = str_replace('http://', '', $CFG->test_wwwroot);
387 $process = proc_open('php -S ' . $server, $descriptorspec, $pipes, $CFG->dirroot);
388
389 if (!is_resource($process)) {
390 print_error('testservercantrun');
391 }
392
393 return array($process, $pipes);
394 }
395
7f541ea3
DM
396 /**
397 * Disables test mode
398 */
cca2c806 399 private static function stop_test_mode() {
7f541ea3 400
33005f68 401 $testenvfile = self::get_test_filepath();
7f541ea3 402
cca2c806 403 if (!self::is_test_mode_enabled()) {
7f541ea3
DM
404 debugging('Test environment was already disabled');
405 } else {
cc544646
DM
406 if (!unlink($testenvfile)) {
407 throw new file_exception('cannotdeletetestenvironmentfile');
408 }
7f541ea3
DM
409 }
410 }
411
cca2c806
DM
412 /**
413 * Stops the built-in server
414 *
415 * @param resource $process
416 * @param array $pipes IN, OUT and error pipes
417 */
418 private static function stop_test_server($process, $pipes) {
419
420 if (is_resource($process)) {
421
422 // Closing pipes.
423 fclose($pipes[0]);
424 fclose($pipes[1]);
425 fclose($pipes[2]);
426
427 // Closing process.
428 proc_terminate($process);
429 proc_close($process);
430 }
431 }
432
7f541ea3
DM
433 /**
434 * Checks whether test environment is enabled or disabled
cc544646
DM
435 *
436 * It does not return if the current script is running
437 * in test environment {@see tool_behat::is_test_environment_running()}
438 *
439 * @return bool
7f541ea3 440 */
cca2c806 441 private static function is_test_mode_enabled() {
7f541ea3 442
33005f68 443 $testenvfile = self::get_test_filepath();
7f541ea3
DM
444 if (file_exists($testenvfile)) {
445 return true;
446 }
447
448 return false;
449 }
450
cc544646
DM
451 /**
452 * Returns true if Moodle is currently running with the test database and dataroot
453 * @return bool
454 */
554820dd 455 private static function is_test_environment_running() {
cc544646
DM
456 global $CFG;
457
cca2c806 458 if (!empty($CFG->originaldataroot) && php_sapi_name() === 'cli-server') {
cc544646
DM
459 return true;
460 }
461
462 return false;
463 }
464
465 /**
466 * Returns the path to the file which specifies if test environment is enabled
467 * @return string
468 */
469 private static function get_test_filepath() {
470 global $CFG;
471
33005f68 472 if (self::is_test_environment_running()) {
cc544646
DM
473 $testenvfile = $CFG->originaldataroot . '/behat/test_environment_enabled.txt';
474 } else {
475 $testenvfile = $CFG->dataroot . '/behat/test_environment_enabled.txt';
476 }
477
478 return $testenvfile;
479 }
480
8cdc0ce8
DM
481
482 /**
483 * Ensures the behat dir exists in moodledata
484 * @throws file_exception
485 * @return string Full path
486 */
487 private static function get_behat_dir() {
488 global $CFG;
489
490 $behatdir = $CFG->dataroot . '/behat';
491
492 if (!is_dir($behatdir)) {
493 if (!mkdir($behatdir, $CFG->directorypermissions, true)) {
494 throw new file_exception('storedfilecannotcreatefiledirs');
495 }
496 }
497
498 if (!is_writable($behatdir)) {
499 throw new file_exception('storedfilecannotcreatefiledirs');
500 }
501
502 return $behatdir;
503 }
504
df8dcf3e
DM
505 /**
506 * Returns the behat config file path
507 * @return string
508 */
509 private static function get_behat_config_filepath() {
510 return self::get_behat_dir() . '/behat.yml';
511 }
512
cc544646
DM
513 /**
514 * Returns header output
515 * @return string
516 */
517 private static function get_header() {
518 global $OUTPUT;
519
33005f68
DM
520 $action = optional_param('action', 'info', PARAM_ALPHAEXT);
521
cc544646 522 if (CLI_SCRIPT) {
33005f68 523 return '';
cc544646
DM
524 }
525
cc544646
DM
526 $title = get_string('pluginname', 'tool_behat') . ' - ' . get_string('command' . $action, 'tool_behat');
527 $html = $OUTPUT->header();
528 $html .= $OUTPUT->heading($title);
529
530 return $html;
531 }
532
533 /**
534 * Returns footer output
535 * @return string
536 */
537 private static function get_footer() {
538 global $OUTPUT;
539
540 if (CLI_SCRIPT) {
541 return '';
542 }
543
544 return $OUTPUT->footer();
545 }
546
33005f68
DM
547 /**
548 * Returns a message and a button to continue if web execution
549 * @param string $html
550 * @param string $url
551 * @return string
552 */
553 private static function output_success($html, $url = false) {
554 global $CFG, $OUTPUT;
555
556 if (!$url) {
557 $url = $CFG->wwwroot . '/' . $CFG->admin . '/tool/behat/index.php';
558 }
559
560 if (!CLI_SCRIPT) {
561 $html = $OUTPUT->box($html, 'generalbox', 'notice');
562 $html .= $OUTPUT->continue_button($url);
563 }
564
565 return $html;
566 }
567
7f541ea3
DM
568 /**
569 * Returns the installation instructions
570 *
571 * (hardcoded in English)
572 *
573 * @return string
574 */
575 private static function get_info() {
576 global $OUTPUT;
577
603c95dc 578 $url = 'http://docs.moodle.org/dev/Acceptance_testing';
cc544646
DM
579
580 $html = $OUTPUT->box_start();
603c95dc
DM
581 $html .= html_writer::tag('h1', 'Info');
582 $html .= html_writer::tag('div', 'Follow <a href="' . $url . '" target="_blank">' . $url . '</a> instructions for info about installation and tests execution');
cc544646
DM
583 $html .= $OUTPUT->box_end();
584
585 return $html;
586 }
587
7f541ea3
DM
588 /**
589 * Returns the steps definitions form
590 * @param string $filter To filter the steps definitions list by keyword
591 * @return string
592 */
593 private static function get_steps_definitions_form($filter = false) {
594 global $OUTPUT;
595
596 if ($filter === false) {
597 $filter = '';
598 } else {
599 $filter = s($filter);
600 }
601
602 $html = $OUTPUT->box_start();
603 $html .= '<form method="get" action="index.php">';
7f541ea3 604 $html .= '<fieldset class="invisiblefieldset">';
df8dcf3e
DM
605 $html .= '<label for="id_filter">' . get_string('stepsdefinitions', 'tool_behat') . '</label> ';
606 $html .= '<input type="text" id="id_filter" value="' . $filter . '" name="filter"/> (' . get_string('stepsdefinitionsemptyfilter', 'tool_behat') . ')';
33005f68 607 $html .= '<p></p>';
df8dcf3e 608 $html .= '<input type="submit" value="' . get_string('viewsteps', 'tool_behat') . '" />';
7f541ea3
DM
609 $html .= '<input type="hidden" name="action" value="stepsdefinitions" />';
610 $html .= '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
611 $html .= '</fieldset>';
612 $html .= '</form>';
613 $html .= $OUTPUT->box_end();
614
615 return $html;
616 }
617
7f541ea3 618}