Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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          // Keeps the current version of database and dataroot.
 129          self::store_versions_hash();
 130  
 131          // Stores the database contents for fast reset.
 132          self::store_database_state();
 133      }
 134  
 135      /**
 136       * Build theme CSS.
 137       */
 138      public static function build_themes($mtraceprogress = false) {
 139          global $CFG;
 140          require_once("{$CFG->libdir}/outputlib.php");
 141  
 142          $themenames = array_keys(\core_component::get_plugin_list('theme'));
 143  
 144          // Load the theme configs.
 145          $themeconfigs = array_map(function($themename) {
 146              return \theme_config::load($themename);
 147          }, $themenames);
 148  
 149          // Build the list of themes and cache them in local cache.
 150          $themes = theme_build_css_for_themes($themeconfigs, ['ltr'], true, $mtraceprogress);
 151  
 152          $framework = self::get_framework();
 153          $storageroot = self::get_dataroot() . "/{$framework}/themedata";
 154  
 155          foreach ($themes as $themename => $themedata) {
 156              $dirname = "{$storageroot}/{$themename}";
 157              check_dir_exists($dirname);
 158              foreach ($themedata as $direction => $css) {
 159                  file_put_contents("{$dirname}/{$direction}.css", $css);
 160              }
 161          }
 162      }
 163  
 164      /**
 165       * Drops dataroot and remove test database tables
 166       * @throws coding_exception
 167       * @return void
 168       */
 169      public static function drop_site() {
 170  
 171          if (!defined('BEHAT_UTIL')) {
 172              throw new coding_exception('This method can be only used by Behat CLI tool');
 173          }
 174  
 175          self::reset_dataroot();
 176          self::drop_database(true);
 177          self::drop_dataroot();
 178      }
 179  
 180      /**
 181       * Delete files and directories under dataroot.
 182       */
 183      public static function drop_dataroot() {
 184          global $CFG;
 185  
 186          // As behat directory is now created under default $CFG->behat_dataroot_parent, so remove the whole dir.
 187          if ($CFG->behat_dataroot !== $CFG->behat_dataroot_parent) {
 188              remove_dir($CFG->behat_dataroot, false);
 189          } else {
 190              // It should never come here.
 191              throw new moodle_exception("Behat dataroot should not be same as parent behat data root.");
 192          }
 193      }
 194  
 195      /**
 196       * Checks if $CFG->behat_wwwroot is available and using same versions for cli and web.
 197       *
 198       * @return void
 199       */
 200      public static function check_server_status() {
 201          global $CFG;
 202  
 203          $url = $CFG->behat_wwwroot . '/admin/tool/behat/tests/behat/fixtures/environment.php';
 204  
 205          // Get web versions used by behat site.
 206          $ch = curl_init($url);
 207          curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 208          $result = curl_exec($ch);
 209          $statuscode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
 210          curl_close($ch);
 211  
 212          if ($statuscode !== 200 || empty($result) || (!$result = json_decode($result, true))) {
 213  
 214              behat_error (BEHAT_EXITCODE_REQUIREMENT, $CFG->behat_wwwroot . ' is not available, ensure you specified ' .
 215                  'correct url and that the server is set up and started.' . PHP_EOL . ' More info in ' .
 216                  behat_command::DOCS_URL . PHP_EOL);
 217          }
 218  
 219          // Check if cli version is same as web version.
 220          $clienv = self::get_environment();
 221          if ($result != $clienv) {
 222              $output = 'Differences detected between cli and webserver...'.PHP_EOL;
 223              foreach ($result as $key => $version) {
 224                  if ($clienv[$key] != $version) {
 225                      $output .= ' ' . $key . ': ' . PHP_EOL;
 226                      $output .= ' - web server: ' . $version . PHP_EOL;
 227                      $output .= ' - cli: ' . $clienv[$key] . PHP_EOL;
 228                  }
 229              }
 230              echo $output;
 231              ob_flush();
 232          }
 233      }
 234  
 235      /**
 236       * Checks whether the test database and dataroot is ready
 237       * Stops execution if something went wrong
 238       * @throws coding_exception
 239       * @return void
 240       */
 241      protected static function test_environment_problem() {
 242          global $CFG, $DB;
 243  
 244          if (!defined('BEHAT_UTIL')) {
 245              throw new coding_exception('This method can be only used by Behat CLI tool');
 246          }
 247  
 248          if (!self::is_test_site()) {
 249              behat_error(1, 'This is not a behat test site!');
 250          }
 251  
 252          $tables = $DB->get_tables(false);
 253          if (empty($tables)) {
 254              behat_error(BEHAT_EXITCODE_INSTALL, '');
 255          }
 256  
 257          if (!self::is_test_data_updated()) {
 258              behat_error(BEHAT_EXITCODE_REINSTALL, 'The test environment was initialised for a different version');
 259          }
 260      }
 261  
 262      /**
 263       * Enables test mode
 264       *
 265       * It uses CFG->behat_dataroot
 266       *
 267       * Starts the test mode checking the composer installation and
 268       * the test environment and updating the available
 269       * features and steps definitions.
 270       *
 271       * Stores a file in dataroot/behat to allow Moodle to switch
 272       * to the test environment when using cli-server.
 273       * @param bool $themesuitewithallfeatures List themes to include core features.
 274       * @param string $tags comma separated tag, which will be given preference while distributing features in parallel run.
 275       * @param int $parallelruns number of parallel runs.
 276       * @param int $run current run.
 277       * @throws coding_exception
 278       * @return void
 279       */
 280      public static function start_test_mode($themesuitewithallfeatures = false, $tags = '', $parallelruns = 0, $run = 0) {
 281  
 282          if (!defined('BEHAT_UTIL')) {
 283              throw new coding_exception('This method can be only used by Behat CLI tool');
 284          }
 285  
 286          // Checks the behat set up and the PHP version.
 287          if ($errorcode = behat_command::behat_setup_problem()) {
 288              exit($errorcode);
 289          }
 290  
 291          // Check that test environment is correctly set up.
 292          self::test_environment_problem();
 293  
 294          // Updates all the Moodle features and steps definitions.
 295          behat_config_manager::update_config_file('', true, $tags, $themesuitewithallfeatures, $parallelruns, $run);
 296  
 297          if (self::is_test_mode_enabled()) {
 298              return;
 299          }
 300  
 301          $contents = '$CFG->behat_wwwroot, $CFG->behat_prefix and $CFG->behat_dataroot' .
 302              ' are currently used as $CFG->wwwroot, $CFG->prefix and $CFG->dataroot';
 303          $filepath = self::get_test_file_path();
 304          if (!file_put_contents($filepath, $contents)) {
 305              behat_error(BEHAT_EXITCODE_PERMISSIONS, 'File ' . $filepath . ' can not be created');
 306          }
 307      }
 308  
 309      /**
 310       * Returns the status of the behat test environment
 311       *
 312       * @return int Error code
 313       */
 314      public static function get_behat_status() {
 315  
 316          if (!defined('BEHAT_UTIL')) {
 317              throw new coding_exception('This method can be only used by Behat CLI tool');
 318          }
 319  
 320          // Checks the behat set up and the PHP version, returning an error code if something went wrong.
 321          if ($errorcode = behat_command::behat_setup_problem()) {
 322              return $errorcode;
 323          }
 324  
 325          // Check that test environment is correctly set up, stops execution.
 326          self::test_environment_problem();
 327      }
 328  
 329      /**
 330       * Disables test mode
 331       * @throws coding_exception
 332       * @return void
 333       */
 334      public static function stop_test_mode() {
 335  
 336          if (!defined('BEHAT_UTIL')) {
 337              throw new coding_exception('This method can be only used by Behat CLI tool');
 338          }
 339  
 340          $testenvfile = self::get_test_file_path();
 341          behat_config_manager::set_behat_run_config_value('behatsiteenabled', 0);
 342  
 343          if (!self::is_test_mode_enabled()) {
 344              echo "Test environment was already disabled\n";
 345          } else {
 346              if (!unlink($testenvfile)) {
 347                  behat_error(BEHAT_EXITCODE_PERMISSIONS, 'Can not delete test environment file');
 348              }
 349          }
 350      }
 351  
 352      /**
 353       * Checks whether test environment is enabled or disabled
 354       *
 355       * To check is the current script is running in the test
 356       * environment
 357       *
 358       * @return bool
 359       */
 360      public static function is_test_mode_enabled() {
 361  
 362          $testenvfile = self::get_test_file_path();
 363          if (file_exists($testenvfile)) {
 364              return true;
 365          }
 366  
 367          return false;
 368      }
 369  
 370      /**
 371       * Returns the path to the file which specifies if test environment is enabled
 372       * @return string
 373       */
 374      public final static function get_test_file_path() {
 375          return behat_command::get_parent_behat_dir() . '/test_environment_enabled.txt';
 376      }
 377  
 378      /**
 379       * Removes config settings that were added to the main $CFG config within the Behat CLI
 380       * run.
 381       *
 382       * Database storage is already handled by reset_database and existing config values will
 383       * be reset automatically by initialise_cfg(), so we only need to remove added ones.
 384       */
 385      public static function remove_added_config() {
 386          global $CFG;
 387          if (!empty($CFG->behat_cli_added_config)) {
 388              foreach ($CFG->behat_cli_added_config as $key => $value) {
 389                  unset($CFG->{$key});
 390              }
 391              unset($CFG->behat_cli_added_config);
 392          }
 393      }
 394  
 395      /**
 396       * Reset contents of all database tables to initial values, reset caches, etc.
 397       */
 398      public static function reset_all_data() {
 399          // Reset database.
 400          self::reset_database();
 401  
 402          // Purge dataroot directory.
 403          self::reset_dataroot();
 404  
 405          // Reset all static caches.
 406          accesslib_clear_all_caches(true);
 407          accesslib_reset_role_cache();
 408          // Reset the nasty strings list used during the last test.
 409          nasty_strings::reset_used_strings();
 410  
 411          filter_manager::reset_caches();
 412  
 413          // Reset course and module caches.
 414          if (class_exists('format_base')) {
 415              // If file containing class is not loaded, there is no cache there anyway.
 416              format_base::reset_course_cache(0);
 417          }
 418          get_fast_modinfo(0, 0, true);
 419  
 420          // Inform data generator.
 421          self::get_data_generator()->reset();
 422  
 423          // Initialise $CFG with default values. This is needed for behat cli process, so we don't have modified
 424          // $CFG values from the old run. @see set_config.
 425          self::remove_added_config();
 426          initialise_cfg();
 427      }
 428  
 429      /**
 430       * Restore theme CSS stored during behat setup.
 431       */
 432      public static function restore_saved_themes(): void {
 433          global $CFG;
 434  
 435          $themerev = theme_get_revision();
 436  
 437          $framework = self::get_framework();
 438          $storageroot = self::get_dataroot() . "/{$framework}/themedata";
 439          $themenames = array_keys(\core_component::get_plugin_list('theme'));
 440          $directions = ['ltr', 'rtl'];
 441  
 442          $themeconfigs = array_map(function($themename) {
 443              return \theme_config::load($themename);
 444          }, $themenames);
 445  
 446          foreach ($themeconfigs as $themeconfig) {
 447              $themename = $themeconfig->name;
 448              $themesubrev = theme_get_sub_revision_for_theme($themename);
 449  
 450              $dirname = "{$storageroot}/{$themename}";
 451              foreach ($directions as $direction) {
 452                  $cssfile = "{$dirname}/{$direction}.css";
 453                  if (file_exists($cssfile)) {
 454                      $themeconfig->set_css_content_cache(file_get_contents($cssfile));
 455                  }
 456              }
 457          }
 458      }
 459  
 460      /**
 461       * Pause execution immediately.
 462       *
 463       * @param Session $session
 464       * @param string $message The message to show when pausing.
 465       * This will be passed through cli_ansi_format so appropriate ANSI formatting and features are available.
 466       */
 467      public static function pause(Session $session, string $message): void {
 468          $posixexists = function_exists('posix_isatty');
 469  
 470          // Make sure this step is only used with interactive terminal (if detected).
 471          if ($posixexists && !@posix_isatty(STDOUT)) {
 472              throw new ExpectationException('Break point should only be used with interactive terminal.', $session);
 473          }
 474  
 475          // Save the cursor position, ring the bell, and add a new line.
 476          fwrite(STDOUT, cli_ansi_format("<cursor:save><bell><newline>"));
 477  
 478          // Output the formatted message and reset colour back to normal.
 479          $formattedmessage = cli_ansi_format("{$message}<colour:normal>");
 480          fwrite(STDOUT, $formattedmessage);
 481  
 482          // Wait for input.
 483          fread(STDIN, 1024);
 484  
 485          // Move the cursor back up to the previous position, then restore the original position stored earlier, and move
 486          // it back down again.
 487          fwrite(STDOUT, cli_ansi_format("<cursor:up><cursor:up><cursor:restore><cursor:down><cursor:down>"));
 488  
 489          // Add any extra lines back if the provided message was spread over multiple lines.
 490          $linecount = count(explode("\n", $formattedmessage));
 491          fwrite(STDOUT, str_repeat(cli_ansi_format("<cursor:down>"), $linecount - 1));
 492      }
 493  
 494      /**
 495       * Gets a text-based site version description.
 496       *
 497       * @return string The site info
 498       */
 499      public static function get_site_info() {
 500          $siteinfo = parent::get_site_info();
 501  
 502          $accessibility = empty(behat_config_manager::get_behat_run_config_value('axe')) ? 'No' : 'Yes';
 503  
 504          $siteinfo .= <<<EOF
 505  Run optional tests:
 506  - Accessibility: {$accessibility}
 507  
 508  EOF;
 509  
 510          return $siteinfo;
 511      }
 512  }