Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 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.

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

   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() {
 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);
 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          global $CFG;
 282  
 283          if (!defined('BEHAT_UTIL')) {
 284              throw new coding_exception('This method can be only used by Behat CLI tool');
 285          }
 286  
 287          // Checks the behat set up and the PHP version.
 288          if ($errorcode = behat_command::behat_setup_problem()) {
 289              exit($errorcode);
 290          }
 291  
 292          // Check that test environment is correctly set up.
 293          self::test_environment_problem();
 294  
 295          // Updates all the Moodle features and steps definitions.
 296          behat_config_manager::update_config_file('', true, $tags, $themesuitewithallfeatures, $parallelruns, $run);
 297  
 298          if (self::is_test_mode_enabled()) {
 299              return;
 300          }
 301  
 302          $contents = '$CFG->behat_wwwroot, $CFG->behat_prefix and $CFG->behat_dataroot' .
 303              ' are currently used as $CFG->wwwroot, $CFG->prefix and $CFG->dataroot';
 304          $filepath = self::get_test_file_path();
 305          if (!file_put_contents($filepath, $contents)) {
 306              behat_error(BEHAT_EXITCODE_PERMISSIONS, 'File ' . $filepath . ' can not be created');
 307          }
 308      }
 309  
 310      /**
 311       * Returns the status of the behat test environment
 312       *
 313       * @return int Error code
 314       */
 315      public static function get_behat_status() {
 316  
 317          if (!defined('BEHAT_UTIL')) {
 318              throw new coding_exception('This method can be only used by Behat CLI tool');
 319          }
 320  
 321          // Checks the behat set up and the PHP version, returning an error code if something went wrong.
 322          if ($errorcode = behat_command::behat_setup_problem()) {
 323              return $errorcode;
 324          }
 325  
 326          // Check that test environment is correctly set up, stops execution.
 327          self::test_environment_problem();
 328      }
 329  
 330      /**
 331       * Disables test mode
 332       * @throws coding_exception
 333       * @return void
 334       */
 335      public static function stop_test_mode() {
 336  
 337          if (!defined('BEHAT_UTIL')) {
 338              throw new coding_exception('This method can be only used by Behat CLI tool');
 339          }
 340  
 341          $testenvfile = self::get_test_file_path();
 342          behat_config_manager::set_behat_run_config_value('behatsiteenabled', 0);
 343  
 344          if (!self::is_test_mode_enabled()) {
 345              echo "Test environment was already disabled\n";
 346          } else {
 347              if (!unlink($testenvfile)) {
 348                  behat_error(BEHAT_EXITCODE_PERMISSIONS, 'Can not delete test environment file');
 349              }
 350          }
 351      }
 352  
 353      /**
 354       * Checks whether test environment is enabled or disabled
 355       *
 356       * To check is the current script is running in the test
 357       * environment
 358       *
 359       * @return bool
 360       */
 361      public static function is_test_mode_enabled() {
 362  
 363          $testenvfile = self::get_test_file_path();
 364          if (file_exists($testenvfile)) {
 365              return true;
 366          }
 367  
 368          return false;
 369      }
 370  
 371      /**
 372       * Returns the path to the file which specifies if test environment is enabled
 373       * @return string
 374       */
 375      public final static function get_test_file_path() {
 376          return behat_command::get_parent_behat_dir() . '/test_environment_enabled.txt';
 377      }
 378  
 379      /**
 380       * Removes config settings that were added to the main $CFG config within the Behat CLI
 381       * run.
 382       *
 383       * Database storage is already handled by reset_database and existing config values will
 384       * be reset automatically by initialise_cfg(), so we only need to remove added ones.
 385       */
 386      public static function remove_added_config() {
 387          global $CFG;
 388          if (!empty($CFG->behat_cli_added_config)) {
 389              foreach ($CFG->behat_cli_added_config as $key => $value) {
 390                  unset($CFG->{$key});
 391              }
 392              unset($CFG->behat_cli_added_config);
 393          }
 394      }
 395  
 396      /**
 397       * Reset contents of all database tables to initial values, reset caches, etc.
 398       */
 399      public static function reset_all_data() {
 400          // Reset database.
 401          self::reset_database();
 402  
 403          // Purge dataroot directory.
 404          self::reset_dataroot();
 405  
 406          // Reset all static caches.
 407          accesslib_clear_all_caches(true);
 408          accesslib_reset_role_cache();
 409          // Reset the nasty strings list used during the last test.
 410          nasty_strings::reset_used_strings();
 411  
 412          filter_manager::reset_caches();
 413  
 414          // Reset course and module caches.
 415          if (class_exists('format_base')) {
 416              // If file containing class is not loaded, there is no cache there anyway.
 417              format_base::reset_course_cache(0);
 418          }
 419          get_fast_modinfo(0, 0, true);
 420  
 421          // Inform data generator.
 422          self::get_data_generator()->reset();
 423  
 424          // Initialise $CFG with default values. This is needed for behat cli process, so we don't have modified
 425          // $CFG values from the old run. @see set_config.
 426          self::remove_added_config();
 427          initialise_cfg();
 428      }
 429  
 430      /**
 431       * Restore theme CSS stored during behat setup.
 432       */
 433      public static function restore_saved_themes(): void {
 434          global $CFG;
 435  
 436          $themerev = theme_get_revision();
 437  
 438          $framework = self::get_framework();
 439          $storageroot = self::get_dataroot() . "/{$framework}/themedata";
 440          $themenames = array_keys(\core_component::get_plugin_list('theme'));
 441          $directions = ['ltr', 'rtl'];
 442  
 443          $themeconfigs = array_map(function($themename) {
 444              return \theme_config::load($themename);
 445          }, $themenames);
 446  
 447          foreach ($themeconfigs as $themeconfig) {
 448              $themename = $themeconfig->name;
 449              $themesubrev = theme_get_sub_revision_for_theme($themename);
 450  
 451              $dirname = "{$storageroot}/{$themename}";
 452              foreach ($directions as $direction) {
 453                  $cssfile = "{$dirname}/{$direction}.css";
 454                  if (file_exists($cssfile)) {
 455                      $themeconfig->set_css_content_cache(file_get_contents($cssfile));
 456                  }
 457              }
 458          }
 459      }
 460  
 461      /**
 462       * Pause execution immediately.
 463       *
 464       * @param Session $session
 465       * @param string $message The message to show when pausing.
 466       * This will be passed through cli_ansi_format so appropriate ANSI formatting and features are available.
 467       */
 468      public static function pause(Session $session, string $message): void {
 469          $posixexists = function_exists('posix_isatty');
 470  
 471          // Make sure this step is only used with interactive terminal (if detected).
 472          if ($posixexists && !@posix_isatty(STDOUT)) {
 473              throw new ExpectationException('Break point should only be used with interactive terminal.', $session);
 474          }
 475  
 476          // Save the cursor position, ring the bell, and add a new line.
 477          fwrite(STDOUT, cli_ansi_format("<cursor:save><bell><newline>"));
 478  
 479          // Output the formatted message and reset colour back to normal.
 480          $formattedmessage = cli_ansi_format("{$message}<colour:normal>");
 481          fwrite(STDOUT, $formattedmessage);
 482  
 483          // Wait for input.
 484          fread(STDIN, 1024);
 485  
 486          // Move the cursor back up to the previous position, then restore the original position stored earlier, and move
 487          // it back down again.
 488          fwrite(STDOUT, cli_ansi_format("<cursor:up><cursor:up><cursor:restore><cursor:down><cursor:down>"));
 489  
 490          // Add any extra lines back if the provided message was spread over multiple lines.
 491          $linecount = count(explode("\n", $formattedmessage));
 492          fwrite(STDOUT, str_repeat(cli_ansi_format("<cursor:down>"), $linecount - 1));
 493      }
 494  }