Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
/lib/behat/ -> lib.php (source)

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   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);
 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  }