See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
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 basic functions 19 * 20 * It does not include MOODLE_INTERNAL because is part of the bootstrap. 21 * 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')) { ... 27 * 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 32 */ 33 34 require_once (__DIR__ . '/../testing/lib.php'); 35 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); 42 43 /** 44 * The behat test site fullname and shortname. 45 */ 46 define('BEHAT_PARALLEL_SITE_NAME', "behatrun"); 47 48 /** 49 * Exits with an error code 50 * 51 * @param mixed $errorcode 52 * @param string $text 53 * @return void Stops execution with error code 54 */ 55 function behat_error($errorcode, $text = '') { 56 57 // Adding error prefixes. 58 switch ($errorcode) { 59 case BEHAT_EXITCODE_CONFIG: 60 $text = 'Behat config error: ' . $text; 61 break; 62 case BEHAT_EXITCODE_REQUIREMENT: 63 $text = 'Behat requirement not satisfied: ' . $text; 64 break; 65 case BEHAT_EXITCODE_PERMISSIONS: 66 $text = 'Behat permissions problem: ' . $text . ', check the permissions'; 67 break; 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; 71 break; 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; 75 break; 76 case BEHAT_EXITCODE_INSTALLED: 77 $text = "The Behat site is already installed"; 78 break; 79 default: 80 $text = 'Unknown error ' . $errorcode . ' ' . $text; 81 break; 82 } 83 84 testing_error($errorcode, $text); 85 } 86 87 /** 88 * Return logical error string. 89 * 90 * @param int $errtype php error type. 91 * @return string string which will be returned. 92 */ 93 function behat_get_error_string($errtype) { 94 switch ($errtype) { 95 case E_USER_ERROR: 96 $errnostr = 'Fatal error'; 97 break; 98 case E_WARNING: 99 case E_USER_WARNING: 100 $errnostr = 'Warning'; 101 break; 102 case E_NOTICE: 103 case E_USER_NOTICE: 104 case E_STRICT: 105 $errnostr = 'Notice'; 106 break; 107 case E_RECOVERABLE_ERROR: 108 $errnostr = 'Catchable'; 109 break; 110 default: 111 $errnostr = 'Unknown error type'; 112 } 113 114 return $errnostr; 115 } 116 117 /** 118 * PHP errors handler to use when running behat tests. 119 * 120 * Adds specific CSS classes to identify 121 * the messages. 122 * 123 * @param int $errno 124 * @param string $errstr 125 * @param string $errfile 126 * @param int $errline 127 * @param array $errcontext 128 * @return bool 129 */ 130 function behat_error_handler($errno, $errstr, $errfile, $errline, $errcontext) { 131 132 // If is preceded by an @ we don't show it. 133 if (!error_reporting()) { 134 return true; 135 } 136 137 // This error handler receives E_ALL | E_STRICT, running the behat test site the debug level is 138 // set to DEVELOPER and will always include E_NOTICE,E_USER_NOTICE... as part of E_ALL, if the current 139 // error_reporting() value does not include one of those levels is because it has been forced through 140 // the moodle code (see fix_utf8() for example) in that cases we respect the forced error level value. 141 $respect = array(E_NOTICE, E_USER_NOTICE, E_STRICT, E_WARNING, E_USER_WARNING, E_DEPRECATED, E_USER_DEPRECATED); 142 foreach ($respect as $respectable) { 143 144 // If the current value does not include this kind of errors and the reported error is 145 // at that level don't print anything. 146 if ($errno == $respectable && !(error_reporting() & $respectable)) { 147 return true; 148 } 149 } 150 151 // Using the default one in case there is a fatal catchable error. 152 default_error_handler($errno, $errstr, $errfile, $errline, $errcontext); 153 154 $errnostr = behat_get_error_string($errno); 155 156 // If ajax script then throw exception, so the calling api catch it and show it on web page. 157 if (defined('AJAX_SCRIPT')) { 158 throw new Exception("$errnostr: $errstr in $errfile on line $errline"); 159 } else { 160 // Wrapping the output. 161 echo '<div class="phpdebugmessage" data-rel="phpdebugmessage">' . PHP_EOL; 162 echo "$errnostr: $errstr in $errfile on line $errline" . PHP_EOL; 163 echo '</div>'; 164 } 165 166 // Also use the internal error handler so we keep the usual behaviour. 167 return false; 168 } 169 170 /** 171 * Before shutdown save last error entries, so we can fail the test. 172 */ 173 function behat_shutdown_function() { 174 // If any error found, then save it. 175 if ($error = error_get_last()) { 176 // Ignore E_WARNING, as they might come via ( @ )suppression and might lead to false failure. 177 if (isset($error['type']) && !($error['type'] & E_WARNING)) { 178 179 $errors = behat_get_shutdown_process_errors(); 180 181 $errors[] = $error; 182 $errorstosave = json_encode($errors); 183 184 set_config('process_errors', $errorstosave, 'tool_behat'); 185 } 186 } 187 } 188 189 /** 190 * Return php errors save which were save during shutdown. 191 * 192 * @return array 193 */ 194 function behat_get_shutdown_process_errors() { 195 global $DB; 196 197 // Don't use get_config, as it use cache and return invalid value, between selenium and cli process. 198 $phperrors = $DB->get_field('config_plugins', 'value', array('name' => 'process_errors', 'plugin' => 'tool_behat')); 199 200 if (!empty($phperrors)) { 201 return json_decode($phperrors, true); 202 } else { 203 return array(); 204 } 205 } 206 207 /** 208 * Restrict the config.php settings allowed. 209 * 210 * When running the behat features the config.php 211 * settings should not affect the results. 212 * 213 * @return void 214 */ 215 function behat_clean_init_config() { 216 global $CFG; 217 218 $allowed = array_flip(array( 219 'wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions', 220 'umaskpermissions', 'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix', 221 'dboptions', 'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword', 222 'proxybypass', 'pathtogs', 'pathtophp', 'pathtodu', 'aspellpath', 'pathtodot', 'skiplangupgrade', 223 'altcacheconfigpath', 'pathtounoconv', 'alternative_file_system_class', 'pathtopython' 224 )); 225 226 // Add extra allowed settings. 227 if (!empty($CFG->behat_extraallowedsettings)) { 228 $allowed = array_merge($allowed, array_flip($CFG->behat_extraallowedsettings)); 229 } 230 231 // Also allowing behat_ prefixed attributes. 232 foreach ($CFG as $key => $value) { 233 if (!isset($allowed[$key]) && strpos($key, 'behat_') !== 0) { 234 unset($CFG->{$key}); 235 } 236 } 237 } 238 239 /** 240 * Checks that the behat config vars are properly set. 241 * 242 * @return void Stops execution with error code if something goes wrong. 243 */ 244 function behat_check_config_vars() { 245 global $CFG; 246 247 // Verify prefix value. 248 if (empty($CFG->behat_prefix)) { 249 behat_error(BEHAT_EXITCODE_CONFIG, 250 'Define $CFG->behat_prefix in config.php'); 251 } 252 if (!empty($CFG->prefix) and $CFG->behat_prefix == $CFG->prefix) { 253 behat_error(BEHAT_EXITCODE_CONFIG, 254 '$CFG->behat_prefix in config.php must be different from $CFG->prefix'); 255 } 256 if (!empty($CFG->phpunit_prefix) and $CFG->behat_prefix == $CFG->phpunit_prefix) { 257 behat_error(BEHAT_EXITCODE_CONFIG, 258 '$CFG->behat_prefix in config.php must be different from $CFG->phpunit_prefix'); 259 } 260 261 // Verify behat wwwroot value. 262 if (empty($CFG->behat_wwwroot)) { 263 behat_error(BEHAT_EXITCODE_CONFIG, 264 'Define $CFG->behat_wwwroot in config.php'); 265 } 266 if (!empty($CFG->wwwroot) and $CFG->behat_wwwroot == $CFG->wwwroot) { 267 behat_error(BEHAT_EXITCODE_CONFIG, 268 '$CFG->behat_wwwroot in config.php must be different from $CFG->wwwroot'); 269 } 270 271 // Verify behat dataroot value. 272 if (empty($CFG->behat_dataroot)) { 273 behat_error(BEHAT_EXITCODE_CONFIG, 274 'Define $CFG->behat_dataroot in config.php'); 275 } 276 clearstatcache(); 277 if (!file_exists($CFG->behat_dataroot_parent)) { 278 $permissions = isset($CFG->directorypermissions) ? $CFG->directorypermissions : 02777; 279 umask(0); 280 if (!mkdir($CFG->behat_dataroot_parent, $permissions, true)) { 281 behat_error(BEHAT_EXITCODE_PERMISSIONS, '$CFG->behat_dataroot directory can not be created'); 282 } 283 } 284 $CFG->behat_dataroot_parent = realpath($CFG->behat_dataroot_parent); 285 if (empty($CFG->behat_dataroot_parent) or !is_dir($CFG->behat_dataroot_parent) or !is_writable($CFG->behat_dataroot_parent)) { 286 behat_error(BEHAT_EXITCODE_CONFIG, 287 '$CFG->behat_dataroot in config.php must point to an existing writable directory'); 288 } 289 if (!empty($CFG->dataroot) and $CFG->behat_dataroot_parent == realpath($CFG->dataroot)) { 290 behat_error(BEHAT_EXITCODE_CONFIG, 291 '$CFG->behat_dataroot in config.php must be different from $CFG->dataroot'); 292 } 293 if (!empty($CFG->phpunit_dataroot) and $CFG->behat_dataroot_parent == realpath($CFG->phpunit_dataroot)) { 294 behat_error(BEHAT_EXITCODE_CONFIG, 295 '$CFG->behat_dataroot in config.php must be different from $CFG->phpunit_dataroot'); 296 } 297 298 // This request is coming from admin/tool/behat/cli/util.php which will call util_single.php. So just return from 299 // here as we don't need to create a dataroot for single run. 300 if (defined('BEHAT_PARALLEL_UTIL') && BEHAT_PARALLEL_UTIL && empty($CFG->behatrunprocess)) { 301 return; 302 } 303 304 if (!file_exists($CFG->behat_dataroot)) { 305 $permissions = isset($CFG->directorypermissions) ? $CFG->directorypermissions : 02777; 306 umask(0); 307 if (!mkdir($CFG->behat_dataroot, $permissions, true)) { 308 behat_error(BEHAT_EXITCODE_PERMISSIONS, '$CFG->behat_dataroot directory can not be created'); 309 } 310 } 311 $CFG->behat_dataroot = realpath($CFG->behat_dataroot); 312 } 313 314 /** 315 * Should we switch to the test site data? 316 * @return bool 317 */ 318 function behat_is_test_site() { 319 global $CFG; 320 321 if (defined('BEHAT_UTIL')) { 322 // This is the admin tool that installs/drops the test site install. 323 return true; 324 } 325 if (defined('BEHAT_TEST')) { 326 // This is the main vendor/bin/behat script. 327 return true; 328 } 329 if (empty($CFG->behat_wwwroot)) { 330 return false; 331 } 332 if (isset($_SERVER['REMOTE_ADDR']) and behat_is_requested_url($CFG->behat_wwwroot)) { 333 // Something is accessing the web server like a real browser. 334 return true; 335 } 336 337 return false; 338 } 339 340 /** 341 * Fix variables for parallel behat testing. 342 * - behat_wwwroot = behat_wwwroot{behatrunprocess} 343 * - behat_dataroot = behat_dataroot{behatrunprocess} 344 * - behat_prefix = behat_prefix.{behatrunprocess}_ (For oracle it will be firstletter of prefix and behatrunprocess) 345 **/ 346 function behat_update_vars_for_process() { 347 global $CFG; 348 349 $allowedconfigoverride = array('dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'behat_prefix', 350 'behat_wwwroot', 'behat_dataroot'); 351 $behatrunprocess = behat_get_run_process(); 352 $CFG->behatrunprocess = $behatrunprocess; 353 354 // Data directory will be a directory under parent directory. 355 $CFG->behat_dataroot_parent = $CFG->behat_dataroot; 356 $CFG->behat_dataroot .= '/'. BEHAT_PARALLEL_SITE_NAME; 357 358 if ($behatrunprocess) { 359 if (empty($CFG->behat_parallel_run[$behatrunprocess - 1]['behat_wwwroot'])) { 360 // Set www root for run process. 361 if (isset($CFG->behat_wwwroot) && 362 !preg_match("#/" . BEHAT_PARALLEL_SITE_NAME . $behatrunprocess . "\$#", $CFG->behat_wwwroot)) { 363 $CFG->behat_wwwroot .= "/" . BEHAT_PARALLEL_SITE_NAME . $behatrunprocess; 364 } 365 } 366 367 if (empty($CFG->behat_parallel_run[$behatrunprocess - 1]['behat_dataroot'])) { 368 // Set behat_dataroot. 369 if (!preg_match("#" . $behatrunprocess . "\$#", $CFG->behat_dataroot)) { 370 $CFG->behat_dataroot .= $behatrunprocess; 371 } 372 } 373 374 // Set behat_prefix for db, just suffix run process number, to avoid max length exceed. 375 // For oracle only 2 letter prefix is possible. 376 // NOTE: This will not work for parallel process > 9. 377 if ($CFG->dbtype === 'oci') { 378 $CFG->behat_prefix = substr($CFG->behat_prefix, 0, 1); 379 $CFG->behat_prefix .= "{$behatrunprocess}"; 380 } else { 381 $CFG->behat_prefix .= "{$behatrunprocess}_"; 382 } 383 384 if (!empty($CFG->behat_parallel_run[$behatrunprocess - 1])) { 385 // Override allowed config vars. 386 foreach ($allowedconfigoverride as $config) { 387 if (isset($CFG->behat_parallel_run[$behatrunprocess - 1][$config])) { 388 $CFG->$config = $CFG->behat_parallel_run[$behatrunprocess - 1][$config]; 389 } 390 } 391 } 392 } 393 } 394 395 /** 396 * Checks if the URL requested by the user matches the provided argument 397 * 398 * @param string $url 399 * @return bool Returns true if it matches. 400 */ 401 function behat_is_requested_url($url) { 402 403 $parsedurl = parse_url($url . '/'); 404 $parsedurl['port'] = isset($parsedurl['port']) ? $parsedurl['port'] : 80; 405 $parsedurl['path'] = rtrim($parsedurl['path'], '/'); 406 407 // Removing the port. 408 $pos = strpos($_SERVER['HTTP_HOST'], ':'); 409 if ($pos !== false) { 410 $requestedhost = substr($_SERVER['HTTP_HOST'], 0, $pos); 411 } else { 412 $requestedhost = $_SERVER['HTTP_HOST']; 413 } 414 415 // The path should also match. 416 if (empty($parsedurl['path'])) { 417 $matchespath = true; 418 } else if (strpos($_SERVER['SCRIPT_NAME'], $parsedurl['path']) === 0) { 419 $matchespath = true; 420 } 421 422 // The host and the port should match 423 if ($parsedurl['host'] == $requestedhost && $parsedurl['port'] == $_SERVER['SERVER_PORT'] && !empty($matchespath)) { 424 return true; 425 } 426 427 return false; 428 } 429 430 /** 431 * Get behat run process from either $_SERVER or command config. 432 * 433 * @return bool|int false if single run, else run process number. 434 */ 435 function behat_get_run_process() { 436 global $argv, $CFG; 437 $behatrunprocess = false; 438 439 // Get behat run process, if set. 440 if (defined('BEHAT_CURRENT_RUN') && BEHAT_CURRENT_RUN) { 441 $behatrunprocess = BEHAT_CURRENT_RUN; 442 } else if (!empty($_SERVER['REMOTE_ADDR'])) { 443 // Try get it from config if present. 444 if (!empty($CFG->behat_parallel_run)) { 445 foreach ($CFG->behat_parallel_run as $run => $behatconfig) { 446 if (isset($behatconfig['behat_wwwroot']) && behat_is_requested_url($behatconfig['behat_wwwroot'])) { 447 $behatrunprocess = $run + 1; // We start process from 1. 448 break; 449 } 450 } 451 } 452 // Check if parallel site prefix is used. 453 if (empty($behatrunprocess) && preg_match('#/' . BEHAT_PARALLEL_SITE_NAME . '(.+?)/#', $_SERVER['REQUEST_URI'])) { 454 $dirrootrealpath = str_replace("\\", "/", realpath($CFG->dirroot)); 455 $serverrealpath = str_replace("\\", "/", realpath($_SERVER['SCRIPT_FILENAME'])); 456 $afterpath = str_replace($dirrootrealpath.'/', '', $serverrealpath); 457 if (!$behatrunprocess = preg_filter("#.*/" . BEHAT_PARALLEL_SITE_NAME . "(.+?)/$afterpath#", '$1', 458 $_SERVER['SCRIPT_FILENAME'])) { 459 throw new Exception("Unable to determine behat process [afterpath=" . $afterpath . 460 ", scriptfilename=" . $_SERVER['SCRIPT_FILENAME'] . "]!"); 461 } 462 } 463 } else if (defined('BEHAT_TEST') || defined('BEHAT_UTIL')) { 464 $behatconfig = ''; 465 466 if ($match = preg_filter('#--run=(.+)#', '$1', $argv)) { 467 // Try to guess the run from the existence of the --run arg. 468 $behatrunprocess = reset($match); 469 470 } else { 471 // Try to guess the run from the existence of the --config arg. Note there are 2 alternatives below. 472 if ($k = array_search('--config', $argv)) { 473 // Alternative 1: --config /path/to/config.yml => (next arg, pick it). 474 $behatconfig = str_replace("\\", "/", $argv[$k + 1]); 475 476 } else if ($config = preg_filter('#^(?:--config[ =]*)(.+)$#', '$1', $argv)) { 477 // Alternative 2: --config=/path/to/config.yml => (same arg, just get the path part). 478 $behatconfig = str_replace("\\", "/", reset($config)); 479 } 480 481 // Try get it from config if present. 482 if ($behatconfig) { 483 if (!empty($CFG->behat_parallel_run)) { 484 foreach ($CFG->behat_parallel_run as $run => $parallelconfig) { 485 if (!empty($parallelconfig['behat_dataroot']) && 486 $parallelconfig['behat_dataroot'] . '/behat/behat.yml' == $behatconfig) { 487 $behatrunprocess = $run + 1; // We start process from 1. 488 break; 489 } 490 } 491 } 492 // Check if default behat dataroot increment was done. 493 if (empty($behatrunprocess)) { 494 $behatdataroot = str_replace("\\", "/", $CFG->behat_dataroot . '/' . BEHAT_PARALLEL_SITE_NAME); 495 $behatrunprocess = preg_filter("#^{$behatdataroot}" . "(.+?)[/|\\\]behat[/|\\\]behat\.yml#", '$1', 496 $behatconfig); 497 } 498 } 499 } 500 } 501 502 return $behatrunprocess; 503 } 504 505 /** 506 * Execute commands in parallel. 507 * 508 * @param array $cmds list of commands to be executed. 509 * @param string $cwd absolute path of working directory. 510 * @param int $delay time in seconds to add delay between each parallel process. 511 * @return array list of processes. 512 */ 513 function cli_execute_parallel($cmds, $cwd = null, $delay = 0) { 514 require_once(__DIR__ . "/../../vendor/autoload.php"); 515 516 $processes = array(); 517 518 // Create child process. 519 foreach ($cmds as $name => $cmd) { 520 if (method_exists('\\Symfony\\Component\\Process\\Process', 'fromShellCommandline')) { 521 // Process 4.2 and up. 522 $process = Symfony\Component\Process\Process::fromShellCommandline($cmd); 523 } else { 524 // Process 4.1 and older. 525 $process = new Symfony\Component\Process\Process(null); 526 $process->setCommandLine($cmd); 527 } 528 529 $process->setWorkingDirectory($cwd); 530 $process->setTimeout(null); 531 $processes[$name] = $process; 532 $processes[$name]->start(); 533 534 // If error creating process then exit. 535 if ($processes[$name]->getStatus() !== 'started') { 536 echo "Error starting process: $name"; 537 foreach ($processes[$name] as $process) { 538 if ($process) { 539 $process->signal(SIGKILL); 540 } 541 } 542 exit(1); 543 } 544 545 // Sleep for specified delay. 546 if ($delay) { 547 sleep($delay); 548 } 549 } 550 return $processes; 551 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body