MDL-41402 tool_generator: fix whitespace
[moodle.git] / admin / tool / generator / classes / course_backend.php
CommitLineData
8cac8d3e
DM
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 * 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 */
24
25defined('MOODLE_INTERNAL') || die();
26
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 */
34class 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);
76
77 /**
78 * @var string Course shortname
79 */
80 private $shortname;
81
82 /**
83 * @var testing_data_generator Data generator
84 */
85 protected $generator;
86
87 /**
88 * @var stdClass Course object
89 */
90 private $course;
91
92 /**
93 * @var array Array from test user number (1...N) to userid in database
94 */
95 private $userids;
96
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) {
107
108 // Set parameters.
109 $this->shortname = $shortname;
110
111 parent::__construct($size, $fixeddataset, $progress);
112 }
113
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 }
126
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 }
146
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');
155
156 raise_memory_limit(MEMORY_EXTRA);
157
158 if ($this->progress && !CLI_SCRIPT) {
159 echo html_writer::start_tag('ul');
160 }
161
162 $entirestart = microtime(true);
163
164 // Start transaction.
165 $transaction = $DB->start_delegated_transaction();
166
167 // Get generator.
168 $this->generator = phpunit_util::get_data_generator();
169
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();
177
178 // Log total time.
179 $this->log('coursecompleted', round(microtime(true) - $entirestart, 1));
180
181 if ($this->progress && !CLI_SCRIPT) {
182 echo html_writer::end_tag('ul');
183 }
184
185 // Commit transaction and finish.
186 $transaction->allow_commit();
187 return $this->course->id;
188 }
189
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 }
203
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;
211
212 // Work out total number of users.
213 $count = self::$paramusers[$this->size];
214
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];
228
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 }
235
236 // Stop if we've got enough users.
237 $nextnumber = $number + 1;
238 if ($number >= $count) {
239 break;
240 }
241 }
242 $rs->close();
243
244 // Create users from end of existing range.
245 if ($nextnumber <= $count) {
246 $this->create_user_accounts($nextnumber, $count);
247 }
248
249 // Assign all users to course.
250 $this->log('enrol', $count, true);
251
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);
263
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 }
269
270 // Sets the pointer at the beginning to be aware of the users we use.
271 reset($this->userids);
272
273 $this->end_log();
274 }
275
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;
293
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 }
303
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');
310
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 }
320
321 $this->end_log();
322 }
323
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);
330
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);
337
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';
345
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]);
349
350 $fs->create_file_from_string($filerecord, $data);
351 $this->dot($i, $count);
352 }
353
354 $this->end_log();
355 }
356
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 }
376
377 /**
378 * Creates a number of resource activities with one big file each.
379 */
380 private function create_big_files() {
381 global $CFG;
382
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);
387
388 $this->log('createbigfiles', $count, true);
389
390 // Prepare temp area.
391 $tempfolder = make_temp_directory('tool_generator');
392 $tempfile = $tempfolder . '/' . rand();
393
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);
403
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);
415
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 }
423
424 unlink($tempfile);
425 $this->end_log();
426 }
427
428 /**
429 * Creates one forum activity with a bunch of posts.
430 */
431 private function create_forum() {
432 global $DB;
433
434 $discussions = self::$paramforumdiscussions[$this->size];
435 $posts = self::$paramforumposts[$this->size];
436 $totalposts = $discussions * $posts;
437
438 $this->log('createforum', $totalposts, true);
439
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);
446
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 }
462
463 $this->end_log();
464 }
465
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() {
474
475 if (!$this->fixeddataset) {
476 $key = rand(1, self::$paramsections[$this->size]);
477 } else {
478 // Using section 1.
479 $key = 1;
480 }
481
482 return $key;
483 }
484
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() {
493
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 }
503
504 return $userid;
505 }
506
507}