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]

   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   * Utils for behat-related stuff
  19   *
  20   * @package    core
  21   * @category   test
  22   * @copyright  2012 David MonllaĆ³
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  require_once (__DIR__ . '/../lib.php');
  29  require_once (__DIR__ . '/../../testing/classes/util.php');
  30  require_once (__DIR__ . '/behat_command.php');
  31  require_once (__DIR__ . '/behat_config_manager.php');
  32  
  33  require_once (__DIR__ . '/../../filelib.php');
  34  require_once (__DIR__ . '/../../clilib.php');
  35  require_once (__DIR__ . '/../../csslib.php');
  36  
  37  use Behat\Mink\Session;
  38  use Behat\Mink\Exception\ExpectationException;
  39  
  40  /**
  41   * Init/reset utilities for Behat database and dataroot
  42   *
  43   * @package   core
  44   * @category  test
  45   * @copyright 2013 David MonllaĆ³
  46   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  47   */
  48  class behat_util extends testing_util {
  49  
  50      /**
  51       * The behat test site fullname and shortname.
  52       */
  53      const BEHATSITENAME = "Acceptance test site";
  54  
  55      /**
  56       * @var array Files to skip when resetting dataroot folder
  57       */
  58      protected static $datarootskiponreset = array('.', '..', 'behat', 'behattestdir.txt');
  59  
  60      /**
  61       * @var array Files to skip when dropping dataroot folder
  62       */
  63      protected static $datarootskipondrop = array('.', '..', 'lock');
  64  
  65      /**
  66       * Installs a site using $CFG->dataroot and $CFG->prefix
  67       * @throws coding_exception
  68       * @return void
  69       */
  70      public static function install_site() {
  71          global $DB, $CFG;
  72          require_once($CFG->dirroot.'/user/lib.php');
  73          if (!defined('BEHAT_UTIL')) {
  74              throw new coding_exception('This method can be only used by Behat CLI tool');
  75          }
  76  
  77          $tables = $DB->get_tables(false);
  78          if (!empty($tables)) {
  79              behat_error(BEHAT_EXITCODE_INSTALLED);
  80          }
  81  
  82          // New dataroot.
  83          self::reset_dataroot();
  84  
  85          $options = array();
  86          $options['adminuser'] = 'admin';
  87          $options['adminpass'] = 'admin';
  88          $options['fullname'] = self::BEHATSITENAME;
  89          $options['shortname'] = self::BEHATSITENAME;
  90  
  91          install_cli_database($options, false);
  92  
  93          // We need to keep the installed dataroot filedir files.
  94          // So each time we reset the dataroot before running a test, the default files are still installed.
  95          self::save_original_data_files();
  96  
  97          $frontpagesummary = new admin_setting_special_frontpagedesc();
  98          $frontpagesummary->write_setting(self::BEHATSITENAME);
  99  
 100          // Update admin user info.
 101          $user = $DB->get_record('user', array('username' => 'admin'));
 102          $user->email = 'moodle@example.com';
 103          $user->firstname = 'Admin';
 104          $user->lastname = 'User';
 105          $user->city = 'Perth';
 106          $user->country = 'AU';
 107          user_update_user($user, false);
 108  
 109          // Disable email message processor.
 110          $DB->set_field('message_processors', 'enabled', '0', array('name' => 'email'));
 111  
 112          // Sets maximum debug level.
 113          set_config('debug', DEBUG_DEVELOPER);
 114          set_config('debugdisplay', 1);
 115  
 116          // Disable some settings that are not wanted on test sites.
 117          set_config('noemailever', 1);
 118  
 119          // Enable web cron.
 120          set_config('cronclionly', 0);
 121  
 122          // Set editor autosave to high value, so as to avoid unwanted ajax.
 123          set_config('autosavefrequency', '604800', 'editor_atto');
 124  
 125          // Set noreplyaddress to an example domain, as it should be valid email address and test site can be a localhost.
 126          set_config('noreplyaddress', 'noreply@example.com');
 127  
 128          // Set the support email address.
 129          set_config('supportemail', 'email@example.com');
 130  
 131          // Remove any default blocked hosts and port restrictions, to avoid blocking tests (eg those using local files).
 132          set_config('curlsecurityblockedhosts', '');
 133          set_config('curlsecurityallowedport', '');
 134  
 135          // Execute all the adhoc tasks.
 136          while ($task = \core\task\manager::get_next_adhoc_task(time())) {
 137              $task->execute();
 138              \core\task\manager::adhoc_task_complete($task);
 139          }
 140  
 141          // Keeps the current version of database and dataroot.
 142          self::store_versions_hash();
 143  
 144          // Stores the database contents for fast reset.
 145          self::store_database_state();
 146      }
 147  
 148      /**
 149       * Build theme CSS.
 150       */
 151      public static function build_themes($mtraceprogress = false) {
 152          global $CFG;
 153          require_once("{$CFG->libdir}/outputlib.php");
 154  
 155          $themenames = array_keys(\core_component::get_plugin_list('theme'));
 156  
 157          // Load the theme configs.
 158          $themeconfigs = array_map(function($themename) {
 159              return \theme_config::load($themename);
 160          }, $themenames);
 161  
 162          // Build the list of themes and cache them in local cache.
 163          $themes = theme_build_css_for_themes($themeconfigs, ['ltr'], true, $mtraceprogress);
 164  
 165          $framework = self::get_framework();
 166          $storageroot = self::get_dataroot() . "/{$framework}/themedata";
 167  
 168          foreach ($themes as $themename => $themedata) {
 169              $dirname = "{$storageroot}/{$themename}";
 170              check_dir_exists($dirname);
 171              foreach ($themedata as $direction => $css) {
 172                  file_put_contents("{$dirname}/{$direction}.css", $css);
 173              }
 174          }
 175      }
 176  
 177      /**
 178       * Drops dataroot and remove test database tables
 179       * @throws coding_exception
 180       * @return void
 181       */
 182      public static function drop_site() {
 183  
 184          if (!defined('BEHAT_UTIL')) {
 185              throw new coding_exception('This method can be only used by Behat CLI tool');
 186          }
 187  
 188          self::reset_dataroot();
 189          self::drop_database(true);
 190          self::drop_dataroot();
 191      }
 192  
 193      /**
 194       * Delete files and directories under dataroot.
 195       */
 196      public static function drop_dataroot() {
 197          global $CFG;
 198  
 199          // As behat directory is now created under default $CFG->behat_dataroot_parent, so remove the whole dir.
 200          if ($CFG->behat_dataroot !== $CFG->behat_dataroot_parent) {
 201              remove_dir($CFG->behat_dataroot, false);
 202          } else {
 203              // It should never come here.
 204              throw new moodle_exception("Behat dataroot should not be same as parent behat data root.");
 205          }
 206      }
 207  
 208      /**
 209       * Checks if $CFG->behat_wwwroot is available and using same versions for cli and web.
 210       *
 211       * @return void
 212       */
 213      public static function check_server_status() {
 214          global $CFG;
 215  
 216          $url = $CFG->behat_wwwroot . '/admin/tool/behat/tests/behat/fixtures/environment.php';
 217  
 218          // Get web versions used by behat site.
 219          $ch = curl_init($url);
 220          curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 221          $result = curl_exec($ch);
 222          $statuscode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
 223          curl_close($ch);
 224  
 225          if ($statuscode !== 200 || empty($result) || (!$result = json_decode($result, true))) {
 226  
 227              behat_error (BEHAT_EXITCODE_REQUIREMENT, $CFG->behat_wwwroot . ' is not available, ensure you specified ' .
 228                  'correct url and that the server is set up and started.' . PHP_EOL . ' More info in ' .
 229                  behat_command::DOCS_URL . PHP_EOL);
 230          }
 231  
 232          // Check if cli version is same as web version.
 233          $clienv = self::get_environment();
 234          if ($result != $clienv) {
 235              $output = 'Differences detected between cli and webserver...'.PHP_EOL;
 236              foreach ($result as $key => $version) {
 237                  if ($clienv[$key] != $version) {
 238                      $output .= ' ' . $key . ': ' . PHP_EOL;
 239                      $output .= ' - web server: ' . $version . PHP_EOL;
 240                      $output .= ' - cli: ' . $clienv[$key] . PHP_EOL;
 241                  }
 242              }
 243              echo $output;
 244              ob_flush();
 245          }
 246      }
 247  
 248      /**
 249       * Checks whether the test database and dataroot is ready
 250       * Stops execution if something went wrong
 251       * @throws coding_exception
 252       * @return void
 253       */
 254      protected static function test_environment_problem() {
 255          global $CFG, $DB;
 256  
 257          if (!defined('BEHAT_UTIL')) {
 258              throw new coding_exception('This method can be only used by Behat CLI tool');
 259          }
 260  
 261          if (!self::is_test_site()) {
 262              behat_error(1, 'This is not a behat test site!');
 263          }
 264  
 265          $tables = $DB->get_tables(false);
 266          if (empty($tables)) {
 267              behat_error(BEHAT_EXITCODE_INSTALL, '');
 268          }
 269  
 270          if (!self::is_test_data_updated()) {
 271              behat_error(BEHAT_EXITCODE_REINSTALL, 'The test environment was initialised for a different version');
 272          }
 273      }
 274  
 275      /**
 276       * Enables test mode
 277       *
 278       * It uses CFG->behat_dataroot
 279       *
 280       * Starts the test mode checking the composer installation and
 281       * the test environment and updating the available
 282       * features and steps definitions.
 283       *
 284       * Stores a file in dataroot/behat to allow Moodle to switch
 285       * to the test environment when using cli-server.
 286       * @param bool $themesuitewithallfeatures List themes to include core features.
 287       * @param string $tags comma separated tag, which will be given preference while distributing features in parallel run.
 288       * @param int $parallelruns number of parallel runs.
 289       * @param int $run current run.
 290       * @throws coding_exception
 291       * @return void
 292       */
 293      public static function start_test_mode($themesuitewithallfeatures = false, $tags = '', $parallelruns = 0, $run = 0) {
 294  
 295          if (!defined('BEHAT_UTIL')) {
 296              throw new coding_exception('This method can be only used by Behat CLI tool');
 297          }
 298  
 299          // Checks the behat set up and the PHP version.
 300          if ($errorcode = behat_command::behat_setup_problem()) {
 301              exit($errorcode);
 302          }
 303  
 304          // Check that test environment is correctly set up.
 305          self::test_environment_problem();
 306  
 307          // Updates all the Moodle features and steps definitions.
 308          behat_config_manager::update_config_file('', true, $tags, $themesuitewithallfeatures, $parallelruns, $run);
 309  
 310          if (self::is_test_mode_enabled()) {
 311              return;
 312          }
 313  
 314          $contents = '$CFG->behat_wwwroot, $CFG->behat_prefix and $CFG->behat_dataroot' .
 315              ' are currently used as $CFG->wwwroot, $CFG->prefix and $CFG->dataroot';
 316          $filepath = self::get_test_file_path();
 317          if (!file_put_contents($filepath, $contents)) {
 318              behat_error(BEHAT_EXITCODE_PERMISSIONS, 'File ' . $filepath . ' can not be created');
 319          }
 320      }
 321  
 322      /**
 323       * Returns the status of the behat test environment
 324       *
 325       * @return int Error code
 326       */
 327      public static function get_behat_status() {
 328  
 329          if (!defined('BEHAT_UTIL')) {
 330              throw new coding_exception('This method can be only used by Behat CLI tool');
 331          }
 332  
 333          // Checks the behat set up and the PHP version, returning an error code if something went wrong.
 334          if ($errorcode = behat_command::behat_setup_problem()) {
 335              return $errorcode;
 336          }
 337  
 338          // Check that test environment is correctly set up, stops execution.
 339          self::test_environment_problem();
 340      }
 341  
 342      /**
 343       * Disables test mode
 344       * @throws coding_exception
 345       * @return void
 346       */
 347      public static function stop_test_mode() {
 348  
 349          if (!defined('BEHAT_UTIL')) {
 350              throw new coding_exception('This method can be only used by Behat CLI tool');
 351          }
 352  
 353          $testenvfile = self::get_test_file_path();
 354          behat_config_manager::set_behat_run_config_value('behatsiteenabled', 0);
 355  
 356          if (!self::is_test_mode_enabled()) {
 357              echo "Test environment was already disabled\n";
 358          } else {
 359              if (!unlink($testenvfile)) {
 360                  behat_error(BEHAT_EXITCODE_PERMISSIONS, 'Can not delete test environment file');
 361              }
 362          }
 363      }
 364  
 365      /**
 366       * Checks whether test environment is enabled or disabled
 367       *
 368       * To check is the current script is running in the test
 369       * environment
 370       *
 371       * @return bool
 372       */
 373      public static function is_test_mode_enabled() {
 374  
 375          $testenvfile = self::get_test_file_path();
 376          if (file_exists($testenvfile)) {
 377              return true;
 378          }
 379  
 380          return false;
 381      }
 382  
 383      /**
 384       * Returns the path to the file which specifies if test environment is enabled
 385       * @return string
 386       */
 387      public final static function get_test_file_path() {
 388          return behat_command::get_parent_behat_dir() . '/test_environment_enabled.txt';
 389      }
 390  
 391      /**
 392       * Removes config settings that were added to the main $CFG config within the Behat CLI
 393       * run.
 394       *
 395       * Database storage is already handled by reset_database and existing config values will
 396       * be reset automatically by initialise_cfg(), so we only need to remove added ones.
 397       */
 398      public static function remove_added_config() {
 399          global $CFG;
 400          if (!empty($CFG->behat_cli_added_config)) {
 401              foreach ($CFG->behat_cli_added_config as $key => $value) {
 402                  unset($CFG->{$key});
 403              }
 404              unset($CFG->behat_cli_added_config);
 405          }
 406      }
 407  
 408      /**
 409       * Reset contents of all database tables to initial values, reset caches, etc.
 410       */
 411      public static function reset_all_data() {
 412          // Reset database.
 413          self::reset_database();
 414  
 415          // Purge dataroot directory.
 416          self::reset_dataroot();
 417  
 418          // Reset all static caches.
 419          accesslib_clear_all_caches(true);
 420          accesslib_reset_role_cache();
 421          // Reset the nasty strings list used during the last test.
 422          nasty_strings::reset_used_strings();
 423  
 424          filter_manager::reset_caches();
 425  
 426          \core_reportbuilder\manager::reset_caches();
 427  
 428          // Reset course and module caches.
 429          core_courseformat\base::reset_course_cache(0);
 430          get_fast_modinfo(0, 0, true);
 431  
 432          // Inform data generator.
 433          self::get_data_generator()->reset();
 434  
 435          // Initialise $CFG with default values. This is needed for behat cli process, so we don't have modified
 436          // $CFG values from the old run. @see set_config.
 437          self::remove_added_config();
 438          initialise_cfg();
 439      }
 440  
 441      /**
 442       * Restore theme CSS stored during behat setup.
 443       */
 444      public static function restore_saved_themes(): void {
 445          global $CFG;
 446  
 447          $themerev = theme_get_revision();
 448  
 449          $framework = self::get_framework();
 450          $storageroot = self::get_dataroot() . "/{$framework}/themedata";
 451          $themenames = array_keys(\core_component::get_plugin_list('theme'));
 452          $directions = ['ltr', 'rtl'];
 453  
 454          $themeconfigs = array_map(function($themename) {
 455              return \theme_config::load($themename);
 456          }, $themenames);
 457  
 458          foreach ($themeconfigs as $themeconfig) {
 459              $themename = $themeconfig->name;
 460              $themesubrev = theme_get_sub_revision_for_theme($themename);
 461  
 462              $dirname = "{$storageroot}/{$themename}";
 463              foreach ($directions as $direction) {
 464                  $cssfile = "{$dirname}/{$direction}.css";
 465                  if (file_exists($cssfile)) {
 466                      $themeconfig->set_css_content_cache(file_get_contents($cssfile));
 467                  }
 468              }
 469          }
 470      }
 471  
 472      /**
 473       * Pause execution immediately.
 474       *
 475       * @param Session $session
 476       * @param string $message The message to show when pausing.
 477       * This will be passed through cli_ansi_format so appropriate ANSI formatting and features are available.
 478       */
 479      public static function pause(Session $session, string $message): void {
 480          $posixexists = function_exists('posix_isatty');
 481  
 482          // Make sure this step is only used with interactive terminal (if detected).
 483          if ($posixexists && !@posix_isatty(STDOUT)) {
 484              throw new ExpectationException('Break point should only be used with interactive terminal.', $session);
 485          }
 486  
 487          // Save the cursor position, ring the bell, and add a new line.
 488          fwrite(STDOUT, cli_ansi_format("<cursor:save><bell><newline>"));
 489  
 490          // Output the formatted message and reset colour back to normal.
 491          $formattedmessage = cli_ansi_format("{$message}<colour:normal>");
 492          fwrite(STDOUT, $formattedmessage);
 493  
 494          // Wait for input.
 495          fread(STDIN, 1024);
 496  
 497          // Move the cursor back up to the previous position, then restore the original position stored earlier, and move
 498          // it back down again.
 499          fwrite(STDOUT, cli_ansi_format("<cursor:up><cursor:up><cursor:restore><cursor:down><cursor:down>"));
 500  
 501          // Add any extra lines back if the provided message was spread over multiple lines.
 502          $linecount = count(explode("\n", $formattedmessage));
 503          fwrite(STDOUT, str_repeat(cli_ansi_format("<cursor:down>"), $linecount - 1));
 504      }
 505  
 506      /**
 507       * Gets a text-based site version description.
 508       *
 509       * @return string The site info
 510       */
 511      public static function get_site_info() {
 512          $siteinfo = parent::get_site_info();
 513  
 514          $accessibility = empty(behat_config_manager::get_behat_run_config_value('axe')) ? 'No' : 'Yes';
 515  
 516          $siteinfo .= <<<EOF
 517  Run optional tests:
 518  - Accessibility: {$accessibility}
 519  
 520  EOF;
 521  
 522          return $siteinfo;
 523      }
 524  }