first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Backend generic code.
*
* @package tool_generator
* @copyright 2013 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Backend generic code for all tool_generator commands.
*
* @abstract
* @package tool_generator
* @copyright 2013 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class tool_generator_backend {
/**
* @var int Lowest (smallest) size index
*/
const MIN_SIZE = 0;
/**
* @var int Highest (largest) size index
*/
const MAX_SIZE = 5;
/**
* @var int Default size index
*/
const DEFAULT_SIZE = 3;
/**
* @var bool True if we want a fixed dataset or false to generate random data
*/
protected $fixeddataset;
/**
* @var int|bool Maximum number of bytes for file.
*/
protected $filesizelimit;
/**
* @var bool True if displaying progress
*/
protected $progress;
/**
* @var int Epoch time at which current step (current set of dots) started
*/
protected $starttime;
/**
* @var int Size code (index in the above arrays)
*/
protected $size;
/**
* @var progress_bar progressbar
*/
protected $progressbar;
/**
* @var string Part of the language string.
*/
protected $langstring;
/**
* @var string Module for the language string.
*/
protected $module;
/**
* @var string|object|array|int Optional language string parameters.
*/
protected $aparam;
/**
* Generic generator class
*
* @param int $size Size as numeric index
* @param bool $fixeddataset To use fixed or random data
* @param int|bool $filesizelimit The max number of bytes for a generated file
* @param bool $progress True if progress information should be displayed
* @throws coding_exception If parameters are invalid
*/
public function __construct($size, $fixeddataset = false, $filesizelimit = false, $progress = true) {
// Check parameter.
if ($size < self::MIN_SIZE || $size > self::MAX_SIZE) {
throw new coding_exception('Invalid size');
}
// Set parameters.
$this->size = $size;
$this->fixeddataset = $fixeddataset;
$this->filesizelimit = $filesizelimit;
$this->progress = $progress;
}
/**
* Converts a size name into the numeric constant.
*
* @param string $sizename Size name e.g. 'L'
* @return int Numeric version
* @throws coding_exception If the size name is not known
*/
public static function size_for_name($sizename) {
for ($size = self::MIN_SIZE; $size <= self::MAX_SIZE; $size++) {
if ($sizename == get_string('shortsize_' . $size, 'tool_generator')) {
return $size;
}
}
throw new coding_exception("Unknown size name '$sizename'");
}
/**
* Displays information as part of progress.
*
* @param string $langstring Part of langstring (after progress_)
* @param mixed $a Optional lang string parameters
* @param bool $leaveopen If true, doesn't close LI tag (ready for dots)
* @param string $module module for language string
*/
public function log(string $langstring, $a = null, bool $leaveopen = false, string $module = 'tool_generator'): void {
if (!$this->progress) {
return;
}
$this->langstring = $langstring;
$this->module = $module;
$this->aparam = $a;
$this->starttime = microtime(true);
$this->progressbar = new progress_bar();
$this->progressbar->create();
}
/**
* Outputs dots. There is up to one dot per second. Once a minute, it
* displays a percentage.
*
* @param int $number Number of completed items
* @param int $total Total number of items to complete
*/
public function dot(int $number, int $total): void {
if (!$this->progress) {
return;
}
$now = time();
// Update time limit so PHP doesn't time out.
if (!CLI_SCRIPT) {
core_php_time_limit::raise(120);
}
$status = get_string('progress_' . $this->langstring, $this->module, $number);
$this->progressbar->update($number, $total, $status);
}
/**
* Ends a log string that was started using log function with $leaveopen.
*/
public function end_log(): void {
if (!$this->progress) {
return;
}
$status = get_string('progress_' . $this->langstring, $this->module, $this->aparam);
$done = get_string('done', 'tool_generator', round(microtime(true) - $this->starttime, 1));
$this->progressbar->update_full(100, $status . ' - ' . $done);
}
}
@@ -0,0 +1,621 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* tool_generator course backend code.
*
* @package tool_generator
* @copyright 2013 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Backend code for the 'make large course' tool.
*
* @package tool_generator
* @copyright 2013 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_generator_course_backend extends tool_generator_backend {
/**
* @var array Number of sections in course
*/
private static $paramsections = array(1, 10, 100, 500, 1000, 2000);
/**
* @var array Number of assignments in course
*/
private static $paramassignments = array(1, 10, 100, 500, 1000, 2000);
/**
* @var array Number of Page activities in course
*/
private static $parampages = array(1, 50, 200, 1000, 5000, 10000);
/**
* @var array Number of students enrolled in course
*/
private static $paramusers = array(1, 100, 1000, 10000, 50000, 100000);
/**
* Total size of small files: 1KB, 1MB, 10MB, 100MB, 1GB, 2GB.
*
* @var array Number of small files created in a single file activity
*/
private static $paramsmallfilecount = array(1, 64, 128, 1024, 16384, 32768);
/**
* @var array Size of small files (to make the totals into nice numbers)
*/
private static $paramsmallfilesize = array(1024, 16384, 81920, 102400, 65536, 65536);
/**
* Total size of big files: 8KB, 8MB, 80MB, 800MB, 8GB, 16GB.
*
* @var array Number of big files created as individual file activities
*/
private static $parambigfilecount = array(1, 2, 5, 10, 10, 10);
/**
* @var array Size of each large file
*/
private static $parambigfilesize = array(8192, 4194304, 16777216, 83886080,
858993459, 1717986918);
/**
* @var array Number of forum discussions
*/
private static $paramforumdiscussions = array(1, 10, 100, 500, 1000, 2000);
/**
* @var array Number of forum posts per discussion
*/
private static $paramforumposts = array(2, 2, 5, 10, 10, 10);
/**
* @var array Number of assignments in course
*/
private static $paramactivities = array(1, 10, 100, 500, 1000, 2000);
/**
* @var string Course shortname
*/
private $shortname;
/**
* @var string Course fullname.
*/
private $fullname = "";
/**
* @var string Course summary.
*/
private $summary = "";
/**
* @var string Course summary format, defaults to FORMAT_HTML.
*/
private $summaryformat = FORMAT_HTML;
/**
* @var testing_data_generator Data generator
*/
protected $generator;
/**
* @var stdClass Course object
*/
private $course;
/**
* @var array Array from test user number (1...N) to userid in database
*/
private $userids;
/**
* @var array $additionalmodules
*/
private $additionalmodules;
/**
* Constructs object ready to create course.
*
* @param string $shortname Course shortname
* @param int $size Size as numeric index
* @param bool $fixeddataset To use fixed or random data
* @param int|bool $filesizelimit The max number of bytes for a generated file
* @param bool $progress True if progress information should be displayed
* @param array $additionalmodules potential additional modules to be added (quiz, bigbluebutton...)
*/
public function __construct(
$shortname,
$size,
$fixeddataset = false,
$filesizelimit = false,
$progress = true,
$fullname = null,
$summary = null,
$summaryformat = FORMAT_HTML,
$additionalmodules = []
) {
// Set parameters.
$this->shortname = $shortname;
// We can't allow fullname to be set to an empty string.
if (empty($fullname)) {
$this->fullname = get_string(
'fullname',
'tool_generator',
array(
'size' => get_string('shortsize_' . $size, 'tool_generator')
)
);
} else {
$this->fullname = $fullname;
}
// Summary, on the other hand, should be empty-able.
if (!is_null($summary)) {
$this->summary = $summary;
$this->summaryformat = $summaryformat;
}
$this->additionalmodules = $additionalmodules;
parent::__construct($size, $fixeddataset, $filesizelimit, $progress);
}
/**
* Returns the relation between users and course sizes.
*
* @return array
*/
public static function get_users_per_size() {
return self::$paramusers;
}
/**
* Gets a list of size choices supported by this backend.
*
* @return array List of size (int) => text description for display
*/
public static function get_size_choices() {
$options = array();
for ($size = self::MIN_SIZE; $size <= self::MAX_SIZE; $size++) {
$options[$size] = get_string('coursesize_' . $size, 'tool_generator');
}
return $options;
}
/**
* Checks that a shortname is available (unused).
*
* @param string $shortname Proposed course shortname
* @return string An error message if the name is unavailable or '' if OK
*/
public static function check_shortname_available($shortname) {
global $DB;
$fullname = $DB->get_field('course', 'fullname',
array('shortname' => $shortname), IGNORE_MISSING);
if ($fullname !== false) {
// I wanted to throw an exception here but it is not possible to
// use strings from moodle.php in exceptions, and I didn't want
// to duplicate the string in tool_generator, so I changed this to
// not use exceptions.
return get_string('shortnametaken', 'moodle', $fullname);
}
return '';
}
/**
* Runs the entire 'make' process.
*
* @return int Course id
*/
public function make() {
global $DB, $CFG, $USER;
require_once($CFG->dirroot . '/lib/phpunit/classes/util.php');
raise_memory_limit(MEMORY_EXTRA);
if ($this->progress && !CLI_SCRIPT) {
echo html_writer::start_tag('ul');
}
$entirestart = microtime(true);
// Get generator.
$this->generator = phpunit_util::get_data_generator();
// Make course.
$this->course = $this->create_course();
$this->create_assignments();
$this->create_pages();
$this->create_small_files();
$this->create_big_files();
// Create users as late as possible to reduce regarding in the gradebook.
$this->create_users();
$this->create_forum();
// Let plugins hook into user settings navigation.
$pluginsfunction = get_plugins_with_function('course_backend_generator_create_activity');
foreach ($pluginsfunction as $plugintype => $plugins) {
foreach ($plugins as $pluginname => $pluginfunction) {
if (in_array($pluginname, $this->additionalmodules)) {
$pluginfunction($this, $this->generator, $this->course->id, self::$paramactivities[$this->size]);
}
}
}
// We are checking 'enroladminnewcourse' setting to decide to enrol admins or not.
if (!empty($CFG->creatornewroleid) && !empty($CFG->enroladminnewcourse) && is_siteadmin($USER->id)) {
// Deal with course creators - enrol them internally with default role.
enrol_try_internal_enrol($this->course->id, $USER->id, $CFG->creatornewroleid);
}
// Log total time.
$this->log('coursecompleted', round(microtime(true) - $entirestart, 1));
$this->end_log();
if ($this->progress && !CLI_SCRIPT) {
echo html_writer::end_tag('ul');
}
return $this->course->id;
}
/**
* Creates the actual course.
*
* @return stdClass Course record
*/
private function create_course() {
$this->log('createcourse', $this->shortname);
$courserecord = array(
'shortname' => $this->shortname,
'fullname' => $this->fullname,
'numsections' => self::$paramsections[$this->size],
'startdate' => usergetmidnight(time())
);
if (strlen($this->summary) > 0) {
$courserecord['summary'] = $this->summary;
$courserecord['summary_format'] = $this->summaryformat;
}
$return = $this->generator->create_course($courserecord, array('createsections' => true));
$this->end_log();
return $return;
}
/**
* Creates a number of user accounts and enrols them on the course.
* Note: Existing user accounts that were created by this system are
* reused if available.
*/
private function create_users() {
global $DB;
// Work out total number of users.
$count = self::$paramusers[$this->size];
// Get existing users in order. We will 'fill up holes' in this up to
// the required number.
$this->log('checkaccounts', $count);
$this->end_log();
$nextnumber = 1;
$rs = $DB->get_recordset_select('user', $DB->sql_like('username', '?'),
array('tool_generator_%'), 'username', 'id, username');
foreach ($rs as $rec) {
// Extract number from username.
$matches = array();
if (!preg_match('~^tool_generator_([0-9]{6})$~', $rec->username, $matches)) {
continue;
}
$number = (int)$matches[1];
// Create missing users in range up to this.
if ($number != $nextnumber) {
$this->create_user_accounts($nextnumber, min($number - 1, $count));
} else {
$this->userids[$number] = (int)$rec->id;
}
// Stop if we've got enough users.
$nextnumber = $number + 1;
if ($number >= $count) {
break;
}
}
$rs->close();
// Create users from end of existing range.
if ($nextnumber <= $count) {
$this->create_user_accounts($nextnumber, $count);
}
// Assign all users to course.
$this->log('enrol', $count, true);
$enrolplugin = enrol_get_plugin('manual');
$instances = enrol_get_instances($this->course->id, true);
foreach ($instances as $instance) {
if ($instance->enrol === 'manual') {
break;
}
}
if ($instance->enrol !== 'manual') {
throw new coding_exception('No manual enrol plugin in course');
}
$role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
for ($number = 1; $number <= $count; $number++) {
// Enrol user.
$enrolplugin->enrol_user($instance, $this->userids[$number], $role->id);
$this->dot($number, $count);
}
// Sets the pointer at the beginning to be aware of the users we use.
reset($this->userids);
$this->end_log();
}
/**
* Creates user accounts with a numeric range.
*
* @param int $first Number of first user
* @param int $last Number of last user
*/
private function create_user_accounts($first, $last) {
global $CFG;
$count = $last - $first + 1;
$this->log('createusers', $count, true);
$done = 0;
for ($number = $first; $number <= $last; $number++, $done++) {
// Work out username with 6-digit number.
$textnumber = (string)$number;
while (strlen($textnumber) < 6) {
$textnumber = '0' . $textnumber;
}
$username = 'tool_generator_' . $textnumber;
// Create user account.
$record = array('username' => $username, 'idnumber' => $number);
// We add a user password if it has been specified.
if (!empty($CFG->tool_generator_users_password)) {
$record['password'] = $CFG->tool_generator_users_password;
}
$user = $this->generator->create_user($record);
$this->userids[$number] = (int)$user->id;
$this->dot($done, $count);
}
$this->end_log();
}
/**
* Creates a number of Assignment activities.
*/
private function create_assignments() {
// Set up generator.
$assigngenerator = $this->generator->get_plugin_generator('mod_assign');
// Create assignments.
$number = self::$paramassignments[$this->size];
$this->log('createassignments', $number, true);
for ($i = 0; $i < $number; $i++) {
$record = array('course' => $this->course);
$options = array('section' => $this->get_target_section());
$assigngenerator->create_instance($record, $options);
$this->dot($i, $number);
}
$this->end_log();
}
/**
* Creates a number of Page activities.
*/
private function create_pages() {
// Set up generator.
$pagegenerator = $this->generator->get_plugin_generator('mod_page');
// Create pages.
$number = self::$parampages[$this->size];
$this->log('createpages', $number, true);
for ($i = 0; $i < $number; $i++) {
$record = array('course' => $this->course);
$options = array('section' => $this->get_target_section());
$pagegenerator->create_instance($record, $options);
$this->dot($i, $number);
}
$this->end_log();
}
/**
* Creates one resource activity with a lot of small files.
*/
private function create_small_files() {
$count = self::$paramsmallfilecount[$this->size];
$this->log('createsmallfiles', $count, true);
// Create resource with default textfile only.
$resourcegenerator = $this->generator->get_plugin_generator('mod_resource');
$record = array('course' => $this->course,
'name' => get_string('smallfiles', 'tool_generator'));
$options = array('section' => 0);
$resource = $resourcegenerator->create_instance($record, $options);
// Add files.
$fs = get_file_storage();
$context = context_module::instance($resource->cmid);
$filerecord = array('component' => 'mod_resource', 'filearea' => 'content',
'contextid' => $context->id, 'itemid' => 0, 'filepath' => '/');
for ($i = 0; $i < $count; $i++) {
$filerecord['filename'] = 'smallfile' . $i . '.dat';
// Generate random binary data (different for each file so it
// doesn't compress unrealistically).
$data = random_bytes($this->limit_filesize(self::$paramsmallfilesize[$this->size]));
$fs->create_file_from_string($filerecord, $data);
$this->dot($i, $count);
}
$this->end_log();
}
/**
* Creates a number of resource activities with one big file each.
*/
private function create_big_files() {
// Work out how many files and how many blocks to use (up to 64KB).
$count = self::$parambigfilecount[$this->size];
$filesize = $this->limit_filesize(self::$parambigfilesize[$this->size]);
$blocks = ceil($filesize / 65536);
$blocksize = floor($filesize / $blocks);
$this->log('createbigfiles', $count, true);
// Prepare temp area.
$tempfolder = make_temp_directory('tool_generator');
$tempfile = $tempfolder . '/' . rand();
// Create resources and files.
$fs = get_file_storage();
$resourcegenerator = $this->generator->get_plugin_generator('mod_resource');
for ($i = 0; $i < $count; $i++) {
// Create resource.
$record = array('course' => $this->course,
'name' => get_string('bigfile', 'tool_generator', $i));
$options = array('section' => $this->get_target_section());
$resource = $resourcegenerator->create_instance($record, $options);
// Write file.
$handle = fopen($tempfile, 'w');
if (!$handle) {
throw new coding_exception('Failed to open temporary file');
}
for ($j = 0; $j < $blocks; $j++) {
$data = random_bytes($blocksize);
fwrite($handle, $data);
$this->dot($i * $blocks + $j, $count * $blocks);
}
fclose($handle);
// Add file.
$context = context_module::instance($resource->cmid);
$filerecord = array('component' => 'mod_resource', 'filearea' => 'content',
'contextid' => $context->id, 'itemid' => 0, 'filepath' => '/',
'filename' => 'bigfile' . $i . '.dat');
$fs->create_file_from_pathname($filerecord, $tempfile);
}
unlink($tempfile);
$this->end_log();
}
/**
* Creates one forum activity with a bunch of posts.
*/
private function create_forum() {
global $DB;
$discussions = self::$paramforumdiscussions[$this->size];
$posts = self::$paramforumposts[$this->size];
$totalposts = $discussions * $posts;
$this->log('createforum', $totalposts, true);
// Create empty forum.
$forumgenerator = $this->generator->get_plugin_generator('mod_forum');
$record = array('course' => $this->course,
'name' => get_string('pluginname', 'forum'));
$options = array('section' => 0);
$forum = $forumgenerator->create_instance($record, $options);
// Add discussions and posts.
$sofar = 0;
for ($i = 0; $i < $discussions; $i++) {
$record = array('forum' => $forum->id, 'course' => $this->course->id,
'userid' => $this->get_target_user());
$discussion = $forumgenerator->create_discussion($record);
$parentid = $DB->get_field('forum_posts', 'id', array('discussion' => $discussion->id), MUST_EXIST);
$sofar++;
for ($j = 0; $j < $posts - 1; $j++, $sofar++) {
$record = array('discussion' => $discussion->id,
'userid' => $this->get_target_user(), 'parent' => $parentid);
$forumgenerator->create_post($record);
$this->dot($sofar, $totalposts);
}
}
$this->end_log();
}
/**
* Gets a section number.
*
* Depends on $this->fixeddataset.
*
* @return int A section number from 1 to the number of sections
*/
public function get_target_section() {
if (!$this->fixeddataset) {
$key = rand(1, self::$paramsections[$this->size]);
} else {
// Using section 1.
$key = 1;
}
return $key;
}
/**
* Gets a user id.
*
* Depends on $this->fixeddataset.
*
* @return int A user id for a random created user
*/
private function get_target_user() {
if (!$this->fixeddataset) {
$userid = $this->userids[rand(1, self::$paramusers[$this->size])];
} else if ($userid = current($this->userids)) {
// Moving pointer to the next user.
next($this->userids);
} else {
// Returning to the beginning if we reached the end.
$userid = reset($this->userids);
}
return $userid;
}
/**
* Restricts the binary file size if necessary
*
* @param int $length The total length
* @return int The limited length if a limit was specified.
*/
private function limit_filesize($length) {
// Limit to $this->filesizelimit.
if (is_numeric($this->filesizelimit) && $length > $this->filesizelimit) {
$length = floor($this->filesizelimit);
}
return $length;
}
}
@@ -0,0 +1,60 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tool_generator\form;
defined('MOODLE_INTERNAL') || die();
use moodleform;
global $CFG;
require_once($CFG->dirroot . '/lib/formslib.php');
/**
* Form for importting a testing scenario feature file.
*
* @package tool_generator
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class featureimport extends moodleform {
public function definition(): void {
$mform = &$this->_form;
// File upload.
$mform->addElement(
'filepicker',
'featurefile',
get_string('testscenario_file', 'tool_generator'),
null,
['accepted_types' => ['.feature']]
);
$mform->addRule('featurefile', null, 'required');
$this->add_action_buttons(false, get_string('import'));
}
/**
* Get the feature file contents.
* @return string|null the feature file contents or null if not found.
*/
public function get_feature_contents(): ?string {
$result = $this->get_file_content('featurefile');
if (!$result) {
return null;
}
return $result;
}
}
@@ -0,0 +1,123 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
namespace tool_generator\local\testscenario;
use stdClass;
/**
* Class with a scenario feature parsed.
*
* @package tool_generator
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class parsedfeature {
/** @var int the number of steps. */
private int $stepcount = 0;
/** @var bool if the parser is ok or fail. */
private bool $isvalid = true;
/** @var stdClass[] the list of scenarios with all the steps.
*
* scenarionum => {type: string, title: string, steps: steprunner[]}.
*/
private array $scenarios = [];
/**
* Get the general error, if any.
* @return string
*/
public function get_general_error(): string {
if (!$this->isvalid) {
return get_string('testscenario_invalidfile', 'tool_generator');
}
if ($this->stepcount == 0) {
return get_string('testscenario_nosteps', 'tool_generator');
}
return '';
}
/**
* Check if the parsed feature is valid.
* @return bool
*/
public function is_valid(): bool {
return $this->isvalid && $this->stepcount > 0;
}
/**
* Add a line to the current scenario.
* @param steprunner $step the step to add.
*/
public function add_step(steprunner $step) {
if (empty($this->scenarios)) {
$this->add_scenario('scenario', null);
}
$currentscenario = count($this->scenarios) - 1;
$this->scenarios[$currentscenario]->steps[] = $step;
$this->stepcount++;
if (!$step->is_valid()) {
$this->isvalid = false;
}
}
/**
* Insert a new scenario.
* @param string $type the type of the scenario.
* @param string|null $name the name of the scenario.
*/
public function add_scenario(string $type, ?string $name) {
$this->scenarios[] = (object) [
'type' => $type,
'name' => $name ?? '',
'steps' => [],
'error' => '',
];
}
/**
* Add an error to the current scenario.
* @param string $error
*/
public function add_error(string $error) {
$currentscenario = count($this->scenarios) - 1;
$this->scenarios[$currentscenario]->error = $error;
}
/**
* Get the list of scenarios.
* @return stdClass[] array of scenarionum => {type: string, title: string, steps: steprunner[]}
*/
public function get_scenarios(): array {
return $this->scenarios;
}
/**
* Get all the steps form all scenarios.
* @return steprunner[]
*/
public function get_all_steps(): array {
$result = [];
foreach ($this->scenarios as $scenario) {
foreach ($scenario->steps as $step) {
$result[] = $step;
}
}
return $result;
}
}
@@ -0,0 +1,203 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
namespace tool_generator\local\testscenario;
use behat_data_generators;
use Behat\Gherkin\Parser;
use Behat\Gherkin\Lexer;
use Behat\Gherkin\Keywords\ArrayKeywords;
use ReflectionClass;
use ReflectionMethod;
/**
* Class to process a scenario generator file.
*
* @package tool_generator
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class runner {
/** @var behat_data_generators the behat data generator instance. */
private behat_data_generators $generator;
/** @var array of valid steps indexed by given expression tag. */
private array $validsteps;
/**
* Initi all composer, behat libraries and load the valid steps.
*/
public function init() {
$this->include_composer_libraries();
$this->include_behat_libraries();
$this->load_generator();
}
/**
* Include composer autload.
*/
public function include_composer_libraries() {
global $CFG;
if (!file_exists($CFG->dirroot . '/vendor/autoload.php')) {
throw new \moodle_exception('Missing composer.');
}
require_once($CFG->dirroot . '/vendor/autoload.php');
return true;
}
/**
* Include all necessary behat libraries.
*/
public function include_behat_libraries() {
global $CFG;
if (!class_exists('Behat\Gherkin\Lexer')) {
throw new \moodle_exception('Missing behat classes.');
}
// Behat constant.
if (!defined('BEHAT_TEST')) {
define('BEHAT_TEST', 1);
}
// Behat utilities.
require_once($CFG->libdir . '/behat/classes/util.php');
require_once($CFG->libdir . '/behat/classes/behat_command.php');
require_once($CFG->libdir . '/behat/behat_base.php');
require_once("{$CFG->libdir}/tests/behat/behat_data_generators.php");
return true;
}
/**
* Load all generators.
*/
private function load_generator() {
$this->generator = new behat_data_generators();
$this->validsteps = $this->scan_generator($this->generator);
}
/**
* Scan a generator to get all valid steps.
* @param behat_data_generators $generator the generator to scan.
* @return array the valid steps.
*/
private function scan_generator(behat_data_generators $generator): array {
$result = [];
$class = new ReflectionClass($generator);
$methods = $class->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method) {
$given = $this->get_method_given($method);
if ($given) {
$result[$given] = $method->getName();
}
}
return $result;
}
/**
* Get the given expression tag of a method.
*
* @param ReflectionMethod $method the method to get the given expression tag.
* @return string|null the given expression tag or null if not found.
*/
private function get_method_given(ReflectionMethod $method): ?string {
$doccomment = $method->getDocComment();
$doccomment = str_replace("\r\n", "\n", $doccomment);
$doccomment = str_replace("\r", "\n", $doccomment);
$doccomment = explode("\n", $doccomment);
foreach ($doccomment as $line) {
$matches = [];
if (preg_match('/.*\@(given|when|then)\s+(.+)$/i', $line, $matches)) {
return $matches[2];
}
}
return null;
}
/**
* Parse a feature file.
* @param string $content the feature file content.
* @return parsedfeature
*/
public function parse_feature(string $content): parsedfeature {
$result = new parsedfeature();
$parser = $this->get_parser();
$feature = $parser->parse($content);
// No need for background in testing scenarios because scenarios can only contain generators.
// In the future the background can be used to define clean up steps (when clean up methods
// are implemented).
if ($feature->hasScenarios()) {
$scenarios = $feature->getScenarios();
foreach ($scenarios as $scenario) {
if ($scenario->getNodeType() == 'Outline') {
$result->add_scenario($scenario->getNodeType(), $scenario->getTitle());
$result->add_error(get_string('testscenario_outline', 'tool_generator'));
continue;
}
$result->add_scenario($scenario->getNodeType(), $scenario->getTitle());
$steps = $scenario->getSteps();
foreach ($steps as $step) {
$result->add_step(new steprunner($this->generator, $this->validsteps, $step));
}
}
}
return $result;
}
/**
* Get the parser.
* @return Parser
*/
private function get_parser(): Parser {
$keywords = new ArrayKeywords([
'en' => [
'feature' => 'Feature',
// If in the future we have clean up steps, background will be renamed to "Clean up".
'background' => 'Background',
'scenario' => 'Scenario',
'scenario_outline' => 'Scenario Outline|Scenario Template',
'examples' => 'Examples|Scenarios',
'given' => 'Given',
'when' => 'When',
'then' => 'Then',
'and' => 'And',
'but' => 'But',
],
]);
$lexer = new Lexer($keywords);
$parser = new Parser($lexer);
return $parser;
}
/**
* Execute a parsed feature.
* @param parsedfeature $parsedfeature the parsed feature to execute.
* @return bool true if all steps were executed successfully.
*/
public function execute(parsedfeature $parsedfeature): bool {
if (!$parsedfeature->is_valid()) {
return false;
}
$result = true;
$steps = $parsedfeature->get_all_steps();
foreach ($steps as $step) {
$result = $step->execute() && $result;
}
return $result;
}
}
@@ -0,0 +1,217 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
namespace tool_generator\local\testscenario;
use behat_data_generators;
use Behat\Gherkin\Node\StepNode;
/**
* Class to validate and process a scenario step.
*
* @package tool_generator
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class steprunner {
/** @var behat_data_generators the behat data generator instance. */
private behat_data_generators $generator;
/** @var array the valid steps indexed by given expression tag. */
private array $validsteps;
/** @var StepNode the step node to process. */
private StepNode $stepnode;
/** @var string|null the generator method to call. */
private ?string $method = null;
/** @var array the parameters to pass to the generator method. */
private array $params = [];
/** @var bool if the step is valid. */
private bool $isvalid = false;
/** @var bool if the step has been executed. */
private bool $executed = false;
/** @var string the error message if any. */
private string $error = '';
/**
* Constructor.
* @param behat_data_generators $generator the behat data generator instance.
* @param array $validsteps the valid steps indexed by given expression tag.
* @param StepNode $stepnode the step node to process.
*/
public function __construct(behat_data_generators $generator, array $validsteps, StepNode $stepnode) {
$this->generator = $generator;
$this->validsteps = $validsteps;
$this->stepnode = $stepnode;
$this->init();
}
/**
* Init the step runner.
*
* This method will check if the step is valid and all the needed information
* in case it is executed.
*/
private function init() {
$matches = [];
$linetext = $this->stepnode->getText();
foreach ($this->validsteps as $pattern => $method) {
if (!$this->match_given($pattern, $linetext, $matches)) {
continue;
}
$this->method = $method;
$this->params = $this->build_method_params($method, $matches);
$this->isvalid = true;
return;
}
$this->error = get_string('testscenario_invalidstep', 'tool_generator');
}
/**
* Build the method parameters.
* @param string $methodname the method name.
* @param array $matches the matches.
* @return array the method parameters.
*/
private function build_method_params($methodname, $matches) {
$method = new \ReflectionMethod($this->generator, $methodname);
$params = [];
foreach ($method->getParameters() as $param) {
$paramname = $param->getName();
if (isset($matches[$paramname])) {
$params[] = $matches[$paramname];
unset($matches[$paramname]);
} else if (count($matches) > 0) {
// If the param is not present means the regular expressions does not use
// proper names. So we will try to find the param by position.
$params[] = array_pop($matches);
} else {
// No more params to match.
break;
}
}
return array_merge($params, $this->stepnode->getArguments());
}
/**
* Return if the step is valid.
* @return bool
*/
public function is_valid(): bool {
return $this->isvalid;
}
/**
* Return if the step has been executed.
* @return bool
*/
public function is_executed(): bool {
return $this->executed;
}
/**
* Return the step text.
* @return string
*/
public function get_text(): string {
return $this->stepnode->getText();
}
/**
* Return the step error message.
* @return string
*/
public function get_error(): string {
return $this->error;
}
/**
* Return the step arguments as string.
* @return string
*/
public function get_arguments_string(): string {
$result = '';
foreach ($this->stepnode->getArguments() as $argument) {
$result .= $argument->getTableAsString();
}
return $result;
}
/**
* Match a given expression with a text.
* @param string $pattern the given expression.
* @param string $text the text to match.
* @param array $matches the matches.
* @return bool if the step matched the generator given expression.
*/
private function match_given(string $pattern, $text, array &$matches) {
$internalmatcher = [];
if (substr($pattern, 0, 1) === '/') {
// Pattern is a regular expression.
$result = preg_match($pattern, $text, $matches);
foreach ($matches as $key => $value) {
if (is_int($key)) {
unset($matches[$key]);
}
}
return $result;
}
// Patter is a string with parameters.
$elementmatches = [];
preg_match_all('/:([^ ]+)/', $pattern, $elementmatches, PREG_SET_ORDER, 0);
$pattern = preg_replace('/:([^ ]+)/', '(?P<$1>"[^"]+"|[^" ]+)', $pattern);
$pattern = '/^' . $pattern . '$/';
$result = preg_match($pattern, $text, $internalmatcher);
if (!$result) {
return false;
}
foreach ($elementmatches as $elementmatch) {
// Remove any possible " at the beggining and end of $internalmatcher[$elementmatch[1]].
$paramvalue = preg_replace('/^"(.*)"$/', '$1', $internalmatcher[$elementmatch[1]]);
$matches[$elementmatch[1]] = $paramvalue;
}
return true;
}
/**
* Execute the step.
* @return bool if the step is executed or not.
*/
public function execute(): bool {
if (!$this->isvalid) {
return false;
}
$this->executed = true;
try {
call_user_func_array(
[$this->generator, $this->method],
$this->params
);
} catch (\moodle_exception $exception) {
$this->error = $exception->getMessage();
$this->isvalid = false;
return false;
}
return true;
}
}
@@ -0,0 +1,103 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Course form.
*
* @package tool_generator
* @copyright 2013 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
/**
* Form with options for creating large course.
*
* @package tool_generator
* @copyright 2013 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_generator_make_course_form extends moodleform {
/**
* Course generation tool form definition.
*
* @return void
*/
public function definition() {
$mform = $this->_form;
$mform->addElement('select', 'size', get_string('size', 'tool_generator'),
tool_generator_course_backend::get_size_choices());
$mform->setDefault('size', tool_generator_course_backend::DEFAULT_SIZE);
$mform->addElement('text', 'shortname', get_string('shortnamecourse'));
$mform->addRule('shortname', get_string('missingshortname'), 'required', null, 'client');
$mform->setType('shortname', PARAM_TEXT);
$mform->addElement('text', 'fullname', get_string('fullnamecourse'));
$mform->setType('fullname', PARAM_TEXT);
$mform->addElement('editor', 'summary', get_string('coursesummary'));
$mform->setType('summary', PARAM_RAW);
$additionalmodules = [];
$pluginsfunction = get_plugins_with_function('course_backend_generator_create_activity');
foreach ($pluginsfunction as $plugintype => $plugins) {
foreach ($plugins as $pluginname => $pluginfunction) {
$additionalmodules[$pluginname] = get_string("pluginname", "{$plugintype}_{$pluginname}");
}
}
$mform->addElement('autocomplete', 'additionalmodules',
get_string('additionalmodules', 'tool_generator'),
$additionalmodules,
[
'multiple' => true,
'noselectionstring' => get_string('noselection', 'form')
]
);
$mform->addHelpButton('additionalmodules', 'additionalmodules', 'tool_generator');
$mform->setType('additionalmodules', PARAM_ALPHAEXT);
$mform->addElement('submit', 'submit', get_string('createcourse', 'tool_generator'));
}
/**
* Form validation.
*
* @param array $data
* @param array $files
* @return void
*/
public function validation($data, $files) {
global $DB;
$errors = array();
// Check course doesn't already exist.
if (!empty($data['shortname'])) {
// Check shortname.
$error = tool_generator_course_backend::check_shortname_available($data['shortname']);
if ($error) {
$errors['shortname'] = $error;
}
}
return $errors;
}
}
@@ -0,0 +1,81 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test plan form.
*
* @package tool_generator
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
/**
* Form with options for creating large course.
*
* @package tool_generator
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_generator_make_testplan_form extends moodleform {
/**
* Test plan form definition.
*
* @return void
*/
public function definition() {
$mform = $this->_form;
$mform->addElement('select', 'size', get_string('size', 'tool_generator'),
tool_generator_testplan_backend::get_size_choices());
$mform->setDefault('size', tool_generator_testplan_backend::DEFAULT_SIZE);
$mform->addElement('course', 'courseid', get_string('targetcourse', 'tool_generator'));
$mform->addElement('advcheckbox', 'updateuserspassword', get_string('updateuserspassword', 'tool_generator'));
$mform->addHelpButton('updateuserspassword', 'updateuserspassword', 'tool_generator');
$mform->addElement('submit', 'submit', get_string('createtestplan', 'tool_generator'));
}
/**
* Checks that the submitted data allows us to create a test plan.
*
* @param array $data
* @param array $files
* @return array An array of errors
*/
public function validation($data, $files) {
global $CFG;
$errors = array();
if (empty($CFG->tool_generator_users_password) || is_bool($CFG->tool_generator_users_password)) {
$errors['updateuserspassword'] = get_string('error_nouserspassword', 'tool_generator');
}
// Better to repeat here the query than to do it afterwards and end up with an exception.
if ($courseerrors = tool_generator_testplan_backend::has_selected_course_any_problem($data['courseid'], $data['size'])) {
$errors = array_merge($errors, $courseerrors);
}
return $errors;
}
}
@@ -0,0 +1,88 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tool_generator\output;
use renderable;
use renderer_base;
use templatable;
use tool_generator\local\testscenario\parsedfeature;
/**
* A report to show the feature file parsing process.
*
* @package tool_generator
* @copyright 2023 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class parsingresult implements renderable, templatable {
/** @var parsedfeature the processed feature object. */
protected $parsedfeature;
/**
* Constructor.
*
* @param parsedfeature $parsedfeature
*/
public function __construct(parsedfeature $parsedfeature) {
$this->parsedfeature = $parsedfeature;
}
/**
* Export for template.
*
* @param renderer_base $output The renderer.
* @return array
*/
public function export_for_template(renderer_base $output): array {
$data = [
'scenarios' => [],
'isvalid' => $this->parsedfeature->is_valid(),
'generalerror' => $this->parsedfeature->get_general_error(),
];
$haslines = false;
foreach ($this->parsedfeature->get_scenarios() as $scenario) {
$scenariodata = [
'type' => $scenario->type,
'name' => $scenario->name,
'steps' => [],
];
if (!empty($scenario->error)) {
$scenariodata['scenarioerror'] = $scenario->error;
}
foreach ($scenario->steps as $step) {
$scenariodata['steps'][] = [
'text' => $step->get_text(),
'arguments' => $step->get_arguments_string(),
'hasarguments' => !empty($step->get_arguments_string()),
'isvalid' => $step->is_valid(),
'error' => $step->get_error(),
'isexecuted' => $step->is_executed(),
];
$haslines = true;
}
if (!empty($scenariodata['steps'])) {
$scenariodata['hassteps'] = true;
}
$data['scenarios'][] = $scenariodata;
}
if ($haslines) {
$data['haslines'] = $haslines;
}
return $data;
}
}
@@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for tool_generator.
*
* @package tool_generator
* @copyright 2018 Zig Tan <zig@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_generator\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for tool_generator implementing null_provider.
*
* @copyright 2018 Zig Tan <zig@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,212 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* tool_generator site backend.
*
* @package tool_generator
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Backend code for the site generator.
*
* @package tool_generator
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_generator_site_backend extends tool_generator_backend {
/**
* @var string The course's shortname prefix.
*/
const SHORTNAMEPREFIX = 'testcourse_';
/**
* @var bool If the debugging level checking was skipped.
*/
protected $bypasscheck;
/**
* @var array Multidimensional array where the first level is the course size and the second the site size.
*/
protected static $sitecourses = array(
array(2, 8, 64, 256, 1024, 4096),
array(1, 4, 8, 16, 32, 64),
array(0, 0, 1, 4, 8, 16),
array(0, 0, 0, 1, 0, 0),
array(0, 0, 0, 0, 1, 0),
array(0, 0, 0, 0, 0, 1)
);
/**
* Constructs object ready to make the site.
*
* @param int $size Size as numeric index
* @param bool $bypasscheck If debugging level checking was skipped.
* @param bool $fixeddataset To use fixed or random data
* @param int|bool $filesizelimit The max number of bytes for a generated file
* @param bool $progress True if progress information should be displayed
* @return int Course id
*/
public function __construct($size, $bypasscheck, $fixeddataset = false, $filesizelimit = false, $progress = true) {
// Set parameters.
$this->bypasscheck = $bypasscheck;
parent::__construct($size, $fixeddataset, $filesizelimit, $progress);
}
/**
* Gets a list of size choices supported by this backend.
*
* @return array List of size (int) => text description for display
*/
public static function get_size_choices() {
$options = array();
for ($size = self::MIN_SIZE; $size <= self::MAX_SIZE; $size++) {
$options[$size] = get_string('sitesize_' . $size, 'tool_generator');
}
return $options;
}
/**
* Runs the entire 'make' process.
*
* @return int Course id
*/
public function make() {
global $DB, $CFG;
raise_memory_limit(MEMORY_EXTRA);
if ($this->progress && !CLI_SCRIPT) {
echo html_writer::start_tag('ul');
}
$entirestart = microtime(true);
// Create courses.
$prevchdir = getcwd();
chdir($CFG->dirroot);
$ncourse = self::get_last_testcourse_id();
foreach (self::$sitecourses as $coursesize => $ncourses) {
for ($i = 1; $i <= $ncourses[$this->size]; $i++) {
// Non language-dependant shortname.
$ncourse++;
$this->run_create_course(self::SHORTNAMEPREFIX . $ncourse, $coursesize);
}
}
chdir($prevchdir);
// Store last course id to return it (will be the bigger one).
$lastcourseid = $DB->get_field('course', 'id', array('shortname' => self::SHORTNAMEPREFIX . $ncourse));
// Log total time.
$this->log('sitecompleted', round(microtime(true) - $entirestart, 1));
if ($this->progress && !CLI_SCRIPT) {
echo html_writer::end_tag('ul');
}
return $lastcourseid;
}
/**
* Creates a course with the specified shortname, coursesize and the provided maketestsite options.
*
* @param string $shortname The course shortname
* @param int $coursesize One of the possible course sizes.
* @return void
*/
protected function run_create_course($shortname, $coursesize) {
// We are in $CFG->dirroot.
$command = 'php admin/tool/generator/cli/maketestcourse.php';
$options = array(
'--shortname="' . $shortname . '"',
'--size="' . get_string('shortsize_' . $coursesize, 'tool_generator') . '"'
);
if (!$this->progress) {
$options[] = '--quiet';
}
if ($this->filesizelimit) {
$options[] = '--filesizelimit="' . $this->filesizelimit . '"';
}
// Extend options.
$optionstoextend = array(
'fixeddataset' => 'fixeddataset',
'bypasscheck' => 'bypasscheck',
);
// Getting an options string.
foreach ($optionstoextend as $attribute => $option) {
if (!empty($this->{$attribute})) {
$options[] = '--' . $option;
}
}
$options = implode(' ', $options);
if ($this->progress) {
system($command . ' ' . $options, $exitcode);
} else {
passthru($command . ' ' . $options, $exitcode);
}
if ($exitcode != 0) {
exit($exitcode);
}
}
/**
* Obtains the last unique sufix (numeric) using the test course prefix.
*
* @return int The last generated numeric value.
*/
protected static function get_last_testcourse_id() {
global $DB;
$params = array();
$params['shortnameprefix'] = $DB->sql_like_escape(self::SHORTNAMEPREFIX) . '%';
$like = $DB->sql_like('shortname', ':shortnameprefix');
if (!$testcourses = $DB->get_records_select('course', $like, $params, '', 'shortname')) {
return 0;
}
// SQL order by is not appropiate here as is ordering strings.
$shortnames = array_keys($testcourses);
core_collator::asort($shortnames, core_collator::SORT_NATURAL);
$shortnames = array_reverse($shortnames);
// They come ordered by shortname DESC, so non-numeric values will be the first ones.
$prefixnchars = strlen(self::SHORTNAMEPREFIX);
foreach ($shortnames as $shortname) {
$sufix = substr($shortname, $prefixnchars);
if (preg_match('/^[\d]+$/', $sufix)) {
return $sufix;
}
}
// If all sufixes are not numeric this is the first make test site run.
return 0;
}
}
@@ -0,0 +1,317 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test plan generator.
*
* @package tool_generator
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Generates the files required by JMeter.
*
* @package tool_generator
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_generator_testplan_backend extends tool_generator_backend {
/**
* @var The URL to the repository of the external project.
*/
protected static $repourl = 'https://github.com/moodlehq/moodle-performance-comparison';
/**
* @var Number of users depending on the selected size.
*/
protected static $users = array(1, 30, 100, 1000, 5000, 10000);
/**
* @var Number of loops depending on the selected size.
*/
protected static $loops = array(5, 5, 5, 6, 6, 7);
/**
* @var Rampup period depending on the selected size.
*/
protected static $rampups = array(1, 6, 40, 100, 500, 800);
/**
* Gets a list of size choices supported by this backend.
*
* @return array List of size (int) => text description for display
*/
public static function get_size_choices() {
$options = array();
for ($size = self::MIN_SIZE; $size <= self::MAX_SIZE; $size++) {
$a = new stdClass();
$a->users = self::$users[$size];
$a->loops = self::$loops[$size];
$a->rampup = self::$rampups[$size];
$options[$size] = get_string('testplansize_' . $size, 'tool_generator', $a);
}
return $options;
}
/**
* Getter for moodle-performance-comparison project URL.
*
* @return string
*/
public static function get_repourl() {
return self::$repourl;
}
/**
* Creates the test plan file.
*
* @param int $courseid The target course id
* @param int $size The test plan size
* @return stored_file
*/
public static function create_testplan_file($courseid, $size) {
$jmxcontents = self::generate_test_plan($courseid, $size);
$fs = get_file_storage();
$filerecord = self::get_file_record('testplan', 'jmx');
return $fs->create_file_from_string($filerecord, $jmxcontents);
}
/**
* Creates the users data file.
*
* @param int $courseid The target course id
* @param bool $updateuserspassword Updates the course users password to $CFG->tool_generator_users_password
* @param int|null $size of the test plan. Used to limit the number of users exported
* to match the threads in the plan. For BC, defaults to null that means all enrolled users.
* @return stored_file
*/
public static function create_users_file($courseid, $updateuserspassword, ?int $size = null) {
$csvcontents = self::generate_users_file($courseid, $updateuserspassword, $size);
$fs = get_file_storage();
$filerecord = self::get_file_record('users', 'csv');
return $fs->create_file_from_string($filerecord, $csvcontents);
}
/**
* Generates the test plan according to the target course contents.
*
* @param int $targetcourseid The target course id
* @param int $size The test plan size
* @return string The test plan as a string
*/
protected static function generate_test_plan($targetcourseid, $size) {
global $CFG;
// Getting the template.
$template = file_get_contents(__DIR__ . '/../testplan.template.jmx');
// Getting the course modules data.
$coursedata = self::get_course_test_data($targetcourseid);
// Host and path to the site.
$urlcomponents = parse_url($CFG->wwwroot);
if (empty($urlcomponents['path'])) {
$urlcomponents['path'] = '';
}
$replacements = array(
$CFG->version,
self::$users[$size],
self::$loops[$size],
self::$rampups[$size],
$urlcomponents['host'],
$urlcomponents['path'],
get_string('shortsize_' . $size, 'tool_generator'),
$targetcourseid,
$coursedata->pageid,
$coursedata->forumid,
$coursedata->forumdiscussionid,
$coursedata->forumreplyid
);
$placeholders = array(
'{{MOODLEVERSION_PLACEHOLDER}}',
'{{USERS_PLACEHOLDER}}',
'{{LOOPS_PLACEHOLDER}}',
'{{RAMPUP_PLACEHOLDER}}',
'{{HOST_PLACEHOLDER}}',
'{{SITEPATH_PLACEHOLDER}}',
'{{SIZE_PLACEHOLDER}}',
'{{COURSEID_PLACEHOLDER}}',
'{{PAGEACTIVITYID_PLACEHOLDER}}',
'{{FORUMACTIVITYID_PLACEHOLDER}}',
'{{FORUMDISCUSSIONID_PLACEHOLDER}}',
'{{FORUMREPLYID_PLACEHOLDER}}'
);
// Fill the template with the target course values.
return str_replace($placeholders, $replacements, $template);
}
/**
* Generates the user's credentials file with all the course's users
*
* @param int $targetcourseid
* @param bool $updateuserspassword Updates the course users password to $CFG->tool_generator_users_password
* @param int|null $size of the test plan. Used to limit the number of users exported
* to match the threads in the plan. For BC, defaults to null that means all enrolled users.
* @return string The users csv file contents.
*/
protected static function generate_users_file($targetcourseid, $updateuserspassword, ?int $size = null) {
global $CFG;
$coursecontext = context_course::instance($targetcourseid);
// If requested, get the number of users (threads) to use in the plan. We only need those in the exported file.
$planusers = self::$users[$size] ?? 0;
$users = get_enrolled_users($coursecontext, '', 0, 'u.id, u.username, u.auth', 'u.username ASC', 0, $planusers);
if (!$users) {
throw new \moodle_exception('coursewithoutusers', 'tool_generator');
}
$lines = array();
foreach ($users as $user) {
// Updating password to the one set in config.php.
if ($updateuserspassword) {
$userauth = get_auth_plugin($user->auth);
if (!$userauth->user_update_password($user, $CFG->tool_generator_users_password)) {
throw new \moodle_exception('errorpasswordupdate', 'auth');
}
}
// Here we already checked that $CFG->tool_generator_users_password is not null.
$lines[] = $user->username . ',' . $CFG->tool_generator_users_password;
}
return implode(PHP_EOL, $lines);
}
/**
* Returns a tool_generator file record
*
* @param string $filearea testplan or users
* @param string $filetype The file extension jmx or csv
* @return stdClass The file record to use when creating tool_generator files
*/
protected static function get_file_record($filearea, $filetype) {
$systemcontext = context_system::instance();
$filerecord = new stdClass();
$filerecord->contextid = $systemcontext->id;
$filerecord->component = 'tool_generator';
$filerecord->filearea = $filearea;
$filerecord->itemid = 0;
$filerecord->filepath = '/';
// Random generated number to avoid concurrent execution problems.
$filerecord->filename = $filearea . '_' . date('YmdHi', time()) . '_' . rand(1000, 9999) . '.' . $filetype;
return $filerecord;
}
/**
* Gets the data required to fill the test plan template with the database contents.
*
* @param int $targetcourseid The target course id
* @return stdClass The ids required by the test plan
*/
protected static function get_course_test_data($targetcourseid) {
global $DB, $USER;
$data = new stdClass();
// Getting course contents info as the current user (will be an admin).
$course = new stdClass();
$course->id = $targetcourseid;
$courseinfo = new course_modinfo($course, $USER->id);
// Getting the first page module instance.
if (!$pages = $courseinfo->get_instances_of('page')) {
throw new \moodle_exception('error_nopageinstances', 'tool_generator');
}
$data->pageid = reset($pages)->id;
// Getting the first forum module instance and it's first discussion and reply as well.
if (!$forums = $courseinfo->get_instances_of('forum')) {
throw new \moodle_exception('error_noforuminstances', 'tool_generator');
}
$forum = reset($forums);
// Getting the first discussion (and reply).
if (!$discussions = forum_get_discussions($forum, 'd.timemodified ASC', false, -1, 1)) {
throw new \moodle_exception('error_noforumdiscussions', 'tool_generator');
}
$discussion = reset($discussions);
$data->forumid = $forum->id;
$data->forumdiscussionid = $discussion->discussion;
$data->forumreplyid = $discussion->id;
// According to the current test plan.
return $data;
}
/**
* Checks if the selected target course is ok.
*
* @param int|string $course
* @param int $size
* @return array Errors array or false if everything is ok
*/
public static function has_selected_course_any_problem($course, $size) {
global $DB;
$errors = array();
if (!is_numeric($course)) {
if (!$course = $DB->get_field('course', 'id', array('shortname' => $course))) {
$errors['courseid'] = get_string('error_nonexistingcourse', 'tool_generator');
return $errors;
}
}
$coursecontext = context_course::instance($course, IGNORE_MISSING);
if (!$coursecontext) {
$errors['courseid'] = get_string('error_nonexistingcourse', 'tool_generator');
return $errors;
}
if (!$users = get_enrolled_users($coursecontext, '', 0, 'u.id')) {
$errors['courseid'] = get_string('coursewithoutusers', 'tool_generator');
}
// Checks that the selected course has enough users.
$coursesizes = tool_generator_course_backend::get_users_per_size();
if (count($users) < self::$users[$size]) {
$errors['size'] = get_string('notenoughusers', 'tool_generator');
}
if (empty($errors)) {
return false;
}
return $errors;
}
}