07e1e7b1c85bcc29587f1f86fe540b08a987762a
[moodle.git] / admin / tool / generator / classes / course_backend.php
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/>.
17 /**
18  * tool_generator course backend code.
19  *
20  * @package tool_generator
21  * @copyright 2013 The Open University
22  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 /**
28  * Backend code for the 'make large course' tool.
29  *
30  * @package tool_generator
31  * @copyright 2013 The Open University
32  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33  */
34 class tool_generator_course_backend extends tool_generator_backend {
35     /**
36      * @var array Number of sections in course
37      */
38     private static $paramsections = array(1, 10, 100, 500, 1000, 2000);
39     /**
40      * @var array Number of Page activities in course
41      */
42     private static $parampages = array(1, 50, 200, 1000, 5000, 10000);
43     /**
44      * @var array Number of students enrolled in course
45      */
46     private static $paramusers = array(1, 100, 1000, 10000, 50000, 100000);
47     /**
48      * Total size of small files: 1KB, 1MB, 10MB, 100MB, 1GB, 2GB.
49      *
50      * @var array Number of small files created in a single file activity
51      */
52     private static $paramsmallfilecount = array(1, 64, 128, 1024, 16384, 32768);
53     /**
54      * @var array Size of small files (to make the totals into nice numbers)
55      */
56     private static $paramsmallfilesize = array(1024, 16384, 81920, 102400, 65536, 65536);
57     /**
58      * Total size of big files: 8KB, 8MB, 80MB, 800MB, 8GB, 16GB.
59      *
60      * @var array Number of big files created as individual file activities
61      */
62     private static $parambigfilecount = array(1, 2, 5, 10, 10, 10);
63     /**
64      * @var array Size of each large file
65      */
66     private static $parambigfilesize = array(8192, 4194304, 16777216, 83886080,
67             858993459, 1717986918);
68     /**
69      * @var array Number of forum discussions
70      */
71     private static $paramforumdiscussions = array(1, 10, 100, 500, 1000, 2000);
72     /**
73      * @var array Number of forum posts per discussion
74      */
75     private static $paramforumposts = array(2, 2, 5, 10, 10, 10);
77     /**
78      * @var string Course shortname
79      */
80     private $shortname;
82     /**
83      * @var testing_data_generator Data generator
84      */
85     protected $generator;
87     /**
88      * @var stdClass Course object
89      */
90     private $course;
92     /**
93      * @var array Array from test user number (1...N) to userid in database
94      */
95     private $userids;
97     /**
98      * Constructs object ready to create course.
99      *
100      * @param string $shortname Course shortname
101      * @param int $size Size as numeric index
102      * @param bool $fixeddataset To use fixed or random data
103      * @param bool $progress True if progress information should be displayed
104      * @return int Course id
105      */
106     public function __construct($shortname, $size, $fixeddataset = false, $progress = true) {
108         // Set parameters.
109         $this->shortname = $shortname;
111         parent::__construct($size, $fixeddataset, $progress);
112     }
114     /**
115      * Gets a list of size choices supported by this backend.
116      *
117      * @return array List of size (int) => text description for display
118      */
119     public static function get_size_choices() {
120         $options = array();
121         for ($size = self::MIN_SIZE; $size <= self::MAX_SIZE; $size++) {
122             $options[$size] = get_string('coursesize_' . $size, 'tool_generator');
123         }
124         return $options;
125     }
127     /**
128      * Checks that a shortname is available (unused).
129      *
130      * @param string $shortname Proposed course shortname
131      * @return string An error message if the name is unavailable or '' if OK
132      */
133     public static function check_shortname_available($shortname) {
134         global $DB;
135         $fullname = $DB->get_field('course', 'fullname',
136                 array('shortname' => $shortname), IGNORE_MISSING);
137         if ($fullname !== false) {
138             // I wanted to throw an exception here but it is not possible to
139             // use strings from moodle.php in exceptions, and I didn't want
140             // to duplicate the string in tool_generator, so I changed this to
141             // not use exceptions.
142             return get_string('shortnametaken', 'moodle', $fullname);
143         }
144         return '';
145     }
147     /**
148      * Runs the entire 'make' process.
149      *
150      * @return int Course id
151      */
152     public function make() {
153         global $DB, $CFG;
154         require_once($CFG->dirroot . '/lib/phpunit/classes/util.php');
156         raise_memory_limit(MEMORY_EXTRA);
158         if ($this->progress && !CLI_SCRIPT) {
159             echo html_writer::start_tag('ul');
160         }
162         $entirestart = microtime(true);
164         // Start transaction.
165         $transaction = $DB->start_delegated_transaction();
167         // Get generator.
168         $this->generator = phpunit_util::get_data_generator();
170         // Make course.
171         $this->course = $this->create_course();
172         $this->create_users();
173         $this->create_pages();
174         $this->create_small_files();
175         $this->create_big_files();
176         $this->create_forum();
178         // Log total time.
179         $this->log('coursecompleted', round(microtime(true) - $entirestart, 1));
181         if ($this->progress && !CLI_SCRIPT) {
182             echo html_writer::end_tag('ul');
183         }
185         // Commit transaction and finish.
186         $transaction->allow_commit();
187         return $this->course->id;
188     }
190     /**
191      * Creates the actual course.
192      *
193      * @return stdClass Course record
194      */
195     private function create_course() {
196         $this->log('createcourse', $this->shortname);
197         $courserecord = array('shortname' => $this->shortname,
198                 'fullname' => get_string('fullname', 'tool_generator',
199                     array('size' => get_string('shortsize_' . $this->size, 'tool_generator'))),
200                 'numsections' => self::$paramsections[$this->size]);
201         return $this->generator->create_course($courserecord, array('createsections' => true));
202     }
204     /**
205      * Creates a number of user accounts and enrols them on the course.
206      * Note: Existing user accounts that were created by this system are
207      * reused if available.
208      */
209     private function create_users() {
210         global $DB;
212         // Work out total number of users.
213         $count = self::$paramusers[$this->size];
215         // Get existing users in order. We will 'fill up holes' in this up to
216         // the required number.
217         $this->log('checkaccounts', $count);
218         $nextnumber = 1;
219         $rs = $DB->get_recordset_select('user', $DB->sql_like('username', '?'),
220                 array('tool_generator_%'), 'username', 'id, username');
221         foreach ($rs as $rec) {
222             // Extract number from username.
223             $matches = array();
224             if (!preg_match('~^tool_generator_([0-9]{6})$~', $rec->username, $matches)) {
225                 continue;
226             }
227             $number = (int)$matches[1];
229             // Create missing users in range up to this.
230             if ($number != $nextnumber) {
231                 $this->create_user_accounts($nextnumber, min($number - 1, $count));
232             } else {
233                 $this->userids[$number] = (int)$rec->id;
234             }
236             // Stop if we've got enough users.
237             $nextnumber = $number + 1;
238             if ($number >= $count) {
239                 break;
240             }
241         }
242         $rs->close();
244         // Create users from end of existing range.
245         if ($nextnumber <= $count) {
246             $this->create_user_accounts($nextnumber, $count);
247         }
249         // Assign all users to course.
250         $this->log('enrol', $count, true);
252         $enrolplugin = enrol_get_plugin('manual');
253         $instances = enrol_get_instances($this->course->id, true);
254         foreach ($instances as $instance) {
255             if ($instance->enrol === 'manual') {
256                 break;
257             }
258         }
259         if ($instance->enrol !== 'manual') {
260             throw new coding_exception('No manual enrol plugin in course');
261         }
262         $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
264         for ($number = 1; $number <= $count; $number++) {
265             // Enrol user.
266             $enrolplugin->enrol_user($instance, $this->userids[$number], $role->id);
267             $this->dot($number, $count);
268         }
270         // Sets the pointer at the beginning to be aware of the users we use.
271         reset($this->userids);
273         $this->end_log();
274     }
276     /**
277      * Creates user accounts with a numeric range.
278      *
279      * @param int $first Number of first user
280      * @param int $last Number of last user
281      */
282     private function create_user_accounts($first, $last) {
283         $this->log('createaccounts', (object)array('from' => $first, 'to' => $last), true);
284         $count = $last - $first + 1;
285         $done = 0;
286         for ($number = $first; $number <= $last; $number++, $done++) {
287             // Work out username with 6-digit number.
288             $textnumber = (string)$number;
289             while (strlen($textnumber) < 6) {
290                 $textnumber = '0' . $textnumber;
291             }
292             $username = 'tool_generator_' . $textnumber;
294             // Create user account.
295             $record = array('firstname' => get_string('firstname', 'tool_generator'),
296                     'lastname' => $number, 'username' => $username);
297             $user = $this->generator->create_user($record);
298             $this->userids[$number] = (int)$user->id;
299             $this->dot($done, $count);
300         }
301         $this->end_log();
302     }
304     /**
305      * Creates a number of Page activities.
306      */
307     private function create_pages() {
308         // Set up generator.
309         $pagegenerator = $this->generator->get_plugin_generator('mod_page');
311         // Create pages.
312         $number = self::$parampages[$this->size];
313         $this->log('createpages', $number, true);
314         for ($i=0; $i<$number; $i++) {
315             $record = array('course' => $this->course->id);
316             $options = array('section' => $this->get_target_section());
317             $pagegenerator->create_instance($record, $options);
318             $this->dot($i, $number);
319         }
321         $this->end_log();
322     }
324     /**
325      * Creates one resource activity with a lot of small files.
326      */
327     private function create_small_files() {
328         $count = self::$paramsmallfilecount[$this->size];
329         $this->log('createsmallfiles', $count, true);
331         // Create resource with default textfile only.
332         $resourcegenerator = $this->generator->get_plugin_generator('mod_resource');
333         $record = array('course' => $this->course->id,
334                 'name' => get_string('smallfiles', 'tool_generator'));
335         $options = array('section' => 0);
336         $resource = $resourcegenerator->create_instance($record, $options);
338         // Add files.
339         $fs = get_file_storage();
340         $context = context_module::instance($resource->cmid);
341         $filerecord = array('component' => 'mod_resource', 'filearea' => 'content',
342                 'contextid' => $context->id, 'itemid' => 0, 'filepath' => '/');
343         for ($i = 0; $i < $count; $i++) {
344             $filerecord['filename'] = 'smallfile' . $i . '.dat';
346             // Generate random binary data (different for each file so it
347             // doesn't compress unrealistically).
348             $data = self::get_random_binary(self::$paramsmallfilesize[$this->size]);
350             $fs->create_file_from_string($filerecord, $data);
351             $this->dot($i, $count);
352         }
354         $this->end_log();
355     }
357     /**
358      * Creates a string of random binary data. The start of the string includes
359      * the current time, in an attempt to avoid large-scale repetition.
360      *
361      * @param int $length Number of bytes
362      * @return Random data
363      */
364     private static function get_random_binary($length) {
365         $data = microtime(true);
366         if (strlen($data) > $length) {
367             // Use last digits of data.
368             return substr($data, -$length);
369         }
370         $length -= strlen($data);
371         for ($j=0; $j < $length; $j++) {
372             $data .= chr(rand(1, 255));
373         }
374         return $data;
375     }
377     /**
378      * Creates a number of resource activities with one big file each.
379      */
380     private function create_big_files() {
381         global $CFG;
383         // Work out how many files and how many blocks to use (up to 64KB).
384         $count = self::$parambigfilecount[$this->size];
385         $blocks = ceil(self::$parambigfilesize[$this->size] / 65536);
386         $blocksize = floor(self::$parambigfilesize[$this->size] / $blocks);
388         $this->log('createbigfiles', $count, true);
390         // Prepare temp area.
391         $tempfolder = make_temp_directory('tool_generator');
392         $tempfile = $tempfolder . '/' . rand();
394         // Create resources and files.
395         $fs = get_file_storage();
396         $resourcegenerator = $this->generator->get_plugin_generator('mod_resource');
397         for ($i = 0; $i < $count; $i++) {
398             // Create resource.
399             $record = array('course' => $this->course->id,
400                     'name' => get_string('bigfile', 'tool_generator', $i));
401             $options = array('section' => $this->get_target_section());
402             $resource = $resourcegenerator->create_instance($record, $options);
404             // Write file.
405             $handle = fopen($tempfile, 'w');
406             if (!$handle) {
407                 throw new coding_exception('Failed to open temporary file');
408             }
409             for ($j = 0; $j < $blocks; $j++) {
410                 $data = self::get_random_binary($blocksize);
411                 fwrite($handle, $data);
412                 $this->dot($i * $blocks + $j, $count * $blocks);
413             }
414             fclose($handle);
416             // Add file.
417             $context = context_module::instance($resource->cmid);
418             $filerecord = array('component' => 'mod_resource', 'filearea' => 'content',
419                     'contextid' => $context->id, 'itemid' => 0, 'filepath' => '/',
420                     'filename' => 'bigfile' . $i . '.dat');
421             $fs->create_file_from_pathname($filerecord, $tempfile);
422         }
424         unlink($tempfile);
425         $this->end_log();
426     }
428     /**
429      * Creates one forum activity with a bunch of posts.
430      */
431     private function create_forum() {
432         global $DB;
434         $discussions = self::$paramforumdiscussions[$this->size];
435         $posts = self::$paramforumposts[$this->size];
436         $totalposts = $discussions * $posts;
438         $this->log('createforum', $totalposts, true);
440         // Create empty forum.
441         $forumgenerator = $this->generator->get_plugin_generator('mod_forum');
442         $record = array('course' => $this->course->id,
443                 'name' => get_string('pluginname', 'forum'));
444         $options = array('section' => 0);
445         $forum = $forumgenerator->create_instance($record, $options);
447         // Add discussions and posts.
448         $sofar = 0;
449         for ($i=0; $i < $discussions; $i++) {
450             $record = array('forum' => $forum->id, 'course' => $this->course->id,
451                     'userid' => $this->get_target_user());
452             $discussion = $forumgenerator->create_discussion($record);
453             $parentid = $DB->get_field('forum_posts', 'id', array('discussion' => $discussion->id), MUST_EXIST);
454             $sofar++;
455             for ($j=0; $j < $posts - 1; $j++, $sofar++) {
456                 $record = array('discussion' => $discussion->id,
457                         'userid' => $this->get_target_user(), 'parent' => $parentid);
458                 $forumgenerator->create_post($record);
459                 $this->dot($sofar, $totalposts);
460             }
461         }
463         $this->end_log();
464     }
466     /**
467      * Gets a section number.
468      *
469      * Depends on $this->fixeddataset.
470      *
471      * @return int A section number from 1 to the number of sections
472      */
473     private function get_target_section() {
475         if (!$this->fixeddataset) {
476             $key = rand(1, self::$paramsections[$this->size]);
477         } else {
478             // Using section 1.
479             $key = 1;
480         }
482         return $key;
483     }
485     /**
486      * Gets a user id.
487      *
488      * Depends on $this->fixeddataset.
489      *
490      * @return int A user id for a random created user
491      */
492     private function get_target_user() {
494         if (!$this->fixeddataset) {
495             $userid = $this->userids[rand(1, self::$paramusers[$this->size])];
496         } else if ($userid = current($this->userids)) {
497             // Moving pointer to the next user.
498             next($this->userids);
499         } else {
500             // Returning to the beginning if we reached the end.
501             $userid = reset($this->userids);
502         }
504         return $userid;
505     }