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/>.
18 * Behat basic functions
20 * It does not include MOODLE_INTERNAL because is part of the bootstrap.
22 * This script should not be usually included, neither any of its functions
23 * used, within mooodle code at all. It's for exclusive use of behat and
24 * moodle setup.php. For places requiring a different/special behavior
25 * needing to check if are being run as part of behat tests, use:
26 * if (defined('BEHAT_SITE_RUNNING')) { ...
30 * @copyright 2012 David MonllaĆ³
31 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 require_once(__DIR__
. '/../testing/lib.php');
36 define('BEHAT_EXITCODE_CONFIG', 250);
37 define('BEHAT_EXITCODE_REQUIREMENT', 251);
38 define('BEHAT_EXITCODE_PERMISSIONS', 252);
39 define('BEHAT_EXITCODE_REINSTALL', 253);
40 define('BEHAT_EXITCODE_INSTALL', 254);
41 define('BEHAT_EXITCODE_INSTALLED', 256);
44 * The behat test site fullname and shortname.
46 define('BEHAT_PARALLEL_SITE_NAME', "behatrun");
49 * Exits with an error code
51 * @param mixed $errorcode
53 * @return void Stops execution with error code
55 function behat_error($errorcode, $text = '') {
57 // Adding error prefixes.
59 case BEHAT_EXITCODE_CONFIG
:
60 $text = 'Behat config error: ' . $text;
62 case BEHAT_EXITCODE_REQUIREMENT
:
63 $text = 'Behat requirement not satisfied: ' . $text;
65 case BEHAT_EXITCODE_PERMISSIONS
:
66 $text = 'Behat permissions problem: ' . $text . ', check the permissions';
68 case BEHAT_EXITCODE_REINSTALL
:
69 $path = testing_cli_argument_path('/admin/tool/behat/cli/init.php');
70 $text = "Reinstall Behat: ".$text.", use:\n php ".$path;
72 case BEHAT_EXITCODE_INSTALL
:
73 $path = testing_cli_argument_path('/admin/tool/behat/cli/init.php');
74 $text = "Install Behat before enabling it, use:\n php ".$path;
76 case BEHAT_EXITCODE_INSTALLED
:
77 $text = "The Behat site is already installed";
80 $text = 'Unknown error ' . $errorcode . ' ' . $text;
84 testing_error($errorcode, $text);
88 * Return logical error string.
90 * @param int $errtype php error type.
91 * @return string string which will be returned.
93 function behat_get_error_string($errtype) {
96 $errnostr = 'Fatal error';
100 $errnostr = 'Warning';
105 $errnostr = 'Notice';
107 case E_RECOVERABLE_ERROR
:
108 $errnostr = 'Catchable';
111 $errnostr = 'Unknown error type';
118 * PHP errors handler to use when running behat tests.
120 * Adds specific CSS classes to identify
124 * @param string $errstr
125 * @param string $errfile
126 * @param int $errline
129 function behat_error_handler($errno, $errstr, $errfile, $errline) {
131 // If is preceded by an @ we don't show it.
132 if (!error_reporting()) {
136 // This error handler receives E_ALL | E_STRICT, running the behat test site the debug level is
137 // set to DEVELOPER and will always include E_NOTICE,E_USER_NOTICE... as part of E_ALL, if the current
138 // error_reporting() value does not include one of those levels is because it has been forced through
139 // the moodle code (see fix_utf8() for example) in that cases we respect the forced error level value.
140 $respect = array(E_NOTICE
, E_USER_NOTICE
, E_STRICT
, E_WARNING
, E_USER_WARNING
, E_DEPRECATED
, E_USER_DEPRECATED
);
141 foreach ($respect as $respectable) {
143 // If the current value does not include this kind of errors and the reported error is
144 // at that level don't print anything.
145 if ($errno == $respectable && !(error_reporting() & $respectable)) {
150 // Using the default one in case there is a fatal catchable error.
151 default_error_handler($errno, $errstr, $errfile, $errline);
153 $errnostr = behat_get_error_string($errno);
155 // If ajax script then throw exception, so the calling api catch it and show it on web page.
156 if (defined('AJAX_SCRIPT')) {
157 throw new Exception("$errnostr: $errstr in $errfile on line $errline");
159 // Wrapping the output.
160 echo '<div class="phpdebugmessage" data-rel="phpdebugmessage">' . PHP_EOL
;
161 echo "$errnostr: $errstr in $errfile on line $errline" . PHP_EOL
;
165 // Also use the internal error handler so we keep the usual behaviour.
170 * Before shutdown save last error entries, so we can fail the test.
172 function behat_shutdown_function() {
173 // If any error found, then save it.
174 if ($error = error_get_last()) {
175 // Ignore E_WARNING, as they might come via ( @ )suppression and might lead to false failure.
176 if (isset($error['type']) && !($error['type'] & E_WARNING
)) {
178 $errors = behat_get_shutdown_process_errors();
181 $errorstosave = json_encode($errors);
183 set_config('process_errors', $errorstosave, 'tool_behat');
189 * Return php errors save which were save during shutdown.
193 function behat_get_shutdown_process_errors() {
196 // Don't use get_config, as it use cache and return invalid value, between selenium and cli process.
197 $phperrors = $DB->get_field('config_plugins', 'value', array('name' => 'process_errors', 'plugin' => 'tool_behat'));
199 if (!empty($phperrors)) {
200 return json_decode($phperrors, true);
207 * Restrict the config.php settings allowed.
209 * When running the behat features the config.php
210 * settings should not affect the results.
214 function behat_clean_init_config() {
217 $allowed = array_flip(array(
218 'wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions',
219 'umaskpermissions', 'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix',
220 'dboptions', 'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword',
221 'proxybypass', 'pathtogs', 'pathtophp', 'pathtodu', 'aspellpath', 'pathtodot', 'skiplangupgrade',
222 'altcacheconfigpath', 'pathtounoconv', 'alternative_file_system_class', 'pathtopython'
225 // Add extra allowed settings.
226 if (!empty($CFG->behat_extraallowedsettings
)) {
227 $allowed = array_merge($allowed, array_flip($CFG->behat_extraallowedsettings
));
230 // Also allowing behat_ prefixed attributes.
231 foreach ($CFG as $key => $value) {
232 if (!isset($allowed[$key]) && strpos($key, 'behat_') !== 0) {
239 * Checks that the behat config vars are properly set.
241 * @return void Stops execution with error code if something goes wrong.
243 function behat_check_config_vars() {
246 $moodleprefix = empty($CFG->prefix
) ?
'' : $CFG->prefix
;
247 $behatprefix = empty($CFG->behat_prefix
) ?
'' : $CFG->behat_prefix
;
248 $phpunitprefix = empty($CFG->phpunit_prefix
) ?
'' : $CFG->phpunit_prefix
;
249 $behatdbname = empty($CFG->behat_dbname
) ?
$CFG->dbname
: $CFG->behat_dbname
;
250 $phpunitdbname = empty($CFG->phpunit_dbname
) ?
$CFG->dbname
: $CFG->phpunit_dbname
;
251 $behatdbhost = empty($CFG->behat_dbhost
) ?
$CFG->dbhost
: $CFG->behat_dbhost
;
252 $phpunitdbhost = empty($CFG->phpunit_dbhost
) ?
$CFG->dbhost
: $CFG->phpunit_dbhost
;
254 // Verify prefix value.
255 if (empty($CFG->behat_prefix
)) {
256 behat_error(BEHAT_EXITCODE_CONFIG
,
257 'Define $CFG->behat_prefix in config.php');
259 if ($behatprefix == $moodleprefix && $behatdbname == $CFG->dbname
&& $behatdbhost == $CFG->dbhost
) {
260 behat_error(BEHAT_EXITCODE_CONFIG
,
261 '$CFG->behat_prefix in config.php must be different from $CFG->prefix' .
262 ' when $CFG->behat_dbname and $CFG->behat_host are not set or when $CFG->behat_dbname equals $CFG->dbname' .
263 ' and $CFG->behat_dbhost equals $CFG->dbhost');
265 if ($phpunitprefix !== '' && $behatprefix == $phpunitprefix && $behatdbname == $phpunitdbname &&
266 $behatdbhost == $phpunitdbhost) {
267 behat_error(BEHAT_EXITCODE_CONFIG
,
268 '$CFG->behat_prefix in config.php must be different from $CFG->phpunit_prefix' .
269 ' when $CFG->behat_dbname equals $CFG->phpunit_dbname' .
270 ' and $CFG->behat_dbhost equals $CFG->phpunit_dbhost');
273 // Verify behat wwwroot value.
274 if (empty($CFG->behat_wwwroot
)) {
275 behat_error(BEHAT_EXITCODE_CONFIG
,
276 'Define $CFG->behat_wwwroot in config.php');
278 if (!empty($CFG->wwwroot
) and $CFG->behat_wwwroot
== $CFG->wwwroot
) {
279 behat_error(BEHAT_EXITCODE_CONFIG
,
280 '$CFG->behat_wwwroot in config.php must be different from $CFG->wwwroot');
283 // Verify behat dataroot value.
284 if (empty($CFG->behat_dataroot
)) {
285 behat_error(BEHAT_EXITCODE_CONFIG
,
286 'Define $CFG->behat_dataroot in config.php');
289 if (!file_exists($CFG->behat_dataroot_parent
)) {
290 $permissions = isset($CFG->directorypermissions
) ?
$CFG->directorypermissions
: 02777;
292 if (!mkdir($CFG->behat_dataroot_parent
, $permissions, true)) {
293 behat_error(BEHAT_EXITCODE_PERMISSIONS
, '$CFG->behat_dataroot directory can not be created');
296 $CFG->behat_dataroot_parent
= realpath($CFG->behat_dataroot_parent
);
297 if (empty($CFG->behat_dataroot_parent
) or !is_dir($CFG->behat_dataroot_parent
) or !is_writable($CFG->behat_dataroot_parent
)) {
298 behat_error(BEHAT_EXITCODE_CONFIG
,
299 '$CFG->behat_dataroot in config.php must point to an existing writable directory');
301 if (!empty($CFG->dataroot
) and $CFG->behat_dataroot_parent
== realpath($CFG->dataroot
)) {
302 behat_error(BEHAT_EXITCODE_CONFIG
,
303 '$CFG->behat_dataroot in config.php must be different from $CFG->dataroot');
305 if (!empty($CFG->phpunit_dataroot
) and $CFG->behat_dataroot_parent
== realpath($CFG->phpunit_dataroot
)) {
306 behat_error(BEHAT_EXITCODE_CONFIG
,
307 '$CFG->behat_dataroot in config.php must be different from $CFG->phpunit_dataroot');
310 // This request is coming from admin/tool/behat/cli/util.php which will call util_single.php. So just return from
311 // here as we don't need to create a dataroot for single run.
312 if (defined('BEHAT_PARALLEL_UTIL') && BEHAT_PARALLEL_UTIL
&& empty($CFG->behatrunprocess
)) {
316 if (!file_exists($CFG->behat_dataroot
)) {
317 $permissions = isset($CFG->directorypermissions
) ?
$CFG->directorypermissions
: 02777;
319 if (!mkdir($CFG->behat_dataroot
, $permissions, true)) {
320 behat_error(BEHAT_EXITCODE_PERMISSIONS
, '$CFG->behat_dataroot directory can not be created');
323 $CFG->behat_dataroot
= realpath($CFG->behat_dataroot
);
327 * Should we switch to the test site data?
330 function behat_is_test_site() {
333 if (defined('BEHAT_UTIL')) {
334 // This is the admin tool that installs/drops the test site install.
337 if (defined('BEHAT_TEST')) {
338 // This is the main vendor/bin/behat script.
341 if (empty($CFG->behat_wwwroot
)) {
344 if (isset($_SERVER['REMOTE_ADDR']) and behat_is_requested_url($CFG->behat_wwwroot
)) {
345 // Something is accessing the web server like a real browser.
353 * Fix variables for parallel behat testing.
354 * - behat_wwwroot = behat_wwwroot{behatrunprocess}
355 * - behat_dataroot = behat_dataroot{behatrunprocess}
356 * - behat_prefix = behat_prefix.{behatrunprocess}_ (For oracle it will be firstletter of prefix and behatrunprocess)
358 function behat_update_vars_for_process() {
361 $allowedconfigoverride = array('dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'behat_prefix',
362 'behat_wwwroot', 'behat_dataroot');
363 $behatrunprocess = behat_get_run_process();
364 $CFG->behatrunprocess
= $behatrunprocess;
366 // Data directory will be a directory under parent directory.
367 $CFG->behat_dataroot_parent
= $CFG->behat_dataroot
;
368 $CFG->behat_dataroot
.= '/'. BEHAT_PARALLEL_SITE_NAME
;
370 if ($behatrunprocess) {
371 if (empty($CFG->behat_parallel_run
[$behatrunprocess - 1]['behat_wwwroot'])) {
372 // Set www root for run process.
373 if (isset($CFG->behat_wwwroot
) &&
374 !preg_match("#/" . BEHAT_PARALLEL_SITE_NAME
. $behatrunprocess . "\$#", $CFG->behat_wwwroot
)) {
375 $CFG->behat_wwwroot
.= "/" . BEHAT_PARALLEL_SITE_NAME
. $behatrunprocess;
379 if (empty($CFG->behat_parallel_run
[$behatrunprocess - 1]['behat_dataroot'])) {
380 // Set behat_dataroot.
381 if (!preg_match("#" . $behatrunprocess . "\$#", $CFG->behat_dataroot
)) {
382 $CFG->behat_dataroot
.= $behatrunprocess;
386 // Set behat_prefix for db, just suffix run process number, to avoid max length exceed.
387 // For oracle only 2 letter prefix is possible.
388 // NOTE: This will not work for parallel process > 9.
389 if ($CFG->dbtype
=== 'oci') {
390 $CFG->behat_prefix
= substr($CFG->behat_prefix
, 0, 1);
391 $CFG->behat_prefix
.= "{$behatrunprocess}";
393 $CFG->behat_prefix
.= "{$behatrunprocess}_";
396 if (!empty($CFG->behat_parallel_run
[$behatrunprocess - 1])) {
397 // Override allowed config vars.
398 foreach ($allowedconfigoverride as $config) {
399 if (isset($CFG->behat_parallel_run
[$behatrunprocess - 1][$config])) {
400 $CFG->$config = $CFG->behat_parallel_run
[$behatrunprocess - 1][$config];
408 * Checks if the URL requested by the user matches the provided argument
411 * @return bool Returns true if it matches.
413 function behat_is_requested_url($url) {
415 $parsedurl = parse_url($url . '/');
416 if (!isset($parsedurl['port'])) {
417 $parsedurl['port'] = ($parsedurl['scheme'] === 'https') ?
443 : 80;
419 $parsedurl['path'] = rtrim($parsedurl['path'], '/');
421 // Removing the port.
422 $pos = strpos($_SERVER['HTTP_HOST'], ':');
423 if ($pos !== false) {
424 $requestedhost = substr($_SERVER['HTTP_HOST'], 0, $pos);
426 $requestedhost = $_SERVER['HTTP_HOST'];
429 // The path should also match.
430 if (empty($parsedurl['path'])) {
432 } else if (strpos($_SERVER['SCRIPT_NAME'], $parsedurl['path']) === 0) {
436 // The host and the port should match
437 if ($parsedurl['host'] == $requestedhost && $parsedurl['port'] == $_SERVER['SERVER_PORT'] && !empty($matchespath)) {
445 * Get behat run process from either $_SERVER or command config.
447 * @return bool|int false if single run, else run process number.
449 function behat_get_run_process() {
451 $behatrunprocess = false;
453 // Get behat run process, if set.
454 if (defined('BEHAT_CURRENT_RUN') && BEHAT_CURRENT_RUN
) {
455 $behatrunprocess = BEHAT_CURRENT_RUN
;
456 } else if (!empty($_SERVER['REMOTE_ADDR'])) {
457 // Try get it from config if present.
458 if (!empty($CFG->behat_parallel_run
)) {
459 foreach ($CFG->behat_parallel_run
as $run => $behatconfig) {
460 if (isset($behatconfig['behat_wwwroot']) && behat_is_requested_url($behatconfig['behat_wwwroot'])) {
461 $behatrunprocess = $run +
1; // We start process from 1.
466 // Check if parallel site prefix is used.
467 if (empty($behatrunprocess) && preg_match('#/' . BEHAT_PARALLEL_SITE_NAME
. '(.+?)/#', $_SERVER['REQUEST_URI'])) {
468 $dirrootrealpath = str_replace("\\", "/", realpath($CFG->dirroot
));
469 $serverrealpath = str_replace("\\", "/", realpath($_SERVER['SCRIPT_FILENAME']));
470 $afterpath = str_replace($dirrootrealpath.'/', '', $serverrealpath);
471 if (!$behatrunprocess = preg_filter("#.*/" . BEHAT_PARALLEL_SITE_NAME
. "(.+?)/$afterpath#", '$1',
472 $_SERVER['SCRIPT_FILENAME'])) {
473 throw new Exception("Unable to determine behat process [afterpath=" . $afterpath .
474 ", scriptfilename=" . $_SERVER['SCRIPT_FILENAME'] . "]!");
477 } else if (defined('BEHAT_TEST') ||
defined('BEHAT_UTIL')) {
480 if ($match = preg_filter('#--run=(.+)#', '$1', $argv)) {
481 // Try to guess the run from the existence of the --run arg.
482 $behatrunprocess = reset($match);
485 // Try to guess the run from the existence of the --config arg. Note there are 2 alternatives below.
486 if ($k = array_search('--config', $argv)) {
487 // Alternative 1: --config /path/to/config.yml => (next arg, pick it).
488 $behatconfig = str_replace("\\", "/", $argv[$k +
1]);
490 } else if ($config = preg_filter('#^(?:--config[ =]*)(.+)$#', '$1', $argv)) {
491 // Alternative 2: --config=/path/to/config.yml => (same arg, just get the path part).
492 $behatconfig = str_replace("\\", "/", reset($config));
495 // Try get it from config if present.
497 if (!empty($CFG->behat_parallel_run
)) {
498 foreach ($CFG->behat_parallel_run
as $run => $parallelconfig) {
499 if (!empty($parallelconfig['behat_dataroot']) &&
500 $parallelconfig['behat_dataroot'] . '/behat/behat.yml' == $behatconfig) {
501 $behatrunprocess = $run +
1; // We start process from 1.
506 // Check if default behat dataroot increment was done.
507 if (empty($behatrunprocess)) {
508 $behatdataroot = str_replace("\\", "/", $CFG->behat_dataroot
. '/' . BEHAT_PARALLEL_SITE_NAME
);
509 $behatrunprocess = preg_filter("#^{$behatdataroot}" . "(.+?)[/|\\\]behat[/|\\\]behat\.yml#", '$1',
516 return $behatrunprocess;
520 * Execute commands in parallel.
522 * @param array $cmds list of commands to be executed.
523 * @param string $cwd absolute path of working directory.
524 * @param int $delay time in seconds to add delay between each parallel process.
525 * @return array list of processes.
527 function cli_execute_parallel($cmds, $cwd = null, $delay = 0) {
528 require_once(__DIR__
. "/../../vendor/autoload.php");
530 $processes = array();
532 // Create child process.
533 foreach ($cmds as $name => $cmd) {
534 if (method_exists('\\Symfony\\Component\\Process\\Process', 'fromShellCommandline')) {
535 // Process 4.2 and up.
536 $process = Symfony\Component\Process\Process
::fromShellCommandline($cmd);
538 // Process 4.1 and older.
539 $process = new Symfony\Component\Process\
Process(null);
540 $process->setCommandLine($cmd);
543 $process->setWorkingDirectory($cwd);
544 $process->setTimeout(null);
545 $processes[$name] = $process;
546 $processes[$name]->start();
548 // If error creating process then exit.
549 if ($processes[$name]->getStatus() !== 'started') {
550 echo "Error starting process: $name";
551 foreach ($processes[$name] as $process) {
553 $process->signal(SIGKILL
);
559 // Sleep for specified delay.
568 * Get command flags for an option/value combination
570 * @param string $option
571 * @param string|bool|null $value
574 function behat_get_command_flags(string $option, $value): string {
575 $commandoptions = '';
576 if (is_bool($value)) {
578 return " --{$option}";
580 return " --no-{$option}";
582 } else if ($value !== null) {
583 return " --$option=\"$value\"";