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.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 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   * CLI tool with utilities to manage parallel Behat integration in Moodle
  19   *
  20   * All CLI utilities uses $CFG->behat_dataroot and $CFG->prefix_dataroot as
  21   * $CFG->dataroot and $CFG->prefix
  22   * Same applies for $CFG->behat_dbname, $CFG->behat_dbuser, $CFG->behat_dbpass
  23   * and $CFG->behat_dbhost. But if any of those is not defined $CFG->dbname,
  24   * $CFG->dbuser, $CFG->dbpass and/or $CFG->dbhost will be used.
  25   *
  26   * @package    tool_behat
  27   * @copyright  2012 David MonllaĆ³
  28   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  29   */
  30  
  31  
  32  if (isset($_SERVER['REMOTE_ADDR'])) {
  33      die(); // No access from web!.
  34  }
  35  
  36  define('BEHAT_UTIL', true);
  37  define('CLI_SCRIPT', true);
  38  define('NO_OUTPUT_BUFFERING', true);
  39  define('IGNORE_COMPONENT_CACHE', true);
  40  define('ABORT_AFTER_CONFIG', true);
  41  
  42  require_once (__DIR__ . '/../../../../lib/clilib.php');
  43  
  44  // CLI options.
  45  list($options, $unrecognized) = cli_get_params(
  46      array(
  47          'help'        => false,
  48          'install'     => false,
  49          'drop'        => false,
  50          'enable'      => false,
  51          'disable'     => false,
  52          'diag'        => false,
  53          'parallel'    => 0,
  54          'maxruns'     => false,
  55          'updatesteps' => false,
  56          'fromrun'     => 1,
  57          'torun'       => 0,
  58          'optimize-runs' => '',
  59          'add-core-features-to-theme' => false,
  60          'axe'         => true,
  61      ),
  62      array(
  63          'h' => 'help',
  64          'j' => 'parallel',
  65          'm' => 'maxruns',
  66          'o' => 'optimize-runs',
  67          'a' => 'add-core-features-to-theme',
  68      )
  69  );
  70  
  71  // Checking util.php CLI script usage.
  72  $help = "
  73  Behat utilities to manage the test environment
  74  
  75  Usage:
  76    php util.php [--install|--drop|--enable|--disable|--diag|--updatesteps|--no-axe|--help] [--parallel=value [--maxruns=value]]
  77  
  78  Options:
  79  --install      Installs the test environment for acceptance tests
  80  --drop         Drops the database tables and the dataroot contents
  81  --enable       Enables test environment and updates tests list
  82  --disable      Disables test environment
  83  --diag         Get behat test environment status code
  84  --updatesteps  Update feature step file.
  85  --no-axe       Disable axe accessibility tests.
  86  
  87  -j, --parallel Number of parallel behat run operation
  88  -m, --maxruns Max parallel processes to be executed at one time.
  89  -o, --optimize-runs Split features with specified tags in all parallel runs.
  90  -a, --add-core-features-to-theme Add all core features to specified theme's
  91  
  92  -h, --help     Print out this help
  93  
  94  Example from Moodle root directory:
  95  \$ php admin/tool/behat/cli/util.php --enable --parallel=4
  96  
  97  More info in https://moodledev.io/general/development/tools/behat/running
  98  ";
  99  
 100  if (!empty($options['help'])) {
 101      echo $help;
 102      exit(0);
 103  }
 104  
 105  $cwd = getcwd();
 106  
 107  // If Behat parallel site is being initiliased, then define a param to be used to ignore single run install.
 108  if (!empty($options['parallel'])) {
 109      define('BEHAT_PARALLEL_UTIL', true);
 110  }
 111  
 112  require_once(__DIR__ . '/../../../../config.php');
 113  require_once (__DIR__ . '/../../../../lib/behat/lib.php');
 114  require_once (__DIR__ . '/../../../../lib/behat/classes/behat_command.php');
 115  require_once (__DIR__ . '/../../../../lib/behat/classes/behat_config_manager.php');
 116  
 117  // Remove error handling overrides done in config.php. This is consistent with admin/tool/behat/cli/util_single_run.php.
 118  $CFG->debug = (E_ALL | E_STRICT);
 119  $CFG->debugdisplay = 1;
 120  error_reporting($CFG->debug);
 121  ini_set('display_errors', '1');
 122  ini_set('log_errors', '1');
 123  
 124  // Import the necessary libraries.
 125  require_once($CFG->libdir . '/setuplib.php');
 126  require_once($CFG->libdir . '/behat/classes/util.php');
 127  
 128  // For drop option check if parallel site.
 129  if ((empty($options['parallel'])) && ($options['drop']) || $options['updatesteps']) {
 130      $options['parallel'] = behat_config_manager::get_behat_run_config_value('parallel');
 131  }
 132  
 133  // If not a parallel site then open single run.
 134  if (empty($options['parallel'])) {
 135      // Set run config value for single run.
 136      behat_config_manager::set_behat_run_config_value('singlerun', 1);
 137  
 138      chdir(__DIR__);
 139      // Check if behat is initialised, if not exit.
 140      passthru("php util_single_run.php --diag", $status);
 141      if ($status) {
 142          exit ($status);
 143      }
 144      $cmd = commands_to_execute($options);
 145      $processes = cli_execute_parallel(array($cmd), __DIR__);
 146      $status = print_sequential_output($processes, false);
 147      chdir($cwd);
 148      exit($status);
 149  }
 150  
 151  // Default torun is maximum parallel runs.
 152  if (empty($options['torun'])) {
 153      $options['torun'] = $options['parallel'];
 154  }
 155  
 156  $status = false;
 157  $cmds = commands_to_execute($options);
 158  
 159  // Start executing commands either sequential/parallel for options provided.
 160  if ($options['diag'] || $options['enable'] || $options['disable']) {
 161      // Do it sequentially as it's fast and need to be displayed nicely.
 162      foreach (array_chunk($cmds, 1, true) as $cmd) {
 163          $processes = cli_execute_parallel($cmd, __DIR__);
 164          print_sequential_output($processes);
 165      }
 166  
 167  } else if ($options['drop']) {
 168      $processes = cli_execute_parallel($cmds, __DIR__);
 169      $exitcodes = print_combined_drop_output($processes);
 170      foreach ($exitcodes as $exitcode) {
 171          $status = (bool)$status || (bool)$exitcode;
 172      }
 173  
 174      // Remove run config file.
 175      $behatrunconfigfile = behat_config_manager::get_behat_run_config_file_path();
 176      if (file_exists($behatrunconfigfile)) {
 177          if (!unlink($behatrunconfigfile)) {
 178              behat_error(BEHAT_EXITCODE_PERMISSIONS, 'Can not delete behat run config file');
 179          }
 180      }
 181  
 182      // Remove test file path.
 183      if (file_exists(behat_util::get_test_file_path())) {
 184          if (!unlink(behat_util::get_test_file_path())) {
 185              behat_error(BEHAT_EXITCODE_PERMISSIONS, 'Can not delete test file enable info');
 186          }
 187      }
 188  
 189  } else if ($options['install']) {
 190      // This is intensive compared to behat itself so run them in chunk if option maxruns not set.
 191      if ($options['maxruns']) {
 192          foreach (array_chunk($cmds, $options['maxruns'], true) as $chunk) {
 193              $processes = cli_execute_parallel($chunk, __DIR__);
 194              $exitcodes = print_combined_install_output($processes);
 195              foreach ($exitcodes as $name => $exitcode) {
 196                  if ($exitcode != 0) {
 197                      echo "Failed process [[$name]]" . PHP_EOL;
 198                      echo $processes[$name]->getOutput();
 199                      echo PHP_EOL;
 200                      echo $processes[$name]->getErrorOutput();
 201                      echo PHP_EOL . PHP_EOL;
 202                  }
 203                  $status = (bool)$status || (bool)$exitcode;
 204              }
 205          }
 206      } else {
 207          $processes = cli_execute_parallel($cmds, __DIR__);
 208          $exitcodes = print_combined_install_output($processes);
 209          foreach ($exitcodes as $name => $exitcode) {
 210              if ($exitcode != 0) {
 211                  echo "Failed process [[$name]]" . PHP_EOL;
 212                  echo $processes[$name]->getOutput();
 213                  echo PHP_EOL;
 214                  echo $processes[$name]->getErrorOutput();
 215                  echo PHP_EOL . PHP_EOL;
 216              }
 217              $status = (bool)$status || (bool)$exitcode;
 218          }
 219      }
 220  
 221  } else if ($options['updatesteps']) {
 222      // Rewrite config file to ensure we have all the features covered.
 223      if (empty($options['parallel'])) {
 224          behat_config_manager::update_config_file('', true, '', $options['add-core-features-to-theme'], false, false);
 225      } else {
 226          // Update config file, ensuring we have up-to-date behat.yml.
 227          for ($i = $options['fromrun']; $i <= $options['torun']; $i++) {
 228              $CFG->behatrunprocess = $i;
 229  
 230              // Update config file for each run.
 231              behat_config_manager::update_config_file('', true, $options['optimize-runs'], $options['add-core-features-to-theme'],
 232                  $options['parallel'], $i);
 233          }
 234          unset($CFG->behatrunprocess);
 235      }
 236  
 237      // Do it sequentially as it's fast and need to be displayed nicely.
 238      foreach (array_chunk($cmds, 1, true) as $cmd) {
 239          $processes = cli_execute_parallel($cmd, __DIR__);
 240          print_sequential_output($processes);
 241      }
 242      exit(0);
 243  
 244  } else {
 245      // We should never reach here.
 246      echo $help;
 247      exit(1);
 248  }
 249  
 250  // Ensure we have success status to show following information.
 251  if ($status) {
 252      echo "Unknown failure $status" . PHP_EOL;
 253      exit((int)$status);
 254  }
 255  
 256  // Show command o/p (only one per time).
 257  if ($options['install']) {
 258      echo "Acceptance tests site installed for sites:".PHP_EOL;
 259  
 260      // Display all sites which are installed/drop/diabled.
 261      for ($i = $options['fromrun']; $i <= $options['torun']; $i++) {
 262          if (empty($CFG->behat_parallel_run[$i - 1]['behat_wwwroot'])) {
 263              echo $CFG->behat_wwwroot . "/" . BEHAT_PARALLEL_SITE_NAME . $i . PHP_EOL;
 264          } else {
 265              echo $CFG->behat_parallel_run[$i - 1]['behat_wwwroot'] . PHP_EOL;
 266          }
 267  
 268      }
 269  } else if ($options['drop']) {
 270      echo "Acceptance tests site dropped for " . $options['parallel'] . " parallel sites" . PHP_EOL;
 271  
 272  } else if ($options['enable']) {
 273      echo "Acceptance tests environment enabled on $CFG->behat_wwwroot, to run the tests use:" . PHP_EOL;
 274      echo behat_command::get_behat_command(true, true);
 275  
 276      // Save fromrun and to run information.
 277      if (isset($options['fromrun'])) {
 278          behat_config_manager::set_behat_run_config_value('fromrun', $options['fromrun']);
 279      }
 280  
 281      if (isset($options['torun'])) {
 282          behat_config_manager::set_behat_run_config_value('torun', $options['torun']);
 283      }
 284      if (isset($options['parallel'])) {
 285          behat_config_manager::set_behat_run_config_value('parallel', $options['parallel']);
 286      }
 287  
 288      echo PHP_EOL;
 289  
 290  } else if ($options['disable']) {
 291      echo "Acceptance tests environment disabled for " . $options['parallel'] . " parallel sites" . PHP_EOL;
 292  
 293  } else if ($options['diag']) {
 294      // Valid option, so nothing to do.
 295  } else {
 296      echo $help;
 297      chdir($cwd);
 298      exit(1);
 299  }
 300  
 301  chdir($cwd);
 302  exit(0);
 303  
 304  /**
 305   * Create commands to be executed for parallel run.
 306   *
 307   * @param array $options options provided by user.
 308   * @return array commands to be executed.
 309   */
 310  function commands_to_execute($options) {
 311      $removeoptions = array('maxruns', 'fromrun', 'torun');
 312      $cmds = array();
 313      $extraoptions = $options;
 314      $extra = "";
 315  
 316      // Remove extra options not in util_single_run.php.
 317      foreach ($removeoptions as $ro) {
 318          $extraoptions[$ro] = null;
 319          unset($extraoptions[$ro]);
 320      }
 321  
 322      foreach ($extraoptions as $option => $value) {
 323          $extra .= behat_get_command_flags($option, $value);
 324      }
 325  
 326      if (empty($options['parallel'])) {
 327          $cmds = "php util_single_run.php " . $extra;
 328      } else {
 329          // Create commands which has to be executed for parallel site.
 330          for ($i = $options['fromrun']; $i <= $options['torun']; $i++) {
 331              $prefix = BEHAT_PARALLEL_SITE_NAME . $i;
 332              $cmds[$prefix] = "php util_single_run.php " . $extra . " --run=" . $i . " 2>&1";
 333          }
 334      }
 335      return $cmds;
 336  }
 337  
 338  /**
 339   * Print drop output merging each run.
 340   *
 341   * @param array $processes list of processes.
 342   * @return array exit codes of each process.
 343   */
 344  function print_combined_drop_output($processes) {
 345      $exitcodes = array();
 346      $maxdotsonline = 70;
 347      $remainingprintlen = $maxdotsonline;
 348      $progresscount = 0;
 349      echo "Dropping tables:" . PHP_EOL;
 350  
 351      while (count($exitcodes) != count($processes)) {
 352          usleep(10000);
 353          foreach ($processes as $name => $process) {
 354              if ($process->isRunning()) {
 355                  $op = $process->getIncrementalOutput();
 356                  if (trim($op)) {
 357                      $update = preg_filter('#^\s*([FS\.\-]+)(?:\s+\d+)?\s*$#', '$1', $op);
 358                      $strlentoprint = $update ? strlen($update) : 0;
 359  
 360                      // If not enough dots printed on line then just print.
 361                      if ($strlentoprint < $remainingprintlen) {
 362                          echo $update;
 363                          $remainingprintlen = $remainingprintlen - $strlentoprint;
 364                      } else if ($strlentoprint == $remainingprintlen) {
 365                          $progresscount += $maxdotsonline;
 366                          echo $update . " " . $progresscount . PHP_EOL;
 367                          $remainingprintlen = $maxdotsonline;
 368                      } else {
 369                          while ($part = substr($update, 0, $remainingprintlen) > 0) {
 370                              $progresscount += $maxdotsonline;
 371                              echo $part . " " . $progresscount . PHP_EOL;
 372                              $update = substr($update, $remainingprintlen);
 373                              $remainingprintlen = $maxdotsonline;
 374                          }
 375                      }
 376                  }
 377              } else {
 378                  // Process exited.
 379                  $process->clearOutput();
 380                  $exitcodes[$name] = $process->getExitCode();
 381              }
 382          }
 383      }
 384  
 385      echo PHP_EOL;
 386      return $exitcodes;
 387  }
 388  
 389  /**
 390   * Print install output merging each run.
 391   *
 392   * @param array $processes list of processes.
 393   * @return array exit codes of each process.
 394   */
 395  function print_combined_install_output($processes) {
 396      $exitcodes = array();
 397      $line = array();
 398  
 399      // Check what best we can do to accommodate  all parallel run o/p on single line.
 400      // Windows command line has length of 80 chars, so default we will try fit o/p in 80 chars.
 401      if (defined('BEHAT_MAX_CMD_LINE_OUTPUT') && BEHAT_MAX_CMD_LINE_OUTPUT) {
 402          $lengthofprocessline = (int)max(10, BEHAT_MAX_CMD_LINE_OUTPUT / count($processes));
 403      } else {
 404          $lengthofprocessline = (int)max(10, 80 / count($processes));
 405      }
 406  
 407      echo "Installing behat site for " . count($processes) . " parallel behat run" . PHP_EOL;
 408  
 409      // Show process name in first row.
 410      foreach ($processes as $name => $process) {
 411          // If we don't have enough space to show full run name then show runX.
 412          if ($lengthofprocessline < strlen($name) + 2) {
 413              $name = substr($name, -5);
 414          }
 415          // One extra padding as we are adding | separator for rest of the data.
 416          $line[$name] = str_pad('[' . $name . '] ', $lengthofprocessline + 1);
 417      }
 418      ksort($line);
 419      $tableheader = array_keys($line);
 420      echo implode("", $line) . PHP_EOL;
 421  
 422      // Now print o/p from each process.
 423      while (count($exitcodes) != count($processes)) {
 424          usleep(50000);
 425          $poutput = array();
 426          // Create child process.
 427          foreach ($processes as $name => $process) {
 428              if ($process->isRunning()) {
 429                  $output = $process->getIncrementalOutput();
 430                  if (trim($output)) {
 431                      $poutput[$name] = explode(PHP_EOL, $output);
 432                  }
 433              } else {
 434                  // Process exited.
 435                  $exitcodes[$name] = $process->getExitCode();
 436              }
 437          }
 438          ksort($poutput);
 439  
 440          // Get max depth of o/p before displaying.
 441          $maxdepth = 0;
 442          foreach ($poutput as $pout) {
 443              $pdepth = count($pout);
 444              $maxdepth = $pdepth >= $maxdepth ? $pdepth : $maxdepth;
 445          }
 446  
 447          // Iterate over each process to get line to print.
 448          for ($i = 0; $i <= $maxdepth; $i++) {
 449              $pline = "";
 450              foreach ($tableheader as $name) {
 451                  $po = empty($poutput[$name][$i]) ? "" : substr($poutput[$name][$i], 0, $lengthofprocessline - 1);
 452                  $po = str_pad($po, $lengthofprocessline);
 453                  $pline .= "|". $po;
 454              }
 455              if (trim(str_replace("|", "", $pline))) {
 456                  echo $pline . PHP_EOL;
 457              }
 458          }
 459          unset($poutput);
 460          $poutput = null;
 461  
 462      }
 463      echo PHP_EOL;
 464      return $exitcodes;
 465  }
 466  
 467  /**
 468   * Print install output merging showing one run at a time.
 469   * If any process fail then exit.
 470   *
 471   * @param array $processes list of processes.
 472   * @param bool $showprefix show prefix.
 473   * @return bool exitcode.
 474   */
 475  function print_sequential_output($processes, $showprefix = true) {
 476      $status = false;
 477      foreach ($processes as $name => $process) {
 478          $shownname = false;
 479          while ($process->isRunning()) {
 480              $op = $process->getIncrementalOutput();
 481              if (trim($op)) {
 482                  // Show name of the run once for sequential.
 483                  if ($showprefix && !$shownname) {
 484                      echo '[' . $name . '] ';
 485                      $shownname = true;
 486                  }
 487                  echo $op;
 488              }
 489          }
 490          // If any error then exit.
 491          $exitcode = $process->getExitCode();
 492          if ($exitcode != 0) {
 493              exit($exitcode);
 494          }
 495          $status = $status || (bool)$exitcode;
 496      }
 497      return $status;
 498  }