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
+69
View File
@@ -0,0 +1,69 @@
<?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/>.
/**
* Frozen clock for testing purposes.
*
* @package core
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @property-read \DateTimeImmutable $time The current time of the clock
*/
class frozen_clock implements \core\clock {
/** @var DateTimeImmutable The next time of the clock */
public DateTimeImmutable $time;
/**
* Create a new instance of the frozen clock.
*
* @param null|int $time The initial time to use. If not specified, the current time is used.
*/
public function __construct(
?int $time = null,
) {
if ($time) {
$this->time = new \DateTimeImmutable("@{$time}");
} else {
$this->time = new \DateTimeImmutable();
}
}
public function now(): \DateTimeImmutable {
return $this->time;
}
public function time(): int {
return $this->time->getTimestamp();
}
/**
* Set the time of the clock.
*
* @param int $time
*/
public function set_to(int $time): void {
$this->time = new \DateTimeImmutable("@{$time}");
}
/**
* Bump the time by a number of seconds.
*
* @param int $seconds
*/
public function bump(int $seconds = 1): void {
$this->time = $this->time->modify("+{$seconds} seconds");
}
}
@@ -0,0 +1,67 @@
<?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/>.
/**
* Incrementing clock for testing purposes.
*
* @package core
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @property-read int $time The current time of the clock
*/
class incrementing_clock implements \core\clock {
/** @var int The next time of the clock */
public int $time;
/**
* Create a new instance of the incrementing clock.
*
* @param null|int $starttime The initial time to use. If not specified, the current time is used.
*/
public function __construct(
?int $starttime = null,
) {
$this->time = $starttime ?? time();
}
public function now(): \DateTimeImmutable {
return new \DateTimeImmutable('@' . $this->time++);
}
public function time(): int {
return $this->now()->getTimestamp();
}
/**
* Set the time of the clock.
*
* @param int $time
*/
public function set_to(int $time): void {
$this->time = $time;
}
/**
* Bump the time by a number of seconds.
*
* Note: The act of fetching the time will also bump the time by one second.
*
* @param int $seconds
*/
public function bump(int $seconds = 1): void {
$this->time += $seconds;
}
}
+116
View File
@@ -0,0 +1,116 @@
<?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/>.
/**
* Nasty strings to use in tests.
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
/**
* Nasty strings manager.
*
* Responds to nasty strings requests with a random string of the list
* to try with different combinations in different places.
*
* @package core
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class nasty_strings {
/**
* List of different strings to fill fields and assert against them
*
* Non of these strings can be a part of another one, this would not be good
* when using more one string at the same time and asserting results.
*
* @static
* @var array
*/
protected static $strings = array(
'< > & &lt; &gt; &amp; \' \\" \ \'$@NULL@$ @@TEST@@ \\\" \\ , ; : . 日本語­% %%',
'&amp; \' \\" \ \'$@NULL@$ < > & &lt; &gt; @@TEST@@ \\\" \\ , ; : . 日本語­% %%',
'< > & &lt; &gt; &amp; \' \\" \ \\\" \\ , ; : . \'$@NULL@$ @@TEST@@ 日本語­% %%',
'< > & &lt; &gt; &amp; \' \\" \ \'$@NULL@$ 日本語­% %%@@TEST@@ \. \\" \\ , ; :',
'< > & &lt; &gt; \\\" \\ , ; : . 日本語&amp; \' \\" \ \'$@NULL@$ @@TEST@@­% %%',
'\' \\" \ \'$@NULL@$ @@TEST@@ < > & &lt; &gt; &amp; \\\" \\ , ; : . 日本語­% %%',
'\\\" \\ , ; : . 日本語­% < > & &lt; &gt; &amp; \' \\" \ \'$@NULL@$ @@TEST@@ %%',
'< > & &lt; &gt; &amp; \' \\" \ \'$@NULL@$ 日本語­% %% @@TEST@@ \\\" \\ . , ; :',
'. 日本語&amp; \' \\" < > & &lt; &gt; \\ , ; : \ \'$@NULL@$ \\\" @@TEST@@­% %%',
'&amp; \' \\" \ < > & &lt; &gt; \\\" \\ , ; : . 日本語\'$@NULL@$ @@TEST@@­% %%',
);
/**
* Already used nasty strings.
*
* This array will be cleaned before each scenario.
*
* @static
* @var array
*/
protected static $usedstrings = array();
/**
* Returns a nasty string and stores the key mapping.
*
* @static
* @param string $key The key
* @return string
*/
public static function get($key) {
// If have been used during the this tests return it.
if (isset(self::$usedstrings[$key])) {
return self::$strings[self::$usedstrings[$key]];
}
// Getting non-used random string.
do {
$index = self::random_index();
} while (in_array($index, self::$usedstrings));
// Mark the string as already used.
self::$usedstrings[$key] = $index;
return self::$strings[$index];
}
/**
* Resets the used strings var.
* @static
* @return void
*/
public static function reset_used_strings() {
self::$usedstrings = array();
}
/**
* Returns a random index.
* @static
* @return int
*/
protected static function random_index() {
return mt_rand(0, count(self::$strings) - 1);
}
}
+121
View File
@@ -0,0 +1,121 @@
<?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/>.
/**
* Tests lock
*
* @package core
* @category test
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__.'/../lib.php');
/**
* Tests lock to prevent concurrent executions of the same test suite
*
* @package core
* @category test
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_lock {
/**
* @var array Array of resource used for prevention of parallel test execution
*/
protected static $lockhandles = array();
/**
* Prevent parallel test execution - this can not work in Moodle because we modify database and dataroot.
*
* Note: do not call manually!
*
* @internal
* @static
* @param string $framework phpunit|behat
* @param string $lockfilesuffix A sub-type used by the framework
* @return void
*/
public static function acquire(string $framework, string $lockfilesuffix = '') {
global $CFG;
$datarootpath = $CFG->{$framework . '_dataroot'} . '/' . $framework;
$lockfile = "{$datarootpath}/lock{$lockfilesuffix}";
if (!file_exists($datarootpath)) {
// Dataroot not initialised yet.
return;
}
if (!file_exists($lockfile)) {
file_put_contents($lockfile, 'This file prevents concurrent execution of Moodle ' . $framework . ' tests');
testing_fix_file_permissions($lockfile);
}
$lockhandlename = self::get_lock_handle_name($framework, $lockfilesuffix);
if (self::$lockhandles[$lockhandlename] = fopen($lockfile, 'r')) {
$wouldblock = null;
$locked = flock(self::$lockhandles[$lockhandlename], (LOCK_EX | LOCK_NB), $wouldblock);
if (!$locked) {
if ($wouldblock) {
echo "Waiting for other test execution to complete...\n";
}
$locked = flock(self::$lockhandles[$lockhandlename], LOCK_EX);
}
if (!$locked) {
fclose(self::$lockhandles[$lockhandlename]);
self::$lockhandles[$lockhandlename] = null;
}
}
register_shutdown_function(['test_lock', 'release'], $framework, $lockfilesuffix);
}
/**
* Note: do not call manually!
* @internal
* @static
* @param string $framework phpunit|behat
* @param string $lockfilesuffix A sub-type used by the framework
* @return void
*/
public static function release(string $framework, string $lockfilesuffix = '') {
$lockhandlename = self::get_lock_handle_name($framework, $lockfilesuffix);
if (self::$lockhandles[$lockhandlename]) {
flock(self::$lockhandles[$lockhandlename], LOCK_UN);
fclose(self::$lockhandles[$lockhandlename]);
self::$lockhandles[$lockhandlename] = null;
}
}
/**
* Get the name of the lock handle stored in the class.
*
* @param string $framework
* @param string $lockfilesuffix
* @return string
*/
protected static function get_lock_handle_name(string $framework, string $lockfilesuffix): string {
$lockhandlepieces = [$framework];
if (!empty($lockfilesuffix)) {
$lockhandlepieces[] = $lockfilesuffix;
}
return implode('%', $lockhandlepieces);
}
}
+207
View File
@@ -0,0 +1,207 @@
<?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/>.
/**
* Tests finder
*
* @package core
* @category test
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Finds components and plugins with tests
*
* @package core
* @category test
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tests_finder {
/**
* Returns all the components with tests of the specified type
* @param string $testtype The kind of test we are looking for
* @return array
*/
public static function get_components_with_tests($testtype) {
// Get all the components
$components = self::get_all_plugins_with_tests($testtype) + self::get_all_subsystems_with_tests($testtype);
// Get all the directories having tests
$directories = self::get_all_directories_with_tests($testtype);
// Find any directory not covered by proper components
$remaining = array_diff($directories, $components);
// Add them to the list of components
$components += $remaining;
return $components;
}
/**
* Returns all the plugins having tests
* @param string $testtype The kind of test we are looking for
* @return array all the plugins having tests
*/
private static function get_all_plugins_with_tests($testtype) {
$pluginswithtests = array();
$plugintypes = core_component::get_plugin_types();
ksort($plugintypes);
foreach ($plugintypes as $type => $unused) {
$plugs = core_component::get_plugin_list($type);
ksort($plugs);
foreach ($plugs as $plug => $fullplug) {
// Look for tests recursively
if (self::directory_has_tests($fullplug, $testtype)) {
$pluginswithtests[$type . '_' . $plug] = $fullplug;
}
}
}
return $pluginswithtests;
}
/**
* Returns all the subsystems having tests
*
* Note we are hacking here the list of subsystems
* to cover some well-known subsystems that are not properly
* returned by the {@link get_core_subsystems()} function.
*
* @param string $testtype The kind of test we are looking for
* @return array all the subsystems having tests
*/
private static function get_all_subsystems_with_tests($testtype) {
global $CFG;
$subsystemswithtests = array();
$subsystems = core_component::get_core_subsystems();
// Hack the list a bit to cover some well-known ones
$subsystems['backup'] = $CFG->dirroot.'/backup';
$subsystems['db-dml'] = $CFG->dirroot.'/lib/dml';
$subsystems['db-ddl'] = $CFG->dirroot.'/lib/ddl';
ksort($subsystems);
foreach ($subsystems as $subsys => $fullsubsys) {
if ($fullsubsys === null) {
continue;
}
if (!is_dir($fullsubsys)) {
continue;
}
// Look for tests recursively
if (self::directory_has_tests($fullsubsys, $testtype)) {
$subsystemswithtests['core_' . $subsys] = $fullsubsys;
}
}
return $subsystemswithtests;
}
/**
* Returns all the directories having tests
*
* @param string $testtype The kind of test we are looking for
* @return array all directories having tests
*/
private static function get_all_directories_with_tests($testtype) {
global $CFG;
// List of directories to exclude from test file searching.
$excludedir = array('node_modules', 'vendor');
// Get first level directories in which tests should be searched.
$directoriestosearch = array();
$alldirs = glob($CFG->dirroot . DIRECTORY_SEPARATOR . '*' , GLOB_ONLYDIR);
foreach ($alldirs as $dir) {
if (!in_array(basename($dir), $excludedir) && (filetype($dir) != 'link')) {
$directoriestosearch[] = $dir;
}
}
// Search for tests in valid directories.
$dirs = array();
foreach ($directoriestosearch as $dir) {
$dirite = new RecursiveDirectoryIterator($dir);
$iteite = new RecursiveIteratorIterator($dirite);
$regexp = self::get_regexp($testtype);
$regite = new RegexIterator($iteite, $regexp);
foreach ($regite as $path => $element) {
$key = dirname(dirname($path));
$value = trim(str_replace(DIRECTORY_SEPARATOR, '_', str_replace($CFG->dirroot, '', $key)), '_');
$dirs[$key] = $value;
}
}
ksort($dirs);
return array_flip($dirs);
}
/**
* Returns if a given directory has tests (recursively)
*
* @param string $dir full path to the directory to look for phpunit tests
* @param string $testtype phpunit|behat
* @return bool if a given directory has tests (true) or no (false)
*/
private static function directory_has_tests($dir, $testtype) {
if (!is_dir($dir)) {
return false;
}
$dirite = new RecursiveDirectoryIterator($dir);
$iteite = new RecursiveIteratorIterator($dirite);
$regexp = self::get_regexp($testtype);
$regite = new RegexIterator($iteite, $regexp);
$regite->rewind();
if ($regite->valid()) {
return true;
}
return false;
}
/**
* Returns the regular expression to match by the test files
* @param string $testtype
* @return string
*/
private static function get_regexp($testtype) {
$sep = preg_quote(DIRECTORY_SEPARATOR, '|');
switch ($testtype) {
case 'phpunit':
$regexp = '|'.$sep.'tests'.$sep.'.*_test\.php$|';
break;
case 'features':
$regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'.*\.feature$|';
break;
case 'stepsdefinitions':
$regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'behat_.*\.php$|';
break;
case 'behat':
$regexp = '!'.$sep.'tests'.$sep.'behat'.$sep.'(.*\.feature)|(behat_.*\.php)$!';
break;
}
return $regexp;
}
}
File diff suppressed because it is too large Load Diff
+165
View File
@@ -0,0 +1,165 @@
<?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/>.
/**
* Block generator base class.
*
* @package core
* @category test
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Block generator base class.
*
* Extend in blocks/xxxx/tests/generator/lib.php as class block_xxxx_generator.
*
* @package core
* @category test
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class testing_block_generator extends component_generator_base {
/** @var int number of created instances */
protected $instancecount = 0;
/**
* To be called from data reset code only,
* do not use in tests.
* @return void
*/
public function reset() {
$this->instancecount = 0;
}
/**
* Returns block name
* @return string name of block that this class describes
* @throws coding_exception if class invalid
*/
public function get_blockname() {
$matches = null;
if (!preg_match('/^block_([a-z0-9_]+)_generator$/', get_class($this), $matches)) {
throw new coding_exception('Invalid block generator class name: '.get_class($this));
}
if (empty($matches[1])) {
throw new coding_exception('Invalid block generator class name: '.get_class($this));
}
return $matches[1];
}
/**
* Fill in record defaults.
*
* @param stdClass $record
* @return stdClass
*/
protected function prepare_record(stdClass $record) {
$record->blockname = $this->get_blockname();
if (!isset($record->parentcontextid)) {
$record->parentcontextid = context_system::instance()->id;
}
if (!isset($record->showinsubcontexts)) {
$record->showinsubcontexts = 0;
}
if (!isset($record->pagetypepattern)) {
$record->pagetypepattern = '*';
}
if (!isset($record->subpagepattern)) {
$record->subpagepattern = null;
}
if (!isset($record->defaultregion)) {
$record->defaultregion = 'side-pre';
}
if (!isset($record->defaultweight)) {
$record->defaultweight = 5;
}
if (!isset($record->configdata)) {
$record->configdata = null;
}
return $record;
}
/**
* Create a test block instance.
*
* The $record passed in becomes the basis for the new row added to the
* block_instances table. You only need to supply the values of interest.
* Any missing values have sensible defaults filled in.
*
* The $options array provides additional data, not directly related to what
* will be inserted in the block_instance table, which may affect the block
* that is created. The meanings of any data passed here depends on the particular
* type of block being created.
*
* @param array|stdClass $record forms the basis for the entry to be inserted in the block_instances table.
* @param array $options further, block-specific options to control how the block is created.
* @return stdClass the block_instance record that has just been created.
*/
public function create_instance($record = null, $options = array()) {
global $DB, $PAGE;
$this->instancecount++;
// Creating a block is a back end operation, which should not cause any output to happen.
// This will allow us to check that the theme was not initialised while creating the block instance.
$outputstartedbefore = $PAGE->get_where_theme_was_initialised();
$record = (object)(array)$record;
$this->preprocess_record($record, $options);
$record = $this->prepare_record($record);
if (empty($record->timecreated)) {
$record->timecreated = time();
}
if (empty($record->timemodified)) {
$record->timemodified = time();
}
$id = $DB->insert_record('block_instances', $record);
context_block::instance($id);
$instance = $DB->get_record('block_instances', array('id' => $id), '*', MUST_EXIST);
// If the theme was initialised while creating the block instance, something somewhere called an output
// function. Rather than leaving this as a hard-to-debug situation, let's make it fail with a clear error.
$outputstartedafter = $PAGE->get_where_theme_was_initialised();
if ($outputstartedbefore === null && $outputstartedafter !== null) {
throw new coding_exception('Creating a block_' . $this->get_blockname() . ' initialised the theme and output!',
'This should not happen. Creating a block should be a pure back-end operation. Unnecessarily initialising ' .
'the output mechanism at the wrong time can cause subtle bugs and is a significant performance hit. There is ' .
'likely a call to an output function that caused it:' . PHP_EOL . PHP_EOL .
format_backtrace($outputstartedafter, true));
}
return $instance;
}
/**
* Can be overridden to do block-specific processing. $record can be modified
* in-place.
*
* @param stdClass $record the data, before defaults are filled in.
* @param array $options further, block-specific options, as passed to {@link create_instance()}.
*/
protected function preprocess_record(stdClass $record, array $options) {
}
}
@@ -0,0 +1,180 @@
<?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/>.
/**
* Component generator base class.
*
* @package core
* @category test
* @copyright 2013 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Component generator base class.
*
* Extend in path/to/component/tests/generator/lib.php as
* class type_plugin_generator extends component_generator_base
* Note that there are more specific classes to extend for mods and blocks.
*
* @copyright 2013 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class component_generator_base {
/**
* @var testing_data_generator
*/
protected $datagenerator;
/**
* Constructor.
* @param testing_data_generator $datagenerator
*/
public function __construct(testing_data_generator $datagenerator) {
$this->datagenerator = $datagenerator;
}
/**
* To be called from data reset code only,
* do not use in tests.
* @return void
*/
public function reset() {
}
/**
* Set the current user during data generation.
*
* This should be avoided wherever possible, but in some situations underlying code will insert data as the current
* user.
*
* @param stdClass $user
*/
protected function set_user(?stdClass $user = null): void {
global $CFG, $DB;
if ($user === null) {
$user = (object) [
'id' => 0,
'mnethostid' => $CFG->mnet_localhost_id,
];
} else {
$user = clone($user);
unset($user->description);
unset($user->access);
unset($user->preference);
}
// Ensure session is empty, as it may contain caches and user-specific info.
\core\session\manager::init_empty_session();
\core\session\manager::set_user($user);
}
/**
* Update the instance record, inserting any files that are referenced.
*
* @param stdClass $instance The instance record of the already-created record
* @param stdClass $record The data passed in to create the instance
* @param string $table The table that the data exists in
* @param context $context The context of the instance
* @param string $component The component of the owning plugin
* @param string $filearea The name of the file area
* @param int $targetitemid The itemid to use when saving the files
* @return stdClass The updated instance record
*/
protected function insert_files(
stdClass $instance,
stdClass $record,
string $table,
context $context,
string $component,
string $filearea,
int $targetitemid
): stdClass {
global $CFG, $DB, $USER;
$fieldname = "[[files::{$filearea}]]";
if (!isset($record->$fieldname)) {
return $instance;
}
preg_match('/\[\[files::(.*)\]\]/', $fieldname, $matches);
if (empty($matches[1])) {
throw new coding_exception('Invalid file field name: ' . $fieldname);
}
$referencedfieldname = trim($matches[1]);
if (!isset($record->$referencedfieldname)) {
throw new coding_exception("File field '{$fieldname}' references non-existent field '{$referencedfieldname}'");
}
$fs = get_file_storage();
$itemid = file_get_unused_draft_itemid();
$itemidfieldname = "{$referencedfieldname}[itemid]";
$record->$itemidfieldname = $itemid;
$filenames = explode(',', $record->$fieldname);
foreach ($filenames as $filename) {
$filename = trim($filename);
if (!$filename) {
continue;
}
$explodedfilename = explode('::', $filename, 3);
if (count($explodedfilename) === 2) {
[$sourcefile, $targetfile] = $explodedfilename;
$user = $USER;
} else {
[$sourcefile, $targetfile, $username] = $explodedfilename;
$user = \core_user::get_user_by_username($username);
}
$filepath = "{$CFG->dirroot}/{$sourcefile}";
if (!file_exists($filepath)) {
throw new coding_exception("File '{$filepath}' does not exist");
}
$filerecord = [
'userid' => $user->id,
'contextid' => context_user::instance($user->id)->id,
'component' => 'user',
'filearea' => 'draft',
'itemid' => $itemid,
'filepath' => '/' . dirname($targetfile),
'filename' => basename($targetfile),
];
$fs->create_file_from_pathname($filerecord, $filepath);
}
$olduser = $USER;
$this->set_user($user);
$instance->$referencedfieldname = file_save_draft_area_files(
$itemid,
$context->id,
$component,
$referencedfieldname,
$targetitemid,
null,
$instance->$referencedfieldname
);
$this->set_user($olduser);
$DB->update_record($table, $instance);
return $instance;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,59 @@
<?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/>.
/**
* Default block generator class.
*
* @package core
* @category test
* @copyright 2021 Moodle Pty Ltd. (http://moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Default block generator class to be used when a specific one is not supported.
*
* @package core
* @category test
* @copyright 2021 Moodle Pty Ltd. (http://moodle.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class default_block_generator extends testing_block_generator {
/**
* @var string
*/
private $blockname;
/**
* Constructor.
* @param testing_data_generator $datagenerator
* @param string $blockname
*/
public function __construct(testing_data_generator $datagenerator, string $blockname) {
parent::__construct($datagenerator);
$this->blockname = $blockname;
}
/**
* {@inheritdoc}
*/
public function get_blockname() {
return $this->blockname;
}
}
+33
View File
@@ -0,0 +1,33 @@
<?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/>.
/**
* Adds data generator support
*
* @package core
* @category test
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: MOODLE_INTERNAL is not verified here because we load this before setup.php!
require_once(__DIR__.'/data_generator.php');
require_once(__DIR__.'/component_generator_base.php');
require_once(__DIR__.'/module_generator.php');
require_once(__DIR__.'/block_generator.php');
require_once(__DIR__.'/default_block_generator.php');
require_once(__DIR__.'/repository_generator.php');
+326
View File
@@ -0,0 +1,326 @@
<?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/>.
/**
* Module generator base class.
*
* @package core
* @category test
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Module generator base class.
*
* Extend in mod/xxxx/tests/generator/lib.php as class mod_xxxx_generator.
*
* @package core
* @category test
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class testing_module_generator extends component_generator_base {
/**
* @var int number of created instances
*/
protected $instancecount = 0;
/**
* To be called from data reset code only,
* do not use in tests.
* @return void
*/
public function reset() {
$this->instancecount = 0;
}
/**
* Returns module name
* @return string name of module that this class describes
* @throws coding_exception if class invalid
*/
public function get_modulename() {
$matches = null;
if (!preg_match('/^mod_([a-z0-9]+)_generator$/', get_class($this), $matches)) {
throw new coding_exception('Invalid module generator class name: '.get_class($this));
}
if (empty($matches[1])) {
throw new coding_exception('Invalid module generator class name: '.get_class($this));
}
return $matches[1];
}
/**
* Create course module and link it to course
*
* Since 2.6 it is recommended to use function add_moduleinfo() to create a module.
*
* @deprecated since 2.6
* @see testing_module_generator::create_instance()
*
* @param integer $courseid
* @param array $options section, visible
* @return integer $cm instance id
*/
protected function precreate_course_module($courseid, array $options) {
global $DB, $CFG;
require_once("$CFG->dirroot/course/lib.php");
$modulename = $this->get_modulename();
$sectionnum = isset($options['section']) ? $options['section'] : 0;
unset($options['section']); // Prevent confusion, it would be overridden later in course_add_cm_to_section() anyway.
$cm = new stdClass();
$cm->course = $courseid;
$cm->module = $DB->get_field('modules', 'id', array('name'=>$modulename));
$cm->instance = 0;
$cm->section = 0;
$cm->idnumber = isset($options['idnumber']) ? $options['idnumber'] : 0;
$cm->added = time();
$columns = $DB->get_columns('course_modules');
foreach ($options as $key => $value) {
if ($key === 'id' or !isset($columns[$key])) {
continue;
}
if (property_exists($cm, $key)) {
continue;
}
$cm->$key = $value;
}
$cm->id = $DB->insert_record('course_modules', $cm);
course_add_cm_to_section($courseid, $cm->id, $sectionnum);
return $cm->id;
}
/**
* Called after *_add_instance()
*
* Since 2.6 it is recommended to use function add_moduleinfo() to create a module.
*
* @deprecated since 2.6
* @see testing_module_generator::create_instance()
*
* @param int $id
* @param int $cmid
* @return stdClass module instance
*/
protected function post_add_instance($id, $cmid) {
global $DB;
$DB->set_field('course_modules', 'instance', $id, array('id'=>$cmid));
$instance = $DB->get_record($this->get_modulename(), array('id'=>$id), '*', MUST_EXIST);
$cm = get_coursemodule_from_id($this->get_modulename(), $cmid, $instance->course, true, MUST_EXIST);
context_module::instance($cm->id);
$instance->cmid = $cm->id;
return $instance;
}
/**
* Merges together arguments $record and $options and fills default module
* fields that are shared by all module types
*
* @param object|array $record fields (different from defaults) for this module
* @param null|array $options for backward-compatibility this may include fields from course_modules
* table. They are merged into $record
* @throws coding_exception if $record->course is not specified
*/
protected function prepare_moduleinfo_record($record, $options) {
global $DB;
// Make sure we don't modify the original object.
$moduleinfo = (object)(array)$record;
if (empty($moduleinfo->course)) {
throw new coding_exception('module generator requires $record->course');
}
$moduleinfo->modulename = $this->get_modulename();
$moduleinfo->module = $DB->get_field('modules', 'id', array('name' => $moduleinfo->modulename));
// Allow idnumber to be set as either $options['idnumber'] or $moduleinfo->cmidnumber or $moduleinfo->idnumber.
// The actual field name is 'idnumber' but add_moduleinfo() expects 'cmidnumber'.
if (isset($options['idnumber'])) {
$moduleinfo->cmidnumber = $options['idnumber'];
} else if (!isset($moduleinfo->cmidnumber) && isset($moduleinfo->idnumber)) {
$moduleinfo->cmidnumber = $moduleinfo->idnumber;
}
// These are the fields from table 'course_modules' in 2.6 when the second
// argument $options is being deprecated.
// List excludes fields: instance (does not exist yet), course, module and idnumber (set above)
$easymergefields = array('section', 'added', 'score', 'indent',
'visible', 'visibleold', 'groupmode', 'groupingid',
'completion', 'completiongradeitemnumber', 'completionview', 'completionexpected',
'completionpassgrade', 'availability', 'showdescription');
foreach ($easymergefields as $key) {
if (isset($options[$key])) {
$moduleinfo->$key = $options[$key];
}
}
// Set default values. Note that visibleold and completiongradeitemnumber are not used when creating a module.
$defaults = array(
'section' => 0,
'visible' => 1,
'visibleoncoursepage' => 1,
'cmidnumber' => '',
'groupmode' => 0,
'groupingid' => 0,
'availability' => null,
'completion' => 0,
'completionview' => 0,
'completionexpected' => 0,
'completionpassgrade' => 0,
'conditiongradegroup' => array(),
'conditionfieldgroup' => array(),
'conditioncompletiongroup' => array()
);
foreach ($defaults as $key => $value) {
if (!isset($moduleinfo->$key)) {
$moduleinfo->$key = $value;
}
}
return $moduleinfo;
}
/**
* Creates an instance of the module for testing purposes.
*
* Module type will be taken from the class name. Each module type may overwrite
* this function to add other default values used by it.
*
* @param array|stdClass $record data for module being generated. Requires 'course' key
* (an id or the full object). Also can have any fields from add module form.
* @param null|array $options general options for course module. Since 2.6 it is
* possible to omit this argument by merging options into $record
* @return stdClass record from module-defined table with additional field
* cmid (corresponding id in course_modules table)
*/
public function create_instance($record = null, array $options = null) {
global $CFG, $DB, $PAGE;
require_once($CFG->dirroot.'/course/modlib.php');
$this->instancecount++;
// Creating an activity is a back end operation, which should not cause any output to happen.
// This will allow us to check that the theme was not initialised while creating the module instance.
$outputstartedbefore = $PAGE->get_where_theme_was_initialised();
// Merge options into record and add default values.
$record = $this->prepare_moduleinfo_record($record, $options);
// Retrieve the course record.
if (!empty($record->course->id)) {
$course = $record->course;
$record->course = $record->course->id;
} else {
$course = get_course($record->course);
}
// Fill the name and intro with default values (if missing).
if (empty($record->name)) {
$record->name = get_string('pluginname', $this->get_modulename()).' '.$this->instancecount;
// Module label can be created without name specified. It will get its name from the intro's text.
if ($this->get_modulename() === 'label') {
$record->name = '';
}
}
if (empty($record->introeditor) && empty($record->intro)) {
$record->intro = 'Test '.$this->get_modulename().' ' . $this->instancecount;
}
if (empty($record->introeditor) && empty($record->introformat)) {
$record->introformat = FORMAT_MOODLE;
}
if (isset($record->tags) && !is_array($record->tags)) {
$record->tags = preg_split('/\s*,\s*/', trim($record->tags), -1, PREG_SPLIT_NO_EMPTY);
}
// Before Moodle 2.6 it was possible to create a module with completion tracking when
// it is not setup for course and/or site-wide. Display debugging message so it is
// easier to trace an error in unittests.
if ($record->completion && empty($CFG->enablecompletion)) {
debugging('Did you forget to set $CFG->enablecompletion before generating module with completion tracking?', DEBUG_DEVELOPER);
}
if ($record->completion && empty($course->enablecompletion)) {
debugging('Did you forget to enable completion tracking for the course before generating module with completion tracking?', DEBUG_DEVELOPER);
}
if (!empty($record->lang) && !has_capability('moodle/course:setforcedlanguage', context_course::instance($course->id))) {
throw new coding_exception('Attempt to generate an activity when the current user does not have ' .
'permission moodle/course:setforcedlanguage. This does not work.');
}
// Add the module to the course.
$moduleinfo = add_moduleinfo($record, $course);
// Prepare object to return with additional field cmid.
$modulename = $this->get_modulename();
$instance = $DB->get_record($modulename, ['id' => $moduleinfo->instance], '*', MUST_EXIST);
$instance->cmid = $moduleinfo->coursemodule;
// Insert files for the 'intro' file area.
$instance = $this->insert_files(
$instance,
$record,
$modulename,
\context_module::instance($instance->cmid),
"mod_{$modulename}",
'intro',
0
);
// If the theme was initialised while creating the module instance, something somewhere called an output
// function. Rather than leaving this as a hard-to-debug situation, let's make it fail with a clear error.
$outputstartedafter = $PAGE->get_where_theme_was_initialised();
if ($outputstartedbefore === null && $outputstartedafter !== null) {
throw new coding_exception('Creating a mod_' . $this->get_modulename() . ' activity initialised the theme and output!',
'This should not happen. Creating an activity should be a pure back-end operation. Unnecessarily initialising ' .
'the output mechanism at the wrong time can cause subtle bugs and is a significant performance hit. There is ' .
'likely a call to an output function that caused it:' . PHP_EOL . PHP_EOL .
format_backtrace($outputstartedafter, true));
}
return $instance;
}
/**
* Generates a piece of content for the module.
* User is usually taken from global $USER variable.
* @param stdClass $instance object returned from create_instance() call
* @param stdClass|array $record
* @return stdClass generated object
* @throws coding_exception if function is not implemented by module
*/
public function create_content($instance, $record = array()) {
throw new coding_exception('Module generator for '.$this->get_modulename().' does not implement method create_content()');
}
}
@@ -0,0 +1,209 @@
<?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/>.
/**
* Repository data generator
*
* @package repository
* @category test
* @copyright 2013 Frédéric Massart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Repository data generator class
*
* @package core
* @category test
* @copyright 2013 Frédéric Massart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 2.5.1
*/
class testing_repository_generator extends component_generator_base {
/**
* Number of instances created
* @var int
*/
protected $instancecount = 0;
/**
* To be called from data reset code only,
* do not use in tests.
* @return void
*/
public function reset() {
$this->instancecount = 0;
}
/**
* Returns repository type name
*
* @return string name of the type of repository
* @throws coding_exception if class invalid
*/
public function get_typename() {
$matches = null;
if (!preg_match('/^repository_([a-z0-9_]+)_generator$/', get_class($this), $matches)) {
throw new coding_exception('Invalid repository generator class name: '.get_class($this));
}
if (empty($matches[1])) {
throw new coding_exception('Invalid repository generator class name: '.get_class($this));
}
return $matches[1];
}
/**
* Fill in record defaults.
*
* @param array $record
* @return array
*/
protected function prepare_record(array $record) {
if (!isset($record['name'])) {
$record['name'] = $this->get_typename() . ' ' . $this->instancecount;
}
if (!isset($record['contextid'])) {
$record['contextid'] = context_system::instance()->id;
}
return $record;
}
/**
* Fill in type record defaults.
*
* @param array $record
* @return array
*/
protected function prepare_type_record(array $record) {
if (!isset($record['pluginname'])) {
$record['pluginname'] = '';
}
if (!isset($record['enableuserinstances'])) {
$record['enableuserinstances'] = 1;
}
if (!isset($record['enablecourseinstances'])) {
$record['enablecourseinstances'] = 1;
}
return $record;
}
/**
* Create a test repository instance.
*
* @param array|stdClass $record
* @param array $options
* @return stdClass repository instance record
*/
public function create_instance($record = null, array $options = null) {
global $CFG, $DB, $PAGE;
require_once($CFG->dirroot . '/repository/lib.php');
$this->instancecount++;
$record = (array) $record;
// Creating a repository is a back end operation, which should not cause any output to happen.
// This will allow us to check that the theme was not initialised while creating the repository instance.
$outputstartedbefore = $PAGE->get_where_theme_was_initialised();
$typeid = $DB->get_field('repository', 'id', array('type' => $this->get_typename()), MUST_EXIST);
$instanceoptions = repository::static_function($this->get_typename(), 'get_instance_option_names');
if (empty($instanceoptions)) {
// There can only be one instance of this repository, and it should have been created along with the type.
$id = $DB->get_field('repository_instances', 'id', array('typeid' => $typeid), MUST_EXIST);
} else {
// Create the new instance, but first make sure all the required parameters are set.
$record = $this->prepare_record($record);
if (empty($record['contextid'])) {
throw new coding_exception('contextid must be present in testing_repository_generator::create_instance() $record');
}
foreach ($instanceoptions as $option) {
if (!isset($record[$option])) {
throw new coding_exception("$option must be present in testing_repository_generator::create_instance() \$record");
}
}
$context = context::instance_by_id($record['contextid']);
unset($record['contextid']);
if (!in_array($context->contextlevel, array(CONTEXT_SYSTEM, CONTEXT_COURSE, CONTEXT_USER))) {
throw new coding_exception('Wrong contextid passed in testing_repository_generator::create_instance() $record');
}
$id = repository::static_function($this->get_typename(), 'create', $this->get_typename(), 0, $context, $record);
}
// If the theme was initialised while creating the repository instance, something somewhere called an output
// function. Rather than leaving this as a hard-to-debug situation, let's make it fail with a clear error.
$outputstartedafter = $PAGE->get_where_theme_was_initialised();
if ($outputstartedbefore === null && $outputstartedafter !== null) {
throw new coding_exception('Creating a repository_' . $this->get_typename() . ' initialised the theme and output!',
'This should not happen. Creating a repository should be a pure back-end operation. Unnecessarily initialising ' .
'the output mechanism at the wrong time can cause subtle bugs and is a significant performance hit. There is ' .
'likely a call to an output function that caused it:' . PHP_EOL . PHP_EOL .
format_backtrace($outputstartedafter, true));
}
return $DB->get_record('repository_instances', array('id' => $id), '*', MUST_EXIST);
}
/**
* Create the type of repository.
*
* @param stdClass|array $record data to use to set up the type
* @param array $options options for the set up of the type
*
* @return stdClass repository type record
*/
public function create_type($record = null, array $options = null) {
global $CFG, $DB;
require_once($CFG->dirroot . '/repository/lib.php');
$record = (array) $record;
$type = $this->get_typename();
$typeoptions = repository::static_function($type, 'get_type_option_names');
$instanceoptions = repository::static_function($type, 'get_instance_option_names');
// The type allow for user and course instances.
if (!empty($instanceoptions)) {
$typeoptions[] = 'enableuserinstances';
$typeoptions[] = 'enablecourseinstances';
}
// Make sure all the parameters are set.
$record = $this->prepare_type_record($record);
foreach ($typeoptions as $option) {
if (!isset($record[$option])) {
throw new coding_exception("$option must be present in testing::create_repository_type() for $type");
}
}
// Limit to allowed options.
$record = array_intersect_key($record, array_flip($typeoptions));
// Create the type.
$plugintype = new repository_type($type, $record);
$plugintype->create(false);
return $DB->get_record('repository', array('type' => $type));
}
}
+307
View File
@@ -0,0 +1,307 @@
<?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/>.
/**
* Testing general functions
*
* Note: these functions must be self contained and must not rely on any library or include
*
* @package core
* @category test
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Composer error exit status.
*
* @var int
*/
define('TESTING_EXITCODE_COMPOSER', 255);
/**
* Returns relative path against current working directory,
* to be used for shell execution hints.
* @param string $moodlepath starting with "/", ex: "/admin/tool/cli/init.php"
* @return string path relative to current directory or absolute path
*/
function testing_cli_argument_path($moodlepath) {
global $CFG;
if (isset($CFG->admin) and $CFG->admin !== 'admin') {
$moodlepath = preg_replace('|^/admin/|', "/$CFG->admin/", $moodlepath);
}
if (isset($_SERVER['REMOTE_ADDR'])) {
// Web access, this should not happen often.
$cwd = dirname(dirname(__DIR__));
} else {
// This is the real CLI script, work with relative paths.
$cwd = getcwd();
}
// Remove last directory separator as $path will not contain one.
if ((substr($cwd, -1) === '/') || (substr($cwd, -1) === '\\')) {
$cwd = substr($cwd, -1);
}
$path = realpath($CFG->dirroot.$moodlepath);
// We need standrad directory seperator for path and cwd, so it can be compared.
$cwd = testing_cli_fix_directory_separator($cwd);
$path = testing_cli_fix_directory_separator($path);
if (strpos($path, $cwd) === 0) {
// Remove current working directory and directory separator.
$path = substr($path, strlen($cwd) + 1);
}
return $path;
}
/**
* Try to change permissions to $CFG->dirroot or $CFG->dataroot if possible
* @param string $file
* @return bool success
*/
function testing_fix_file_permissions($file) {
global $CFG;
$permissions = fileperms($file);
if ($permissions & $CFG->filepermissions != $CFG->filepermissions) {
$permissions = $permissions | $CFG->filepermissions;
return chmod($file, $permissions);
}
return true;
}
/**
* Find out if running under Cygwin on Windows.
* @return bool
*/
function testing_is_cygwin() {
if (empty($_SERVER['OS']) or $_SERVER['OS'] !== 'Windows_NT') {
return false;
} else if (!empty($_SERVER['SHELL']) and $_SERVER['SHELL'] === '/bin/bash') {
return true;
} else if (!empty($_SERVER['TERM']) and $_SERVER['TERM'] === 'cygwin') {
return true;
} else {
return false;
}
}
/**
* Returns whether a mingw CLI is running.
*
* MinGW sets $_SERVER['TERM'] to cygwin, but it
* can not run .bat files; this function may be useful
* when we need to output proposed commands to users
* using Windows CLI interfaces.
*
* @link http://sourceforge.net/p/mingw/bugs/1902
* @return bool
*/
function testing_is_mingw() {
if (!testing_is_cygwin()) {
return false;
}
if (!empty($_SERVER['MSYSTEM'])) {
return true;
}
return false;
}
/**
* Mark empty dataroot to be used for testing.
* @param string $dataroot The dataroot directory
* @param string $framework The test framework
* @return void
*/
function testing_initdataroot($dataroot, $framework) {
global $CFG;
$filename = $dataroot . '/' . $framework . 'testdir.txt';
umask(0);
if (!file_exists($filename)) {
file_put_contents($filename, 'Contents of this directory are used during tests only, do not delete this file!');
}
testing_fix_file_permissions($filename);
$varname = $framework . '_dataroot';
$datarootdir = $CFG->{$varname} . '/' . $framework;
if (!file_exists($datarootdir)) {
mkdir($datarootdir, $CFG->directorypermissions);
}
}
/**
* Prints an error and stops execution
*
* @param integer $errorcode
* @param string $text
* @return void exits
*/
function testing_error($errorcode, $text = '') {
// do not write to error stream because we need the error message in PHP exec result from web ui
echo($text."\n");
if (isset($_SERVER['REMOTE_ADDR'])) {
header('HTTP/1.1 500 Internal Server Error');
}
exit($errorcode);
}
/**
* Perform necessary steps to install and/or upgrade composer and its dependencies.
*
* Installation is mandatory, but upgrade is optional.
*
* Note: This function does not return, but will call `exit()` on error.
*
* @param bool $selfupdate Perform a composer self-update to update the composer.phar utility
* @param bool $updatedependencies Upgrade dependencies
*/
function testing_update_composer_dependencies(bool $selfupdate = true, bool $updatedependencies = true): void {
// To restore the value after finishing.
$cwd = getcwd();
// Set some paths.
$dirroot = dirname(dirname(__DIR__));
// Switch to Moodle's dirroot for easier path handling.
chdir($dirroot);
// Download or update composer.phar. Unfortunately we can't use the curl
// class in filelib.php as we're running within one of the test platforms.
$composerpath = $dirroot . DIRECTORY_SEPARATOR . 'composer.phar';
if (!file_exists($composerpath)) {
$composerurl = 'https://getcomposer.org/composer.phar';
$file = @fopen($composerpath, 'w');
if ($file === false) {
$errordetails = error_get_last();
$error = sprintf("Unable to create composer.phar\nPHP error: %s",
$errordetails['message']);
testing_error(TESTING_EXITCODE_COMPOSER, $error);
}
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $composerurl);
curl_setopt($curl, CURLOPT_FILE, $file);
$result = curl_exec($curl);
$curlerrno = curl_errno($curl);
$curlerror = curl_error($curl);
$curlinfo = curl_getinfo($curl);
curl_close($curl);
fclose($file);
if (!$result) {
$error = sprintf("Unable to download composer.phar\ncURL error (%d): %s",
$curlerrno, $curlerror);
testing_error(TESTING_EXITCODE_COMPOSER, $error);
} else if ($curlinfo['http_code'] === 404) {
if (file_exists($composerpath)) {
// Deleting the resource as it would contain HTML.
unlink($composerpath);
}
$error = sprintf("Unable to download composer.phar\n" .
"404 http status code fetching $composerurl");
testing_error(TESTING_EXITCODE_COMPOSER, $error);
}
// Do not self-update after installation.
$selfupdate = false;
}
if ($selfupdate) {
passthru("php composer.phar self-update", $code);
if ($code != 0) {
exit($code);
}
}
// If the vendor directory does not exist, force the installation of dependencies.
$vendorpath = $dirroot . DIRECTORY_SEPARATOR . 'vendor';
if (!file_exists($vendorpath)) {
$updatedependencies = true;
}
if ($updatedependencies) {
// Update composer dependencies.
passthru("php composer.phar install", $code);
if ($code != 0) {
exit($code);
}
}
// Return to our original location.
chdir($cwd);
}
/**
* Fix DIRECTORY_SEPARATOR for windows.
*
* In PHP on Windows, DIRECTORY_SEPARATOR is set to the backslash (\)
* character. However, if you're running a Cygwin/Msys/Git shell
* exec() calls will return paths using the forward slash (/) character.
*
* NOTE: Because PHP on Windows will accept either forward or backslashes,
* paths should be built using ONLY forward slashes, regardless of
* OS. MOODLE_DIRECTORY_SEPARATOR should only be used when parsing
* paths returned by the shell.
*
* @param string $path
* @return string
*/
function testing_cli_fix_directory_separator($path) {
global $CFG;
static $dirseparator = null;
if (!$dirseparator) {
// Default directory separator.
$dirseparator = DIRECTORY_SEPARATOR;
// On windows we need to find what directory separator is used.
if ($CFG->ostype = 'WINDOWS') {
if (!empty($_SERVER['argv'][0])) {
if (false === strstr($_SERVER['argv'][0], '\\')) {
$dirseparator = '/';
} else {
$dirseparator = '\\';
}
} else if (testing_is_cygwin()) {
$dirseparator = '/';
}
}
}
// Normalize \ and / to directory separator.
$path = str_replace('\\', $dirseparator, $path);
$path = str_replace('/', $dirseparator, $path);
return $path;
}
+106
View File
@@ -0,0 +1,106 @@
<?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 core;
use frozen_clock;
use incrementing_clock;
/**
* Tests for testing clocks.
*
* @package core
* @copyright 2024 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class clock_test extends \advanced_testcase {
/**
* Test the incrementing mock clock.
*
* @covers \incrementing_clock
*/
public function test_clock_with_incrementing(): void {
require_once(__DIR__ . '/../classes/incrementing_clock.php');
$clock = new incrementing_clock();
$this->assertInstanceOf(\incrementing_clock::class, $clock);
$initialtime = $clock->now()->getTimestamp();
// Test the functionality.
$this->assertEquals($initialtime + 1, $clock->now()->getTimestamp());
$this->assertEquals($initialtime + 2, $clock->time());
$this->assertEquals($initialtime + 3, $clock->now()->getTimestamp());
// Specify a specific start time.
$clock = new incrementing_clock(12345);
$this->assertEquals(12345, $clock->now()->getTimestamp());
$this->assertEquals(12346, $clock->time());
$this->assertEquals(12347, $clock->now()->getTimestamp());
$clock->set_to(12345);
$this->assertEquals(12345, $clock->time());
$this->assertEquals(12346, $clock->time());
$clock->bump();
$this->assertEquals(12348, $clock->time());
$clock->bump();
$this->assertEquals(12350, $clock->time());
$clock->bump(5);
$this->assertEquals(12356, $clock->time());
}
/**
* Test the incrementing mock clock.
*
* @covers \frozen_clock
*/
public function test_mock_clock_with_frozen(): void {
require_once(__DIR__ . '/../classes/frozen_clock.php');
$clock = new frozen_clock();
// Test the functionality.
$initialtime = $clock->now()->getTimestamp();
$this->assertEquals($initialtime, $clock->now()->getTimestamp());
$this->assertEquals($initialtime, $clock->now()->getTimestamp());
$this->assertEquals($initialtime, $clock->now()->getTimestamp());
$this->assertEquals($initialtime, $clock->time());
// Specify a specific start time.
$clock = new frozen_clock(12345);
$initialtime = $clock->now();
$this->assertEquals($initialtime, $clock->now());
$this->assertEquals($initialtime, $clock->now());
$this->assertEquals($initialtime, $clock->now());
$clock->set_to(12345);
$this->assertEquals(12345, $clock->now()->getTimestamp());
$this->assertEquals(12345, $clock->now()->getTimestamp());
$this->assertEquals(12345, $clock->now()->getTimestamp());
$this->assertEquals(12345, $clock->time());
$clock->bump();
$this->assertEquals(12346, $clock->time());
$clock->bump();
$this->assertEquals(12347, $clock->time());
$clock->bump(5);
$this->assertEquals(12352, $clock->time());
}
}
@@ -0,0 +1,624 @@
<?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 core;
/**
* Test data generator
*
* @package core
* @category phpunit
* @copyright 2012 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \core\testing_data_generator
*/
class testing_generator_test extends \advanced_testcase {
public function test_get_plugin_generator_good_case(): void {
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$this->assertInstanceOf('core_question_generator', $generator);
}
public function test_get_plugin_generator_sloppy_name(): void {
$generator = $this->getDataGenerator()->get_plugin_generator('quiz');
$this->assertDebuggingCalled('Please specify the component you want a generator for as ' .
'mod_quiz, not quiz.', DEBUG_DEVELOPER);
$this->assertInstanceOf('mod_quiz_generator', $generator);
}
public function test_get_default_generator(): void {
$generator = $this->getDataGenerator()->get_plugin_generator('block_somethingthatdoesnotexist');
$this->assertInstanceOf('default_block_generator', $generator);
}
/**
* Test plugin generator, with no component directory.
*/
public function test_get_plugin_generator_no_component_dir(): void {
$this->expectException(\coding_exception::class);
$this->expectExceptionMessage('Component core_cohort does not support generators yet. Missing tests/generator/lib.php.');
$generator = $this->getDataGenerator()->get_plugin_generator('core_cohort');
}
public function test_create_user(): void {
global $DB, $CFG;
require_once($CFG->dirroot.'/user/lib.php');
$this->resetAfterTest(true);
$generator = $this->getDataGenerator();
$count = $DB->count_records('user');
$this->setCurrentTimeStart();
$user = $generator->create_user();
$this->assertEquals($count + 1, $DB->count_records('user'));
$this->assertSame($user->username, \core_user::clean_field($user->username, 'username'));
$this->assertSame($user->email, \core_user::clean_field($user->email, 'email'));
$this->assertNotEmpty($user->firstnamephonetic);
$this->assertNotEmpty($user->lastnamephonetic);
$this->assertNotEmpty($user->alternatename);
$this->assertNotEmpty($user->middlename);
$this->assertSame('manual', $user->auth);
$this->assertSame('en', $user->lang);
$this->assertSame('1', $user->confirmed);
$this->assertSame('0', $user->deleted);
$this->assertTimeCurrent($user->timecreated);
$this->assertSame($user->timecreated, $user->timemodified);
$this->assertSame('0.0.0.0', $user->lastip);
$record = array(
'auth' => 'email',
'firstname' => 'Žluťoučký',
'lastname' => 'Koníček',
'firstnamephonetic' => 'Zhlutyoucky',
'lastnamephonetic' => 'Koniiczek',
'middlename' => 'Hopper',
'alternatename' => 'horse',
'idnumber' => 'abc1',
'mnethostid' => (string)$CFG->mnet_localhost_id,
'username' => 'konic666',
'password' => 'password1',
'email' => 'email@example.com',
'confirmed' => '1',
'maildisplay' => '1',
'mailformat' => '0',
'maildigest' => '1',
'autosubscribe' => '0',
'trackforums' => '0',
'deleted' => '0',
'timecreated' => '666',
);
$user = $generator->create_user($record);
$this->assertEquals($count + 2, $DB->count_records('user'));
foreach ($record as $k => $v) {
if ($k === 'password') {
$this->assertTrue(password_verify($v, $user->password));
} else {
$this->assertSame($v, $user->{$k});
}
}
$record = array(
'firstname' => 'Some',
'lastname' => 'User',
'idnumber' => 'def',
'username' => 'user666',
'email' => 'email666@example.com',
'deleted' => '1',
);
$user = $generator->create_user($record);
$this->assertEquals($count + 3, $DB->count_records('user'));
$this->assertSame('', $user->idnumber);
$this->assertSame(md5($record['username']), $user->email);
$this->assertEquals(1, $user->deleted);
// Test generating user with interests.
$user = $generator->create_user(array('interests' => 'Cats, Dogs'));
$userdetails = user_get_user_details($user);
$this->assertSame('Cats, Dogs', $userdetails['interests']);
}
public function test_create(): void {
global $DB;
$this->resetAfterTest(true);
$generator = $this->getDataGenerator();
$count = $DB->count_records('course_categories');
$category = $generator->create_category();
$this->assertEquals($count+1, $DB->count_records('course_categories'));
$this->assertMatchesRegularExpression('/^Course category \d/', $category->name);
$this->assertSame('', $category->idnumber);
$this->assertMatchesRegularExpression('/^Test course category \d/', $category->description);
$this->assertSame(FORMAT_MOODLE, $category->descriptionformat);
$count = $DB->count_records('cohort');
$cohort = $generator->create_cohort();
$this->assertEquals($count+1, $DB->count_records('cohort'));
$this->assertEquals(\context_system::instance()->id, $cohort->contextid);
$this->assertMatchesRegularExpression('/^Cohort \d/', $cohort->name);
$this->assertSame('', $cohort->idnumber);
$this->assertMatchesRegularExpression("/^Description for '{$cohort->name}' \\n/", $cohort->description);
$this->assertSame(FORMAT_MOODLE, $cohort->descriptionformat);
$this->assertSame('', $cohort->component);
$this->assertLessThanOrEqual(time(), $cohort->timecreated);
$this->assertSame($cohort->timecreated, $cohort->timemodified);
$count = $DB->count_records('course');
$course = $generator->create_course();
$this->assertEquals($count+1, $DB->count_records('course'));
$this->assertMatchesRegularExpression('/^Test course \d/', $course->fullname);
$this->assertMatchesRegularExpression('/^tc_\d/', $course->shortname);
$this->assertSame('', $course->idnumber);
$this->assertSame('topics', $course->format);
$this->assertEquals(0, $course->newsitems);
$this->assertEquals(5, course_get_format($course)->get_last_section_number());
$this->assertMatchesRegularExpression('/^Test course \d/', $course->summary);
$this->assertSame(FORMAT_MOODLE, $course->summaryformat);
$section = $generator->create_course_section(array('course'=>$course->id, 'section'=>3));
$this->assertEquals($course->id, $section->course);
$this->assertNull($section->name);
$course = $generator->create_course(array('tags' => 'Cat, Dog'));
$this->assertEquals(array('Cat', 'Dog'), array_values(\core_tag_tag::get_item_tags_array('core', 'course', $course->id)));
$scale = $generator->create_scale();
$this->assertNotEmpty($scale);
}
/**
* Test case for the `test_create_course_initsections` method.
*
* This method tests the behavior of creating a course with initialized sections.
* It checks that the sections are renamed to "Section x" when `initsections` is true,
* and that the sections are not renamed when `initsections` is false.
*
* @covers ::create_course
*/
public function test_create_course_initsections(): void {
global $DB;
$this->resetAfterTest(true);
$generator = $this->getDataGenerator();
// Check that the sections are renamed to "Section x" when initsections is true.
$course = $generator->create_course(['initsections' => 1]);
$sections = course_get_format($course)->get_sections();
foreach ($sections as $section) {
if ($section->sectionnum > 0) {
$this->assertEquals(get_string('section') . ' ' . $section->sectionnum, $section->name);
}
}
// Check that the sections are not renamed when initsections is false.
$course = $generator->create_course(['initsections' => 0]);
$sections = course_get_format($course)->get_sections();
foreach ($sections as $section) {
if ($section->sectionnum > 0) {
$this->assertNull($section->name);
}
}
}
public function test_create_module(): void {
global $CFG, $SITE, $DB;
$this->setAdminUser();
if (!file_exists("$CFG->dirroot/mod/page/")) {
$this->markTestSkipped('Can not find standard Page module');
}
$this->resetAfterTest(true);
$generator = $this->getDataGenerator();
$page = $generator->create_module('page', array('course'=>$SITE->id));
$this->assertNotEmpty($page);
$cm = get_coursemodule_from_instance('page', $page->id, $SITE->id, true);
$this->assertEquals(0, $cm->sectionnum);
$page = $generator->create_module('page', array('course'=>$SITE->id), array('section'=>3));
$this->assertNotEmpty($page);
$cm = get_coursemodule_from_instance('page', $page->id, $SITE->id, true);
$this->assertEquals(3, $cm->sectionnum);
$page = $generator->create_module('page', array('course' => $SITE->id, 'tags' => 'Cat, Dog'));
$this->assertEquals(array('Cat', 'Dog'),
array_values(\core_tag_tag::get_item_tags_array('core', 'course_modules', $page->cmid)));
// Prepare environment to generate modules with all possible options.
// Enable advanced functionality.
$CFG->enablecompletion = 1;
$CFG->enableavailability = 1;
$CFG->enableoutcomes = 1;
require_once($CFG->libdir.'/gradelib.php');
require_once($CFG->libdir.'/completionlib.php');
require_once($CFG->dirroot.'/rating/lib.php');
// Create a course with enabled completion.
$course = $generator->create_course(array('enablecompletion' => true));
// Create new grading category in this course.
$grade_category = new \grade_category();
$grade_category->courseid = $course->id;
$grade_category->fullname = 'Grade category';
$grade_category->insert();
// Create group and grouping.
$group = $generator->create_group(array('courseid' => $course->id));
$grouping = $generator->create_grouping(array('courseid' => $course->id));
$generator->create_grouping_group(array('groupid' => $group->id, 'groupingid' => $grouping->id));
// Prepare arrays with properties that we can both use for creating modules and asserting the data in created modules.
// General properties.
$optionsgeneral = array(
'visible' => 0, // Note: 'visibleold' will always be set to the same value as 'visible'.
'section' => 3, // Note: section will be created if does not exist.
// Module supports FEATURE_IDNUMBER.
'cmidnumber' => 'IDNUM', // Note: alternatively can have key 'idnumber'.
// Module supports FEATURE_GROUPS;
'groupmode' => SEPARATEGROUPS, // Note: will be reset to 0 if course groupmodeforce is set.
// Module supports FEATURE_GROUPINGS.
'groupingid' => $grouping->id,
);
// In case completion is enabled on site and for course every module can have manual completion.
$featurecompletionmanual = array(
'completion' => COMPLETION_TRACKING_MANUAL, // "Students can manually mark activity as completed."
'completionexpected' => time() + 7 * DAYSECS,
);
// Automatic completion is possible if module supports FEATURE_COMPLETION_TRACKS_VIEWS or FEATURE_GRADE_HAS_GRADE.
// Note: completionusegrade is stored in DB and can be found in cm_info as 'completiongradeitemnumber' - either NULL or 0.
// Note: module can have more autocompletion rules as defined in moodleform_mod::add_completion_rules().
$featurecompletionautomatic = array(
'completion' => COMPLETION_TRACKING_AUTOMATIC, // "Show activity as complete when conditions are met."
'completionview' => 1, // "Student must view this activity to complete it"
'completionusegrade' => 1, // "Student must receive a grade to complete this activity"
'completionpassgrade' => 1, // "Student must receive a passing grade to complete this activity"
);
// Module supports FEATURE_RATE:
$featurerate = array(
'assessed' => RATING_AGGREGATE_AVERAGE, // "Aggregate type"
'scale' => 100, // Either max grade or negative number for scale id.
'ratingtime' => 1, // "Restrict ratings to items with dates in this range".
'assesstimestart' => time() - DAYSECS, // Note: Will be ignored if neither 'assessed' nor 'ratingtime' is set.
'assesstimefinish' => time() + DAYSECS, // Note: Will be ignored if neither 'assessed' nor 'ratingtime' is set.
);
// Module supports FEATURE_GRADE_HAS_GRADE:
$featuregrade = array(
'grade' => 10,
'gradecat' => $grade_category->id, // Note: if $CFG->enableoutcomes is set, this can be set to -1 to automatically create new grade category.
);
// Now let's create several modules with different options.
$m1 = $generator->create_module('assign',
array('course' => $course->id) +
$optionsgeneral);
$m2 = $generator->create_module('data',
array('course' => $course->id) +
$featurecompletionmanual +
$featurerate);
$m3 = $generator->create_module('assign',
array('course' => $course->id) +
$featurecompletionautomatic +
$featuregrade);
// We need id of the grading item for the second module to create availability dependency in the 3rd module.
$gradingitem = \grade_item::fetch(array('courseid'=>$course->id, 'itemtype'=>'mod', 'itemmodule' => 'assign', 'iteminstance' => $m3->id));
// Now prepare option to create the 4th module with an availability condition.
$optionsavailability = array(
'availability' => '{"op":"&","showc":[true],"c":[' .
'{"type":"date","d":">=","t":' . (time() - WEEKSECS) . '}]}',
);
// Create module with conditional availability.
$m4 = $generator->create_module('assign',
array('course' => $course->id) +
$optionsavailability
);
// Verifying that everything is generated correctly.
$modinfo = get_fast_modinfo($course->id);
$cm1 = $modinfo->cms[$m1->cmid];
$this->assertEquals($optionsgeneral['visible'], $cm1->visible);
$this->assertEquals($optionsgeneral['section'], $cm1->sectionnum); // Note difference in key.
$this->assertEquals($optionsgeneral['cmidnumber'], $cm1->idnumber); // Note difference in key.
$this->assertEquals($optionsgeneral['groupmode'], $cm1->groupmode);
$this->assertEquals($optionsgeneral['groupingid'], $cm1->groupingid);
$cm2 = $modinfo->cms[$m2->cmid];
$this->assertEquals($featurecompletionmanual['completion'], $cm2->completion);
$this->assertEquals($featurecompletionmanual['completionexpected'], $cm2->completionexpected);
$this->assertEquals(null, $cm2->completiongradeitemnumber);
// Rating info is stored in the module's table (in our test {data}).
$data = $DB->get_record('data', array('id' => $m2->id));
$this->assertEquals($featurerate['assessed'], $data->assessed);
$this->assertEquals($featurerate['scale'], $data->scale);
$this->assertEquals($featurerate['assesstimestart'], $data->assesstimestart);
$this->assertEquals($featurerate['assesstimefinish'], $data->assesstimefinish);
// No validation for 'ratingtime'. It is only used in to enable/disable assesstime* when adding module.
$cm3 = $modinfo->cms[$m3->cmid];
$this->assertEquals($featurecompletionautomatic['completion'], $cm3->completion);
$this->assertEquals($featurecompletionautomatic['completionview'], $cm3->completionview);
$this->assertEquals($featurecompletionautomatic['completionpassgrade'], $cm3->completionpassgrade);
$this->assertEquals(0, $cm3->completiongradeitemnumber); // Zero instead of default null since 'completionusegrade' was set.
$gradingitem = \grade_item::fetch(array('courseid'=>$course->id, 'itemtype'=>'mod', 'itemmodule' => 'assign', 'iteminstance' => $m3->id));
$this->assertEquals(0, $gradingitem->grademin);
$this->assertEquals($featuregrade['grade'], $gradingitem->grademax);
$this->assertEquals($featuregrade['gradecat'], $gradingitem->categoryid);
$cm4 = $modinfo->cms[$m4->cmid];
$this->assertEquals($optionsavailability['availability'], $cm4->availability);
}
public function test_create_block(): void {
global $CFG;
if (!file_exists("$CFG->dirroot/blocks/online_users/")) {
$this->markTestSkipped('Can not find standard Online users block');
}
$this->resetAfterTest(true);
$generator = $this->getDataGenerator();
$page = $generator->create_block('online_users');
$this->assertNotEmpty($page);
}
public function test_enrol_user(): void {
global $DB;
$this->resetAfterTest();
$selfplugin = enrol_get_plugin('self');
$this->assertNotEmpty($selfplugin);
$manualplugin = enrol_get_plugin('manual');
$this->assertNotEmpty($manualplugin);
// Prepare some data.
$studentrole = $DB->get_record('role', array('shortname'=>'student'));
$this->assertNotEmpty($studentrole);
$teacherrole = $DB->get_record('role', array('shortname'=>'teacher'));
$this->assertNotEmpty($teacherrole);
$course1 = $this->getDataGenerator()->create_course();
$course2 = $this->getDataGenerator()->create_course();
$course3 = $this->getDataGenerator()->create_course();
$context1 = \context_course::instance($course1->id);
$context2 = \context_course::instance($course2->id);
$context3 = \context_course::instance($course3->id);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
$user4 = $this->getDataGenerator()->create_user();
$this->assertEquals(3, $DB->count_records('enrol', array('enrol'=>'self')));
$instance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'self'), '*', MUST_EXIST);
$instance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'self'), '*', MUST_EXIST);
$instance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'self'), '*', MUST_EXIST);
$this->assertEquals($studentrole->id, $instance1->roleid);
$this->assertEquals($studentrole->id, $instance2->roleid);
$this->assertEquals($studentrole->id, $instance3->roleid);
$this->assertEquals(3, $DB->count_records('enrol', array('enrol'=>'manual')));
$maninstance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'manual'), '*', MUST_EXIST);
$maninstance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'manual'), '*', MUST_EXIST);
$maninstance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'manual'), '*', MUST_EXIST);
$maninstance3->roleid = $teacherrole->id;
$DB->update_record('enrol', $maninstance3, array('id'=>$maninstance3->id));
$this->assertEquals($studentrole->id, $maninstance1->roleid);
$this->assertEquals($studentrole->id, $maninstance2->roleid);
$this->assertEquals($teacherrole->id, $maninstance3->roleid);
$result = $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
$this->assertTrue($result);
$this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance1->id, 'userid'=>$user1->id)));
$this->assertTrue($DB->record_exists('role_assignments', array('contextid'=>$context1->id, 'userid'=>$user1->id, 'roleid'=>$studentrole->id)));
$result = $this->getDataGenerator()->enrol_user($user1->id, $course2->id, $teacherrole->id, 'manual');
$this->assertTrue($result);
$this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance2->id, 'userid'=>$user1->id)));
$this->assertTrue($DB->record_exists('role_assignments', array('contextid'=>$context2->id, 'userid'=>$user1->id, 'roleid'=>$teacherrole->id)));
$result = $this->getDataGenerator()->enrol_user($user4->id, $course2->id, 'teacher', 'manual');
$this->assertTrue($result);
$this->assertTrue($DB->record_exists('user_enrolments',
array('enrolid' => $maninstance2->id, 'userid' => $user4->id)));
$this->assertTrue($DB->record_exists('role_assignments',
array('contextid' => $context2->id, 'userid' => $user4->id, 'roleid' => $teacherrole->id)));
$result = $this->getDataGenerator()->enrol_user($user1->id, $course3->id, 0, 'manual');
$this->assertTrue($result);
$this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$maninstance3->id, 'userid'=>$user1->id)));
$this->assertFalse($DB->record_exists('role_assignments', array('contextid'=>$context3->id, 'userid'=>$user1->id)));
$result = $this->getDataGenerator()->enrol_user($user2->id, $course1->id, null, 'self');
$this->assertTrue($result);
$this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user2->id)));
$this->assertTrue($DB->record_exists('role_assignments', array('contextid'=>$context1->id, 'userid'=>$user2->id, 'roleid'=>$studentrole->id)));
$selfplugin->add_instance($course2, array('status'=>ENROL_INSTANCE_ENABLED, 'roleid'=>$teacherrole->id));
$result = $this->getDataGenerator()->enrol_user($user2->id, $course2->id, null, 'self');
$this->assertFalse($result);
$DB->delete_records('enrol', array('enrol'=>'self', 'courseid'=>$course3->id));
$result = $this->getDataGenerator()->enrol_user($user2->id, $course3->id, null, 'self');
$this->assertFalse($result);
}
public function test_create_grade_category(): void {
global $DB, $CFG;
require_once $CFG->libdir . '/grade/constants.php';
$this->resetAfterTest(true);
$generator = $this->getDataGenerator();
$course = $generator->create_course();
// Generate category and make sure number of records in DB table increases.
// Note we only count grade cats with depth > 1 because the course grade category
// is lazily created.
$count = $DB->count_records_select('grade_categories', 'depth <> 1');
$gradecategory = $generator->create_grade_category(array('courseid'=>$course->id));
$this->assertEquals($count+1, $DB->count_records_select('grade_categories', 'depth <> 1'));
$this->assertEquals(2, $gradecategory->depth);
$this->assertEquals($course->id, $gradecategory->courseid);
$this->assertEquals('Grade category 1', $gradecategory->fullname);
// Generate category and make sure aggregation is set.
$gradecategory = $generator->create_grade_category(
array('courseid' => $course->id, 'aggregation' => GRADE_AGGREGATE_MEDIAN));
$this->assertEquals(GRADE_AGGREGATE_MEDIAN, $gradecategory->aggregation);
// Generate category and make sure parent is set.
$gradecategory2 = $generator->create_grade_category(
array('courseid' => $course->id,
'parent' => $gradecategory->id));
$this->assertEquals($gradecategory->id, $gradecategory2->parent);
}
public function test_create_custom_profile_field_category(): void {
global $DB;
$this->resetAfterTest();
$generator = $this->getDataGenerator();
// Insert first category without specified sortorder.
$result = $generator->create_custom_profile_field_category(['name' => 'Frogs']);
$record = $DB->get_record('user_info_category', ['name' => 'Frogs']);
$this->assertEquals(1, $record->sortorder);
// Also check the return value.
$this->assertEquals(1, $result->sortorder);
$this->assertEquals('Frogs', $result->name);
$this->assertEquals($record->id, $result->id);
// Insert next category without specified sortorder.
$generator->create_custom_profile_field_category(['name' => 'Zombies']);
$record = $DB->get_record('user_info_category', ['name' => 'Zombies']);
$this->assertEquals(2, $record->sortorder);
// Insert category with specified sortorder.
$generator->create_custom_profile_field_category(['name' => 'Toads', 'sortorder' => 9]);
$record = $DB->get_record('user_info_category', ['name' => 'Toads']);
$this->assertEquals(9, $record->sortorder);
// Insert another with unspecified sortorder.
$generator->create_custom_profile_field_category(['name' => 'Werewolves']);
$record = $DB->get_record('user_info_category', ['name' => 'Werewolves']);
$this->assertEquals(10, $record->sortorder);
}
public function test_create_custom_profile_field(): void {
global $DB;
$this->resetAfterTest();
$generator = $this->getDataGenerator();
// Insert minimal field without specified category.
$field1 = $generator->create_custom_profile_field(
['datatype' => 'text', 'shortname' => 'colour', 'name' => 'Colour']);
$record = $DB->get_record('user_info_field', ['shortname' => 'colour']);
// Check specified values.
$this->assertEquals('Colour', $record->name);
$this->assertEquals('text', $record->datatype);
// Check sortorder (first in category).
$this->assertEquals(1, $record->sortorder);
// Check shared defaults for most datatypes.
$this->assertEquals('', $record->description);
$this->assertEquals(0, $record->descriptionformat);
$this->assertEquals(0, $record->required);
$this->assertEquals(0, $record->locked);
$this->assertEquals(PROFILE_VISIBLE_ALL, $record->visible);
$this->assertEquals(0, $record->forceunique);
$this->assertEquals(0, $record->signup);
$this->assertEquals('', $record->defaultdata);
$this->assertEquals(0, $record->defaultdataformat);
// Check specific defaults for text datatype.
$this->assertEquals(30, $record->param1);
$this->assertEquals(2048, $record->param2);
// Check the returned value matches the database data.
$this->assertEquals($record, $field1);
// The category should relate to a new 'testing' category.
$catrecord = $DB->get_record('user_info_category', ['id' => $record->categoryid]);
$this->assertEquals('Testing', $catrecord->name);
$this->assertEquals(1, $catrecord->sortorder);
// Create another field, this time supplying values for a few of the fields.
$generator->create_custom_profile_field(
['datatype' => 'text', 'shortname' => 'brightness', 'name' => 'Brightness',
'required' => 1, 'forceunique' => 1]);
$record = $DB->get_record('user_info_field', ['shortname' => 'brightness']);
// Same testing category, next sortorder.
$this->assertEquals($catrecord->id, $record->categoryid);
$this->assertEquals(2, $record->sortorder);
// Check modified fields.
$this->assertEquals(1, $record->required);
$this->assertEquals(1, $record->forceunique);
// Create a field in specified category by id or name...
$category = $generator->create_custom_profile_field_category(['name' => 'Amphibians']);
$field3 = $generator->create_custom_profile_field(
['datatype' => 'text', 'shortname' => 'frog', 'name' => 'Frog',
'categoryid' => $category->id]);
$this->assertEquals($category->id, $field3->categoryid);
$this->assertEquals(1, $field3->sortorder);
$field4 = $generator->create_custom_profile_field(
['datatype' => 'text', 'shortname' => 'toad', 'name' => 'Toad',
'category' => 'Amphibians', 'sortorder' => 4]);
$this->assertEquals($category->id, $field4->categoryid);
$this->assertEquals(4, $field4->sortorder);
// Check defaults for menu, datetime, and checkbox.
$field5 = $generator->create_custom_profile_field(
['datatype' => 'menu', 'shortname' => 'cuisine', 'name' => 'Cuisine']);
$this->assertEquals("Yes\nNo", $field5->param1);
$this->assertEquals('No', $field5->defaultdata);
$field6 = $generator->create_custom_profile_field(
['datatype' => 'datetime', 'shortname' => 'epoch', 'name' => 'Epoch']);
$this->assertEquals(2010, $field6->param1);
$this->assertEquals(2015, $field6->param2);
$this->assertEquals(1, $field6->param3);
$field7 = $generator->create_custom_profile_field(
['datatype' => 'checkbox', 'shortname' => 'areyousure', 'name' => 'Are you sure?']);
$this->assertEquals(0, $field7->defaultdata);
// Check setting options for menu using \n work as expected.
$field8 = $generator->create_custom_profile_field([
'datatype' => 'menu', 'shortname' => 'cuisine', 'name' => 'Cuisine', 'param1' => 'French\nChinese\nLebanese',
'defaultdata' => 'Chinese'
]);
$this->assertEquals("French\nChinese\nLebanese", $field8->param1);
}
}
+80
View File
@@ -0,0 +1,80 @@
<?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 core\testing;
/**
* Testing util tests.
*
* @package core
* @category phpunit
* @copyright 2023 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class util_test extends \advanced_testcase {
/**
* Note: This test is required for the other two parts because the first time
* a table is written to it may not have had the initial value reset.
*
* @coversNothing
*/
public function test_increment_reset_part_one(): void {
global $DB;
switch ($DB->get_dbfamily()) {
case 'mssql':
$this->markTestSkipped('MSSQL does not support sequences');
return;
}
$this->resetAfterTest();
$DB->insert_record('config_plugins', [
'plugin' => 'example',
'name' => 'test_increment',
'value' => 0,
]);
}
/**
* @coversNothing
* @depends test_increment_reset_part_one
*/
public function test_increment_reset_part_two(): int {
global $DB;
$this->resetAfterTest();
return $DB->insert_record('config_plugins', [
'plugin' => 'example',
'name' => 'test_increment',
'value' => 0,
]);
}
/**
* @depends test_increment_reset_part_two
*/
public function test_increment_reset_part_three(int $previousid): void {
global $DB;
$this->resetAfterTest();
$id = $DB->insert_record('config_plugins', [
'plugin' => 'example',
'name' => 'test_increment',
'value' => 0,
]);
$this->assertEquals($previousid, $id);
}
}