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   * Test plan generator.
  19   *
  20   * @package tool_generator
  21   * @copyright 2013 David MonllaĆ³
  22   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  /**
  28   * Generates the files required by JMeter.
  29   *
  30   * @package tool_generator
  31   * @copyright 2013 David MonllaĆ³
  32   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  class tool_generator_testplan_backend extends tool_generator_backend {
  35  
  36      /**
  37       * @var The URL to the repository of the external project.
  38       */
  39      protected static $repourl = 'https://github.com/moodlehq/moodle-performance-comparison';
  40  
  41      /**
  42       * @var Number of users depending on the selected size.
  43       */
  44      protected static $users = array(1, 30, 100, 1000, 5000, 10000);
  45  
  46      /**
  47       * @var Number of loops depending on the selected size.
  48       */
  49      protected static $loops = array(5, 5, 5, 6, 6, 7);
  50  
  51      /**
  52       * @var Rampup period depending on the selected size.
  53       */
  54      protected static $rampups = array(1, 6, 40, 100, 500, 800);
  55  
  56      /**
  57       * Gets a list of size choices supported by this backend.
  58       *
  59       * @return array List of size (int) => text description for display
  60       */
  61      public static function get_size_choices() {
  62  
  63          $options = array();
  64          for ($size = self::MIN_SIZE; $size <= self::MAX_SIZE; $size++) {
  65              $a = new stdClass();
  66              $a->users = self::$users[$size];
  67              $a->loops = self::$loops[$size];
  68              $a->rampup = self::$rampups[$size];
  69              $options[$size] = get_string('testplansize_' . $size, 'tool_generator', $a);
  70          }
  71          return $options;
  72      }
  73  
  74      /**
  75       * Getter for moodle-performance-comparison project URL.
  76       *
  77       * @return string
  78       */
  79      public static function get_repourl() {
  80          return self::$repourl;
  81      }
  82  
  83      /**
  84       * Creates the test plan file.
  85       *
  86       * @param int $courseid The target course id
  87       * @param int $size The test plan size
  88       * @return stored_file
  89       */
  90      public static function create_testplan_file($courseid, $size) {
  91          $jmxcontents = self::generate_test_plan($courseid, $size);
  92  
  93          $fs = get_file_storage();
  94          $filerecord = self::get_file_record('testplan', 'jmx');
  95          return $fs->create_file_from_string($filerecord, $jmxcontents);
  96      }
  97  
  98      /**
  99       * Creates the users data file.
 100       *
 101       * @param int $courseid The target course id
 102       * @param bool $updateuserspassword Updates the course users password to $CFG->tool_generator_users_password
 103       * @param int|null $size of the test plan. Used to limit the number of users exported
 104       *                 to match the threads in the plan. For BC, defaults to null that means all enrolled users.
 105       * @return stored_file
 106       */
 107      public static function create_users_file($courseid, $updateuserspassword, ?int $size = null) {
 108          $csvcontents = self::generate_users_file($courseid, $updateuserspassword, $size);
 109  
 110          $fs = get_file_storage();
 111          $filerecord = self::get_file_record('users', 'csv');
 112          return $fs->create_file_from_string($filerecord, $csvcontents);
 113      }
 114  
 115      /**
 116       * Generates the test plan according to the target course contents.
 117       *
 118       * @param int $targetcourseid The target course id
 119       * @param int $size The test plan size
 120       * @return string The test plan as a string
 121       */
 122      protected static function generate_test_plan($targetcourseid, $size) {
 123          global $CFG;
 124  
 125          // Getting the template.
 126          $template = file_get_contents(__DIR__ . '/../testplan.template.jmx');
 127  
 128          // Getting the course modules data.
 129          $coursedata = self::get_course_test_data($targetcourseid);
 130  
 131          // Host and path to the site.
 132          $urlcomponents = parse_url($CFG->wwwroot);
 133          if (empty($urlcomponents['path'])) {
 134              $urlcomponents['path'] = '';
 135          }
 136  
 137          $replacements = array(
 138              $CFG->version,
 139              self::$users[$size],
 140              self::$loops[$size],
 141              self::$rampups[$size],
 142              $urlcomponents['host'],
 143              $urlcomponents['path'],
 144              get_string('shortsize_' . $size, 'tool_generator'),
 145              $targetcourseid,
 146              $coursedata->pageid,
 147              $coursedata->forumid,
 148              $coursedata->forumdiscussionid,
 149              $coursedata->forumreplyid
 150          );
 151  
 152          $placeholders = array(
 153              '{{MOODLEVERSION_PLACEHOLDER}}',
 154              '{{USERS_PLACEHOLDER}}',
 155              '{{LOOPS_PLACEHOLDER}}',
 156              '{{RAMPUP_PLACEHOLDER}}',
 157              '{{HOST_PLACEHOLDER}}',
 158              '{{SITEPATH_PLACEHOLDER}}',
 159              '{{SIZE_PLACEHOLDER}}',
 160              '{{COURSEID_PLACEHOLDER}}',
 161              '{{PAGEACTIVITYID_PLACEHOLDER}}',
 162              '{{FORUMACTIVITYID_PLACEHOLDER}}',
 163              '{{FORUMDISCUSSIONID_PLACEHOLDER}}',
 164              '{{FORUMREPLYID_PLACEHOLDER}}'
 165          );
 166  
 167          // Fill the template with the target course values.
 168          return str_replace($placeholders, $replacements, $template);
 169      }
 170  
 171      /**
 172       * Generates the user's credentials file with all the course's users
 173       *
 174       * @param int $targetcourseid
 175       * @param bool $updateuserspassword Updates the course users password to $CFG->tool_generator_users_password
 176       * @param int|null $size of the test plan. Used to limit the number of users exported
 177       *                 to match the threads in the plan. For BC, defaults to null that means all enrolled users.
 178       * @return string The users csv file contents.
 179       */
 180      protected static function generate_users_file($targetcourseid, $updateuserspassword, ?int $size = null) {
 181          global $CFG;
 182  
 183          $coursecontext = context_course::instance($targetcourseid);
 184  
 185          // If requested, get the number of users (threads) to use in the plan. We only need those in the exported file.
 186          $planusers = self::$users[$size] ?? 0;
 187          $users = get_enrolled_users($coursecontext, '', 0, 'u.id, u.username, u.auth', 'u.username ASC', 0, $planusers);
 188          if (!$users) {
 189              throw new \moodle_exception('coursewithoutusers', 'tool_generator');
 190          }
 191  
 192          $lines = array();
 193          foreach ($users as $user) {
 194  
 195              // Updating password to the one set in config.php.
 196              if ($updateuserspassword) {
 197                  $userauth = get_auth_plugin($user->auth);
 198                  if (!$userauth->user_update_password($user, $CFG->tool_generator_users_password)) {
 199                      throw new \moodle_exception('errorpasswordupdate', 'auth');
 200                  }
 201              }
 202  
 203              // Here we already checked that $CFG->tool_generator_users_password is not null.
 204              $lines[] = $user->username . ',' . $CFG->tool_generator_users_password;
 205          }
 206  
 207          return implode(PHP_EOL, $lines);
 208      }
 209  
 210      /**
 211       * Returns a tool_generator file record
 212       *
 213       * @param string $filearea testplan or users
 214       * @param string $filetype The file extension jmx or csv
 215       * @return stdClass The file record to use when creating tool_generator files
 216       */
 217      protected static function get_file_record($filearea, $filetype) {
 218  
 219          $systemcontext = context_system::instance();
 220  
 221          $filerecord = new stdClass();
 222          $filerecord->contextid = $systemcontext->id;
 223          $filerecord->component = 'tool_generator';
 224          $filerecord->filearea = $filearea;
 225          $filerecord->itemid = 0;
 226          $filerecord->filepath = '/';
 227  
 228          // Random generated number to avoid concurrent execution problems.
 229          $filerecord->filename = $filearea . '_' . date('YmdHi', time()) . '_' . rand(1000, 9999) . '.' . $filetype;
 230  
 231          return $filerecord;
 232      }
 233  
 234      /**
 235       * Gets the data required to fill the test plan template with the database contents.
 236       *
 237       * @param int $targetcourseid The target course id
 238       * @return stdClass The ids required by the test plan
 239       */
 240      protected static function get_course_test_data($targetcourseid) {
 241          global $DB, $USER;
 242  
 243          $data = new stdClass();
 244  
 245          // Getting course contents info as the current user (will be an admin).
 246          $course = new stdClass();
 247          $course->id = $targetcourseid;
 248          $courseinfo = new course_modinfo($course, $USER->id);
 249  
 250          // Getting the first page module instance.
 251          if (!$pages = $courseinfo->get_instances_of('page')) {
 252              throw new \moodle_exception('error_nopageinstances', 'tool_generator');
 253          }
 254          $data->pageid = reset($pages)->id;
 255  
 256          // Getting the first forum module instance and it's first discussion and reply as well.
 257          if (!$forums = $courseinfo->get_instances_of('forum')) {
 258              throw new \moodle_exception('error_noforuminstances', 'tool_generator');
 259          }
 260          $forum = reset($forums);
 261  
 262          // Getting the first discussion (and reply).
 263          if (!$discussions = forum_get_discussions($forum, 'd.timemodified ASC', false, -1, 1)) {
 264              throw new \moodle_exception('error_noforumdiscussions', 'tool_generator');
 265          }
 266          $discussion = reset($discussions);
 267  
 268          $data->forumid = $forum->id;
 269          $data->forumdiscussionid = $discussion->discussion;
 270          $data->forumreplyid = $discussion->id;
 271  
 272          // According to the current test plan.
 273          return $data;
 274      }
 275  
 276      /**
 277       * Checks if the selected target course is ok.
 278       *
 279       * @param int|string $course
 280       * @param int $size
 281       * @return array Errors array or false if everything is ok
 282       */
 283      public static function has_selected_course_any_problem($course, $size) {
 284          global $DB;
 285  
 286          $errors = array();
 287  
 288          if (!is_numeric($course)) {
 289              if (!$course = $DB->get_field('course', 'id', array('shortname' => $course))) {
 290                  $errors['courseid'] = get_string('error_nonexistingcourse', 'tool_generator');
 291                  return $errors;
 292              }
 293          }
 294  
 295          $coursecontext = context_course::instance($course, IGNORE_MISSING);
 296          if (!$coursecontext) {
 297              $errors['courseid'] = get_string('error_nonexistingcourse', 'tool_generator');
 298              return $errors;
 299          }
 300  
 301          if (!$users = get_enrolled_users($coursecontext, '', 0, 'u.id')) {
 302              $errors['courseid'] = get_string('coursewithoutusers', 'tool_generator');
 303          }
 304  
 305          // Checks that the selected course has enough users.
 306          $coursesizes = tool_generator_course_backend::get_users_per_size();
 307          if (count($users) < self::$users[$size]) {
 308              $errors['size'] = get_string('notenoughusers', 'tool_generator');
 309          }
 310  
 311          if (empty($errors)) {
 312              return false;
 313          }
 314  
 315          return $errors;
 316      }
 317  }