Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
/lib/behat/ -> lib.php (source)

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]

   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   * @return bool
 128   */
 129  function behat_error_handler($errno, $errstr, $errfile, $errline) {
 130  
 131      // If is preceded by an @ we don't show it.
 132      if (!error_reporting()) {
 133          return true;
 134      }
 135  
 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) {
 142  
 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)) {
 146              return true;
 147          }
 148      }
 149  
 150      // Using the default one in case there is a fatal catchable error.
 151      default_error_handler($errno, $errstr, $errfile, $errline);
 152  
 153      $errnostr = behat_get_error_string($errno);
 154  
 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");
 158      } else {
 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;
 162          echo '</div>';
 163      }
 164  
 165      // Also use the internal error handler so we keep the usual behaviour.
 166      return false;
 167  }
 168  
 169  /**
 170   * Before shutdown save last error entries, so we can fail the test.
 171   */
 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)) {
 177  
 178              $errors = behat_get_shutdown_process_errors();
 179  
 180              $errors[] = $error;
 181              $errorstosave = json_encode($errors);
 182  
 183              set_config('process_errors', $errorstosave, 'tool_behat');
 184          }
 185      }
 186  }
 187  
 188  /**
 189   * Return php errors save which were save during shutdown.
 190   *
 191   * @return array
 192   */
 193  function behat_get_shutdown_process_errors() {
 194      global $DB;
 195  
 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'));
 198  
 199      if (!empty($phperrors)) {
 200          return json_decode($phperrors, true);
 201      } else {
 202          return array();
 203      }
 204  }
 205  
 206  /**
 207   * Restrict the config.php settings allowed.
 208   *
 209   * When running the behat features the config.php
 210   * settings should not affect the results.
 211   *
 212   * @return void
 213   */
 214  function behat_clean_init_config() {
 215      global $CFG;
 216  
 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'
 223      ));
 224  
 225      // Add extra allowed settings.
 226      if (!empty($CFG->behat_extraallowedsettings)) {
 227          $allowed = array_merge($allowed, array_flip($CFG->behat_extraallowedsettings));
 228      }
 229  
 230      // Also allowing behat_ prefixed attributes.
 231      foreach ($CFG as $key => $value) {
 232          if (!isset($allowed[$key]) && strpos($key, 'behat_') !== 0) {
 233              unset($CFG->{$key});
 234          }
 235      }
 236  }
 237  
 238  /**
 239   * Checks that the behat config vars are properly set.
 240   *
 241   * @return void Stops execution with error code if something goes wrong.
 242   */
 243  function behat_check_config_vars() {
 244      global $CFG;
 245  
 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;
 253  
 254      // Verify prefix value.
 255      if (empty($CFG->behat_prefix)) {
 256          behat_error(BEHAT_EXITCODE_CONFIG,
 257              'Define $CFG->behat_prefix in config.php');
 258      }
 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');
 264      }
 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');
 271      }
 272  
 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');
 277      }
 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');
 281      }
 282  
 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');
 287      }
 288      clearstatcache();
 289      if (!file_exists($CFG->behat_dataroot_parent)) {
 290          $permissions = isset($CFG->directorypermissions) ? $CFG->directorypermissions : 02777;
 291          umask(0);
 292          if (!mkdir($CFG->behat_dataroot_parent, $permissions, true)) {
 293              behat_error(BEHAT_EXITCODE_PERMISSIONS, '$CFG->behat_dataroot directory can not be created');
 294          }
 295      }
 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');
 300      }
 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');
 304      }
 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');
 308      }
 309  
 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)) {
 313          return;
 314      }
 315  
 316      if (!file_exists($CFG->behat_dataroot)) {
 317          $permissions = isset($CFG->directorypermissions) ? $CFG->directorypermissions : 02777;
 318          umask(0);
 319          if (!mkdir($CFG->behat_dataroot, $permissions, true)) {
 320              behat_error(BEHAT_EXITCODE_PERMISSIONS, '$CFG->behat_dataroot directory can not be created');
 321          }
 322      }
 323      $CFG->behat_dataroot = realpath($CFG->behat_dataroot);
 324  }
 325  
 326  /**
 327   * Should we switch to the test site data?
 328   * @return bool
 329   */
 330  function behat_is_test_site() {
 331      global $CFG;
 332  
 333      if (defined('BEHAT_UTIL')) {
 334          // This is the admin tool that installs/drops the test site install.
 335          return true;
 336      }
 337      if (defined('BEHAT_TEST')) {
 338          // This is the main vendor/bin/behat script.
 339          return true;
 340      }
 341      if (empty($CFG->behat_wwwroot)) {
 342          return false;
 343      }
 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.
 346          return true;
 347      }
 348  
 349      return false;
 350  }
 351  
 352  /**
 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)
 357   **/
 358  function behat_update_vars_for_process() {
 359      global $CFG;
 360  
 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;
 365  
 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;
 369  
 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;
 376              }
 377          }
 378  
 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;
 383              }
 384          }
 385  
 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}";
 392          } else {
 393              $CFG->behat_prefix .= "{$behatrunprocess}_";
 394          }
 395  
 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];
 401                  }
 402              }
 403          }
 404      }
 405  }
 406  
 407  /**
 408   * Checks if the URL requested by the user matches the provided argument
 409   *
 410   * @param string $url
 411   * @return bool Returns true if it matches.
 412   */
 413  function behat_is_requested_url($url) {
 414  
 415      $parsedurl = parse_url($url . '/');
 416      if (!isset($parsedurl['port'])) {
 417          $parsedurl['port'] = ($parsedurl['scheme'] === 'https') ? 443 : 80;
 418      }
 419      $parsedurl['path'] = rtrim($parsedurl['path'], '/');
 420  
 421      // Removing the port.
 422      $pos = strpos($_SERVER['HTTP_HOST'], ':');
 423      if ($pos !== false) {
 424          $requestedhost = substr($_SERVER['HTTP_HOST'], 0, $pos);
 425      } else {
 426          $requestedhost = $_SERVER['HTTP_HOST'];
 427      }
 428  
 429      // The path should also match.
 430      if (empty($parsedurl['path'])) {
 431          $matchespath = true;
 432      } else if (strpos($_SERVER['SCRIPT_NAME'], $parsedurl['path']) === 0) {
 433          $matchespath = true;
 434      }
 435  
 436      // The host and the port should match
 437      if ($parsedurl['host'] == $requestedhost && $parsedurl['port'] == $_SERVER['SERVER_PORT'] && !empty($matchespath)) {
 438          return true;
 439      }
 440  
 441      return false;
 442  }
 443  
 444  /**
 445   * Get behat run process from either $_SERVER or command config.
 446   *
 447   * @return bool|int false if single run, else run process number.
 448   */
 449  function behat_get_run_process() {
 450      global $argv, $CFG;
 451      $behatrunprocess = false;
 452  
 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.
 462                      break;
 463                  }
 464              }
 465          }
 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'] . "]!");
 475              }
 476          }
 477      } else if (defined('BEHAT_TEST') || defined('BEHAT_UTIL')) {
 478          $behatconfig = '';
 479  
 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);
 483  
 484          } else {
 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]);
 489  
 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));
 493              }
 494  
 495              // Try get it from config if present.
 496              if ($behatconfig) {
 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.
 502                              break;
 503                          }
 504                      }
 505                  }
 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',
 510                          $behatconfig);
 511                  }
 512              }
 513          }
 514      }
 515  
 516      return $behatrunprocess;
 517  }
 518  
 519  /**
 520   * Execute commands in parallel.
 521   *
 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.
 526   */
 527  function cli_execute_parallel($cmds, $cwd = null, $delay = 0) {
 528      require_once(__DIR__ . "/../../vendor/autoload.php");
 529  
 530      $processes = array();
 531  
 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);
 537          } else {
 538              // Process 4.1 and older.
 539              $process = new Symfony\Component\Process\Process(null);
 540              $process->setCommandLine($cmd);
 541          }
 542  
 543          $process->setWorkingDirectory($cwd);
 544          $process->setTimeout(null);
 545          $processes[$name] = $process;
 546          $processes[$name]->start();
 547  
 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) {
 552                  if ($process) {
 553                      $process->signal(SIGKILL);
 554                  }
 555              }
 556              exit(1);
 557          }
 558  
 559          // Sleep for specified delay.
 560          if ($delay) {
 561              sleep($delay);
 562          }
 563      }
 564      return $processes;
 565  }
 566  
 567  /**
 568   * Get command flags for an option/value combination
 569   *
 570   * @param string $option
 571   * @param string|bool|null $value
 572   * @return string
 573   */
 574  function behat_get_command_flags(string $option, $value): string {
 575      $commandoptions = '';
 576      if (is_bool($value)) {
 577          if ($value) {
 578              return " --{$option}";
 579          } else {
 580              return " --no-{$option}";
 581          }
 582      } else if ($value !== null) {
 583          return " --$option=\"$value\"";
 584      }
 585      return '';
 586  }