MDL-37046 behat: Restricting the execution to the built-in server
[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 */
6d994c51 149 public static function runtests($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
6d994c51
DM
162 // Priority to the one specified as argument.
163 if (!$tags) {
164 $tags = optional_param('tags', false, PARAM_ALPHANUMEXT);
165 }
6d994c51 166
7f541ea3 167 $tagsoption = '';
6d994c51 168 if ($tags) {
7f541ea3
DM
169 $tagsoption = ' --tags ' . $tags;
170 }
171
df8dcf3e 172 if (!$extra) {
6d994c51
DM
173 $extra = '';
174 }
175
cca2c806
DM
176 // Starts built-in server and inits test mode
177 self::start_test_mode();
178 $server = self::start_test_server();
7f541ea3 179
cca2c806
DM
180 // Runs the tests switching the current working directory to CFG->behatpath.
181 $currentcwd = getcwd();
7f541ea3 182 chdir($CFG->behatpath);
93245e95 183 ob_start();
df8dcf3e 184 passthru('bin/behat --ansi --config="' . self::get_behat_config_filepath() .'" ' . $tagsoption . ' ' .$extra, $code);
93245e95
DM
185 $output = ob_get_contents();
186 ob_end_clean();
7f541ea3 187 chdir($currentcwd);
cc544646 188
cca2c806
DM
189 // Stops built-in server and stops test mode
190 self::stop_test_server($server[0], $server[1]);
191 self::stop_test_mode();
192
93245e95
DM
193 // Output.
194 echo self::get_header();
93245e95 195 echo $output;
33005f68 196 echo self::get_footer();
7f541ea3
DM
197 }
198
8cdc0ce8
DM
199 /**
200 * Updates the config file
201 * @throws file_exception
202 */
203 private static function update_config_file() {
df8dcf3e 204 global $CFG;
8cdc0ce8 205
df8dcf3e
DM
206 $behatpath = rtrim($CFG->behatpath, '/');
207
208 // Basic behat dependencies.
209 $contents = 'default:
210 paths:
211 features: ' . $behatpath . '/features
212 bootstrap: ' . $behatpath . '/features/bootstrap
213 extensions:
214 Behat\MinkExtension\Extension:
cca2c806 215 base_url: ' . $CFG->test_wwwroot . '
df8dcf3e
DM
216 goutte: ~
217 selenium2: ~
218 Sanpi\Behatch\Extension:
219 contexts:
220 browser: ~
221 system: ~
222 json: ~
223 table: ~
df8dcf3e
DM
224 ' . $behatpath . '/vendor/moodlehq/behat-extension/init.php:
225';
93245e95
DM
226
227 // Gets all the components with features.
8cdc0ce8
DM
228 $components = tests_finder::get_components_with_tests('features');
229 if ($components) {
df8dcf3e 230 $featurespaths = array('');
8cdc0ce8 231 foreach ($components as $componentname => $path) {
93245e95 232 $path = self::clean_path($path) . self::$behat_tests_path;
603c95dc
DM
233 if (empty($featurespaths[$path]) && file_exists($path)) {
234 $featurespaths[$path] = $path;
235 }
8cdc0ce8 236 }
df8dcf3e 237 $contents .= ' features:' . implode(PHP_EOL . ' - ', $featurespaths) . PHP_EOL;
8cdc0ce8
DM
238 }
239
93245e95
DM
240 // Gets all the components with steps definitions.
241 $components = tests_finder::get_components_with_tests('stepsdefinitions');
242 if ($components) {
df8dcf3e
DM
243 $stepsdefinitions = array('');
244 foreach ($components as $componentname => $componentpath) {
245 $componentpath = self::clean_path($componentpath);
246 $diriterator = new DirectoryIterator($componentpath . self::$behat_tests_path);
247 $regite = new RegexIterator($diriterator, '|behat_.*\.php$|');
248
249 // All behat_*.php inside self::$behat_tests_path are added as steps definitions files
250 foreach ($regite as $file) {
251 $key = $file->getBasename('.php');
252 $stepsdefinitions[$key] = $key . ': ' . $file->getPathname();
253 }
93245e95 254 }
df8dcf3e 255 $contents .= ' steps_definitions:' . implode(PHP_EOL . ' ', $stepsdefinitions) . PHP_EOL;
93245e95 256 }
8cdc0ce8 257
93245e95 258 // Stores the file.
df8dcf3e
DM
259 if (!file_put_contents(self::get_behat_config_filepath(), $contents)) {
260 throw new file_exception('cannotcreatefile', self::get_behat_config_filepath());
8cdc0ce8 261 }
603c95dc 262
8cdc0ce8 263 }
7f541ea3 264
93245e95
DM
265
266 /**
267 * Cleans the path returned by get_components_with_tests() to standarize it
268 *
269 * {@see tests_finder::get_all_directories_with_tests()} it returns the path including /tests/
270 * @param string $path
271 * @return string The string without the last /tests part
272 */
273 private static function clean_path($path) {
274
275 $path = rtrim($path, '/');
276
277 $parttoremove = '/tests';
278
279 $substr = substr($path, strlen($path) - strlen($parttoremove));
280 if ($substr == $parttoremove) {
281 $path = substr($path, 0, strlen($path) - strlen($parttoremove));
282 }
283
284 return rtrim($path, '/');
285 }
286
7f541ea3
DM
287 /**
288 * Checks whether the test database and dataroot is ready
289 * Stops execution if something went wrong
290 */
291 private static function test_environment_problem() {
292 global $CFG;
293
294 // phpunit --diag returns nothing if the test environment is set up correctly.
295 $currentcwd = getcwd();
296 chdir($CFG->dirroot . '/' . $CFG->admin . '/tool/phpunit/cli');
297 exec("php util.php --diag", $output, $code);
298 chdir($currentcwd);
299
300 // If something is not ready stop execution and display the CLI command output.
301 if ($code != 0) {
302 notice(implode(' ', $output));
303 }
304 }
305
306 /**
cca2c806
DM
307 * Checks if behat is set up and working
308 *
309 * It checks the behatpath setting value and runs the
310 * behat help command to ensure it works as expected
7f541ea3
DM
311 */
312 private static function check_behat_setup() {
313 global $CFG;
314
315 // Moodle setting.
316 if (empty($CFG->behatpath)) {
317 $msg = get_string('nobehatpath', 'tool_behat');
318 $url = $CFG->wwwroot . '/' . $CFG->admin . '/settings.php?section=systempaths';
319
320 if (!CLI_SCRIPT) {
321 $msg .= ' ' . html_writer::tag('a', get_string('systempaths', 'admin'), array('href' => $url));
322 }
323 notice($msg);
324 }
325
326 // Behat test command.
327 $currentcwd = getcwd();
328 chdir($CFG->behatpath);
329 exec('bin/behat --help', $output, $code);
330 chdir($currentcwd);
331
332 if ($code != 0) {
333 notice(get_string('wrongbehatsetup', 'tool_behat'));
334 }
335 }
336
337 /**
cca2c806 338 * Enables test mode
7f541ea3 339 *
cca2c806
DM
340 * Stores a file in dataroot/behat to
341 * allow Moodle to switch to the test
342 * database and dataroot before the initial setup
7f541ea3
DM
343 *
344 * @throws file_exception
cca2c806 345 * @return array
7f541ea3 346 */
cca2c806 347 private static function start_test_mode() {
554820dd 348 global $CFG;
7f541ea3 349
cca2c806 350 if (self::is_test_mode_enabled()) {
7f541ea3
DM
351 debugging('Test environment was already enabled');
352 return;
353 }
354
8cdc0ce8 355 $behatdir = self::get_behat_dir();
7f541ea3 356
33005f68 357 $contents = '$CFG->phpunit_prefix and $CFG->phpunit_dataroot are currently used as $CFG->prefix and $CFG->dataroot';
7f541ea3 358 $filepath = $behatdir . '/test_environment_enabled.txt';
33005f68 359 if (!file_put_contents($filepath, $contents)) {
7f541ea3
DM
360 throw new file_exception('cannotcreatefile', $filepath);
361 }
554820dd 362 chmod($filepath, $CFG->directorypermissions);
7f541ea3
DM
363 }
364
cca2c806
DM
365 /**
366 * Runs the php built-in server
367 * @return array The process running the server and the pipes array
368 */
369 private static function start_test_server() {
370 global $CFG;
371
372 $descriptorspec = array(
373 array("pipe", "r"),
374 array("pipe", "w"),
375 array("pipe", "a")
376 );
377
378 $server = str_replace('http://', '', $CFG->test_wwwroot);
379 $process = proc_open('php -S ' . $server, $descriptorspec, $pipes, $CFG->dirroot);
380
381 if (!is_resource($process)) {
382 print_error('testservercantrun');
383 }
384
385 return array($process, $pipes);
386 }
387
7f541ea3
DM
388 /**
389 * Disables test mode
390 */
cca2c806 391 private static function stop_test_mode() {
7f541ea3 392
33005f68 393 $testenvfile = self::get_test_filepath();
7f541ea3 394
cca2c806 395 if (!self::is_test_mode_enabled()) {
7f541ea3
DM
396 debugging('Test environment was already disabled');
397 } else {
cc544646
DM
398 if (!unlink($testenvfile)) {
399 throw new file_exception('cannotdeletetestenvironmentfile');
400 }
7f541ea3
DM
401 }
402 }
403
cca2c806
DM
404 /**
405 * Stops the built-in server
406 *
407 * @param resource $process
408 * @param array $pipes IN, OUT and error pipes
409 */
410 private static function stop_test_server($process, $pipes) {
411
412 if (is_resource($process)) {
413
414 // Closing pipes.
415 fclose($pipes[0]);
416 fclose($pipes[1]);
417 fclose($pipes[2]);
418
419 // Closing process.
420 proc_terminate($process);
421 proc_close($process);
422 }
423 }
424
7f541ea3
DM
425 /**
426 * Checks whether test environment is enabled or disabled
cc544646
DM
427 *
428 * It does not return if the current script is running
429 * in test environment {@see tool_behat::is_test_environment_running()}
430 *
431 * @return bool
7f541ea3 432 */
cca2c806 433 private static function is_test_mode_enabled() {
7f541ea3 434
33005f68 435 $testenvfile = self::get_test_filepath();
7f541ea3
DM
436 if (file_exists($testenvfile)) {
437 return true;
438 }
439
440 return false;
441 }
442
cc544646
DM
443 /**
444 * Returns true if Moodle is currently running with the test database and dataroot
445 * @return bool
446 */
554820dd 447 private static function is_test_environment_running() {
cc544646
DM
448 global $CFG;
449
cca2c806 450 if (!empty($CFG->originaldataroot) && php_sapi_name() === 'cli-server') {
cc544646
DM
451 return true;
452 }
453
454 return false;
455 }
456
457 /**
458 * Returns the path to the file which specifies if test environment is enabled
459 * @return string
460 */
461 private static function get_test_filepath() {
462 global $CFG;
463
33005f68 464 if (self::is_test_environment_running()) {
cc544646
DM
465 $testenvfile = $CFG->originaldataroot . '/behat/test_environment_enabled.txt';
466 } else {
467 $testenvfile = $CFG->dataroot . '/behat/test_environment_enabled.txt';
468 }
469
470 return $testenvfile;
471 }
472
8cdc0ce8
DM
473
474 /**
475 * Ensures the behat dir exists in moodledata
476 * @throws file_exception
477 * @return string Full path
478 */
479 private static function get_behat_dir() {
480 global $CFG;
481
482 $behatdir = $CFG->dataroot . '/behat';
483
484 if (!is_dir($behatdir)) {
485 if (!mkdir($behatdir, $CFG->directorypermissions, true)) {
486 throw new file_exception('storedfilecannotcreatefiledirs');
487 }
488 }
489
490 if (!is_writable($behatdir)) {
491 throw new file_exception('storedfilecannotcreatefiledirs');
492 }
493
494 return $behatdir;
495 }
496
df8dcf3e
DM
497 /**
498 * Returns the behat config file path
499 * @return string
500 */
501 private static function get_behat_config_filepath() {
502 return self::get_behat_dir() . '/behat.yml';
503 }
504
cc544646
DM
505 /**
506 * Returns header output
507 * @return string
508 */
509 private static function get_header() {
510 global $OUTPUT;
511
33005f68
DM
512 $action = optional_param('action', 'info', PARAM_ALPHAEXT);
513
cc544646 514 if (CLI_SCRIPT) {
33005f68 515 return '';
cc544646
DM
516 }
517
cc544646
DM
518 $title = get_string('pluginname', 'tool_behat') . ' - ' . get_string('command' . $action, 'tool_behat');
519 $html = $OUTPUT->header();
520 $html .= $OUTPUT->heading($title);
521
522 return $html;
523 }
524
525 /**
526 * Returns footer output
527 * @return string
528 */
529 private static function get_footer() {
530 global $OUTPUT;
531
532 if (CLI_SCRIPT) {
533 return '';
534 }
535
536 return $OUTPUT->footer();
537 }
538
33005f68
DM
539 /**
540 * Returns a message and a button to continue if web execution
541 * @param string $html
542 * @param string $url
543 * @return string
544 */
545 private static function output_success($html, $url = false) {
546 global $CFG, $OUTPUT;
547
548 if (!$url) {
549 $url = $CFG->wwwroot . '/' . $CFG->admin . '/tool/behat/index.php';
550 }
551
552 if (!CLI_SCRIPT) {
553 $html = $OUTPUT->box($html, 'generalbox', 'notice');
554 $html .= $OUTPUT->continue_button($url);
555 }
556
557 return $html;
558 }
559
7f541ea3
DM
560 /**
561 * Returns the installation instructions
562 *
563 * (hardcoded in English)
564 *
565 * @return string
566 */
567 private static function get_info() {
568 global $OUTPUT;
569
603c95dc 570 $url = 'http://docs.moodle.org/dev/Acceptance_testing';
cc544646
DM
571
572 $html = $OUTPUT->box_start();
603c95dc
DM
573 $html .= html_writer::tag('h1', 'Info');
574 $html .= html_writer::tag('div', 'Follow <a href="' . $url . '" target="_blank">' . $url . '</a> instructions for info about installation and tests execution');
cc544646
DM
575 $html .= $OUTPUT->box_end();
576
577 return $html;
578 }
579
7f541ea3
DM
580 /**
581 * Returns the steps definitions form
582 * @param string $filter To filter the steps definitions list by keyword
583 * @return string
584 */
585 private static function get_steps_definitions_form($filter = false) {
586 global $OUTPUT;
587
588 if ($filter === false) {
589 $filter = '';
590 } else {
591 $filter = s($filter);
592 }
593
594 $html = $OUTPUT->box_start();
595 $html .= '<form method="get" action="index.php">';
7f541ea3 596 $html .= '<fieldset class="invisiblefieldset">';
df8dcf3e
DM
597 $html .= '<label for="id_filter">' . get_string('stepsdefinitions', 'tool_behat') . '</label> ';
598 $html .= '<input type="text" id="id_filter" value="' . $filter . '" name="filter"/> (' . get_string('stepsdefinitionsemptyfilter', 'tool_behat') . ')';
33005f68 599 $html .= '<p></p>';
df8dcf3e 600 $html .= '<input type="submit" value="' . get_string('viewsteps', 'tool_behat') . '" />';
7f541ea3
DM
601 $html .= '<input type="hidden" name="action" value="stepsdefinitions" />';
602 $html .= '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
603 $html .= '</fieldset>';
604 $html .= '</form>';
605 $html .= $OUTPUT->box_end();
606
607 return $html;
608 }
609
7f541ea3 610}