MDL-49684 timezones: rewrite timezone support
[moodle.git] / lib / behat / lib.php
blob7a422ed9c2446ed5fa387ef55e75ae029aff3348
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.
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 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')) { ...
28 * @package core
29 * @category test
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_COMPOSER', 255);
42 define('BEHAT_EXITCODE_INSTALLED', 256);
44 /**
45 * The behat test site fullname and shortname.
47 define('BEHAT_PARALLEL_SITE_NAME', "behatrun");
49 /**
50 * Exits with an error code
52 * @param mixed $errorcode
53 * @param string $text
54 * @return void Stops execution with error code
56 function behat_error($errorcode, $text = '') {
58 // Adding error prefixes.
59 switch ($errorcode) {
60 case BEHAT_EXITCODE_CONFIG:
61 $text = 'Behat config error: ' . $text;
62 break;
63 case BEHAT_EXITCODE_REQUIREMENT:
64 $text = 'Behat requirement not satisfied: ' . $text;
65 break;
66 case BEHAT_EXITCODE_PERMISSIONS:
67 $text = 'Behat permissions problem: ' . $text . ', check the permissions';
68 break;
69 case BEHAT_EXITCODE_REINSTALL:
70 $path = testing_cli_argument_path('/admin/tool/behat/cli/init.php');
71 $text = "Reinstall Behat: ".$text.", use:\n php ".$path;
72 break;
73 case BEHAT_EXITCODE_INSTALL:
74 $path = testing_cli_argument_path('/admin/tool/behat/cli/init.php');
75 $text = "Install Behat before enabling it, use:\n php ".$path;
76 break;
77 case BEHAT_EXITCODE_INSTALLED:
78 $text = "The Behat site is already installed";
79 break;
80 default:
81 $text = 'Unknown error ' . $errorcode . ' ' . $text;
82 break;
85 testing_error($errorcode, $text);
88 /**
89 * PHP errors handler to use when running behat tests.
91 * Adds specific CSS classes to identify
92 * the messages.
94 * @param int $errno
95 * @param string $errstr
96 * @param string $errfile
97 * @param int $errline
98 * @param array $errcontext
99 * @return bool
101 function behat_error_handler($errno, $errstr, $errfile, $errline, $errcontext) {
103 // If is preceded by an @ we don't show it.
104 if (!error_reporting()) {
105 return true;
108 // This error handler receives E_ALL | E_STRICT, running the behat test site the debug level is
109 // set to DEVELOPER and will always include E_NOTICE,E_USER_NOTICE... as part of E_ALL, if the current
110 // error_reporting() value does not include one of those levels is because it has been forced through
111 // the moodle code (see fix_utf8() for example) in that cases we respect the forced error level value.
112 $respect = array(E_NOTICE, E_USER_NOTICE, E_STRICT, E_WARNING, E_USER_WARNING);
113 foreach ($respect as $respectable) {
115 // If the current value does not include this kind of errors and the reported error is
116 // at that level don't print anything.
117 if ($errno == $respectable && !(error_reporting() & $respectable)) {
118 return true;
122 // Using the default one in case there is a fatal catchable error.
123 default_error_handler($errno, $errstr, $errfile, $errline, $errcontext);
125 switch ($errno) {
126 case E_USER_ERROR:
127 $errnostr = 'Fatal error';
128 break;
129 case E_WARNING:
130 case E_USER_WARNING:
131 $errnostr = 'Warning';
132 break;
133 case E_NOTICE:
134 case E_USER_NOTICE:
135 case E_STRICT:
136 $errnostr = 'Notice';
137 break;
138 case E_RECOVERABLE_ERROR:
139 $errnostr = 'Catchable';
140 break;
141 default:
142 $errnostr = 'Unknown error type';
145 // Wrapping the output.
146 echo '<div class="phpdebugmessage" data-rel="phpdebugmessage">' . PHP_EOL;
147 echo "$errnostr: $errstr in $errfile on line $errline" . PHP_EOL;
148 echo '</div>';
150 // Also use the internal error handler so we keep the usual behaviour.
151 return false;
155 * Restrict the config.php settings allowed.
157 * When running the behat features the config.php
158 * settings should not affect the results.
160 * @return void
162 function behat_clean_init_config() {
163 global $CFG;
165 $allowed = array_flip(array(
166 'wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions',
167 'umaskpermissions', 'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix',
168 'dboptions', 'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword',
169 'proxybypass', 'theme', 'pathtogs', 'pathtoclam', 'pathtodu', 'aspellpath', 'pathtodot', 'skiplangupgrade',
170 'altcacheconfigpath'
173 // Add extra allowed settings.
174 if (!empty($CFG->behat_extraallowedsettings)) {
175 $allowed = array_merge($allowed, array_flip($CFG->behat_extraallowedsettings));
178 // Also allowing behat_ prefixed attributes.
179 foreach ($CFG as $key => $value) {
180 if (!isset($allowed[$key]) && strpos($key, 'behat_') !== 0) {
181 unset($CFG->{$key});
185 // Here we are forcing the navbar to be absolutely positioned in Chrome, Safari and IE in order to
186 // avoid a driver bug whereby when the browser scrolls something into view it doesn't account
187 // for fixed positioned elements that end up obscuring the item thus leading to errors that
188 // could be avoided by scrolling an additional amount.
189 // This should be removed as soon as the affected drivers have been fixed.
190 $CFG->forced_plugin_settings['theme_clean'] = array(
191 'customcss' => 'body.safari .navbar-fixed-top, body.ie .navbar-fixed-top {position: absolute;}'
196 * Checks that the behat config vars are properly set.
198 * @return void Stops execution with error code if something goes wrong.
200 function behat_check_config_vars() {
201 global $CFG;
203 // Verify prefix value.
204 if (empty($CFG->behat_prefix)) {
205 behat_error(BEHAT_EXITCODE_CONFIG,
206 'Define $CFG->behat_prefix in config.php');
208 if (!empty($CFG->prefix) and $CFG->behat_prefix == $CFG->prefix) {
209 behat_error(BEHAT_EXITCODE_CONFIG,
210 '$CFG->behat_prefix in config.php must be different from $CFG->prefix');
212 if (!empty($CFG->phpunit_prefix) and $CFG->behat_prefix == $CFG->phpunit_prefix) {
213 behat_error(BEHAT_EXITCODE_CONFIG,
214 '$CFG->behat_prefix in config.php must be different from $CFG->phpunit_prefix');
217 // Verify behat wwwroot value.
218 if (empty($CFG->behat_wwwroot)) {
219 behat_error(BEHAT_EXITCODE_CONFIG,
220 'Define $CFG->behat_wwwroot in config.php');
222 if (!empty($CFG->wwwroot) and $CFG->behat_wwwroot == $CFG->wwwroot) {
223 behat_error(BEHAT_EXITCODE_CONFIG,
224 '$CFG->behat_wwwroot in config.php must be different from $CFG->wwwroot');
227 // Verify behat dataroot value.
228 if (empty($CFG->behat_dataroot)) {
229 behat_error(BEHAT_EXITCODE_CONFIG,
230 'Define $CFG->behat_dataroot in config.php');
232 clearstatcache();
233 if (!file_exists($CFG->behat_dataroot)) {
234 $permissions = isset($CFG->directorypermissions) ? $CFG->directorypermissions : 02777;
235 umask(0);
236 if (!mkdir($CFG->behat_dataroot, $permissions, true)) {
237 behat_error(BEHAT_EXITCODE_PERMISSIONS, '$CFG->behat_dataroot directory can not be created');
240 $CFG->behat_dataroot = realpath($CFG->behat_dataroot);
241 if (empty($CFG->behat_dataroot) or !is_dir($CFG->behat_dataroot) or !is_writable($CFG->behat_dataroot)) {
242 behat_error(BEHAT_EXITCODE_CONFIG,
243 '$CFG->behat_dataroot in config.php must point to an existing writable directory');
245 if (!empty($CFG->dataroot) and $CFG->behat_dataroot == realpath($CFG->dataroot)) {
246 behat_error(BEHAT_EXITCODE_CONFIG,
247 '$CFG->behat_dataroot in config.php must be different from $CFG->dataroot');
249 if (!empty($CFG->phpunit_dataroot) and $CFG->behat_dataroot == realpath($CFG->phpunit_dataroot)) {
250 behat_error(BEHAT_EXITCODE_CONFIG,
251 '$CFG->behat_dataroot in config.php must be different from $CFG->phpunit_dataroot');
256 * Should we switch to the test site data?
257 * @return bool
259 function behat_is_test_site() {
260 global $CFG;
262 if (defined('BEHAT_UTIL')) {
263 // This is the admin tool that installs/drops the test site install.
264 return true;
266 if (defined('BEHAT_TEST')) {
267 // This is the main vendor/bin/behat script.
268 return true;
270 if (empty($CFG->behat_wwwroot)) {
271 return false;
273 if (isset($_SERVER['REMOTE_ADDR']) and behat_is_requested_url($CFG->behat_wwwroot)) {
274 // Something is accessing the web server like a real browser.
275 return true;
278 return false;
282 * Fix variables for parallel behat testing.
283 * - behat_wwwroot = behat_wwwroot{behatrunprocess}
284 * - behat_dataroot = behat_dataroot{behatrunprocess}
285 * - behat_prefix = behat_prefix.{behatrunprocess}_ (For oracle it will be firstletter of prefix and behatrunprocess)
287 function behat_update_vars_for_process() {
288 global $CFG;
290 $allowedconfigoverride = array('dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'behat_prefix',
291 'behat_wwwroot', 'behat_dataroot');
292 $behatrunprocess = behat_get_run_process();
293 $CFG->behatrunprocess = $behatrunprocess;
295 if ($behatrunprocess) {
296 if (empty($CFG->behat_parallel_run[$behatrunprocess - 1]['behat_wwwroot'])) {
297 // Set www root for run process.
298 if (isset($CFG->behat_wwwroot) &&
299 !preg_match("#/" . BEHAT_PARALLEL_SITE_NAME . $behatrunprocess . "\$#", $CFG->behat_wwwroot)) {
300 $CFG->behat_wwwroot .= "/" . BEHAT_PARALLEL_SITE_NAME . $behatrunprocess;
304 if (empty($CFG->behat_parallel_run[$behatrunprocess - 1]['behat_dataroot'])) {
305 // Set behat_dataroot.
306 if (!preg_match("#" . $behatrunprocess . "\$#", $CFG->behat_dataroot)) {
307 $CFG->behat_dataroot .= $behatrunprocess;
311 // Set behat_prefix for db, just suffix run process number, to avoid max length exceed.
312 // For oracle only 2 letter prefix is possible.
313 // NOTE: This will not work for parallel process > 9.
314 if ($CFG->dbtype === 'oci') {
315 $CFG->behat_prefix = substr($CFG->behat_prefix, 0, 1);
316 $CFG->behat_prefix .= "{$behatrunprocess}";
317 } else {
318 $CFG->behat_prefix .= "{$behatrunprocess}_";
321 if (!empty($CFG->behat_parallel_run[$behatrunprocess - 1])) {
322 // Override allowed config vars.
323 foreach ($allowedconfigoverride as $config) {
324 if (isset($CFG->behat_parallel_run[$behatrunprocess - 1][$config])) {
325 $CFG->$config = $CFG->behat_parallel_run[$behatrunprocess - 1][$config];
333 * Checks if the URL requested by the user matches the provided argument
335 * @param string $url
336 * @return bool Returns true if it matches.
338 function behat_is_requested_url($url) {
340 $parsedurl = parse_url($url . '/');
341 $parsedurl['port'] = isset($parsedurl['port']) ? $parsedurl['port'] : 80;
342 $parsedurl['path'] = rtrim($parsedurl['path'], '/');
344 // Removing the port.
345 $pos = strpos($_SERVER['HTTP_HOST'], ':');
346 if ($pos !== false) {
347 $requestedhost = substr($_SERVER['HTTP_HOST'], 0, $pos);
348 } else {
349 $requestedhost = $_SERVER['HTTP_HOST'];
352 // The path should also match.
353 if (empty($parsedurl['path'])) {
354 $matchespath = true;
355 } else if (strpos($_SERVER['SCRIPT_NAME'], $parsedurl['path']) === 0) {
356 $matchespath = true;
359 // The host and the port should match
360 if ($parsedurl['host'] == $requestedhost && $parsedurl['port'] == $_SERVER['SERVER_PORT'] && !empty($matchespath)) {
361 return true;
364 return false;
368 * Get behat run process from either $_SERVER or command config.
370 * @return bool|int false if single run, else run process number.
372 function behat_get_run_process() {
373 global $argv, $CFG;
374 $behatrunprocess = false;
376 // Get behat run process, if set.
377 if (defined('BEHAT_CURRENT_RUN') && BEHAT_CURRENT_RUN) {
378 $behatrunprocess = BEHAT_CURRENT_RUN;
379 } else if (!empty($_SERVER['REMOTE_ADDR'])) {
380 // Try get it from config if present.
381 if (!empty($CFG->behat_parallel_run)) {
382 foreach ($CFG->behat_parallel_run as $run => $behatconfig) {
383 if (isset($behatconfig['behat_wwwroot']) && behat_is_requested_url($behatconfig['behat_wwwroot'])) {
384 $behatrunprocess = $run + 1; // We start process from 1.
385 break;
389 // Check if parallel site prefix is used.
390 if (empty($behatrunprocess) && preg_match('#/' . BEHAT_PARALLEL_SITE_NAME . '(.+?)/#', $_SERVER['REQUEST_URI'])) {
391 $dirrootrealpath = str_replace("\\", "/", realpath($CFG->dirroot));
392 $serverrealpath = str_replace("\\", "/", realpath($_SERVER['SCRIPT_FILENAME']));
393 $afterpath = str_replace($dirrootrealpath.'/', '', $serverrealpath);
394 if (!$behatrunprocess = preg_filter("#.*/" . BEHAT_PARALLEL_SITE_NAME . "(.+?)/$afterpath#", '$1',
395 $_SERVER['SCRIPT_FILENAME'])) {
396 throw new Exception("Unable to determine behat process [afterpath=" . $afterpath .
397 ", scriptfilename=" . $_SERVER['SCRIPT_FILENAME'] . "]!");
400 } else if (defined('BEHAT_TEST') || defined('BEHAT_UTIL')) {
401 if ($match = preg_filter('#--run=(.+)#', '$1', $argv)) {
402 $behatrunprocess = reset($match);
403 } else if ($k = array_search('--config', $argv)) {
404 $behatconfig = str_replace("\\", "/", $argv[$k + 1]);
405 // Try get it from config if present.
406 if (!empty($CFG->behat_parallel_run)) {
407 foreach ($CFG->behat_parallel_run as $run => $parallelconfig) {
408 if (!empty($parallelconfig['behat_dataroot']) &&
409 $parallelconfig['behat_dataroot'] . '/behat/behat.yml' == $behatconfig) {
411 $behatrunprocess = $run + 1; // We start process from 1.
412 break;
416 // Check if default behat datroot increment was done.
417 if (empty($behatrunprocess)) {
418 $behatdataroot = str_replace("\\", "/", $CFG->behat_dataroot);
419 $behatrunprocess = preg_filter("#^{$behatdataroot}" . "(.+?)[/|\\\]behat[/|\\\]behat\.yml#", '$1',
420 $behatconfig);
425 return $behatrunprocess;
429 * Execute commands in parallel.
431 * @param array $cmds list of commands to be executed.
432 * @param string $cwd absolute path of working directory.
433 * @return array list of processes.
435 function cli_execute_parallel($cmds, $cwd = null) {
436 require_once(__DIR__ . "/../../vendor/autoload.php");
438 $processes = array();
440 // Create child process.
441 foreach ($cmds as $name => $cmd) {
442 $process = new Symfony\Component\Process\Process($cmd);
444 $process->setWorkingDirectory($cwd);
445 $process->setTimeout(null);
446 $processes[$name] = $process;
447 $processes[$name]->start();
449 // If error creating process then exit.
450 if ($processes[$name]->getStatus() !== 'started') {
451 echo "Error starting process: $name";
452 foreach ($processes[$name] as $process) {
453 if ($process) {
454 $process->signal(SIGKILL);
457 exit(1);
460 return $processes;