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
+866
View File
@@ -0,0 +1,866 @@
<?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/>.
use core\di;
use core\hook;
/**
* Advanced PHPUnit test case customised for Moodle.
*
* @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
*/
abstract class advanced_testcase extends base_testcase {
/** @var bool automatically reset everything? null means log changes */
// phpcs:ignore moodle.NamingConventions.ValidVariableName.MemberNameUnderscore
private $resetAfterTest;
/** @var moodle_transaction */
private $testdbtransaction;
/** @var int timestamp used for current time asserts */
private $currenttimestart;
/**
* Constructs a test case with the given name.
*
* Note: use setUp() or setUpBeforeClass() in your test cases.
*
* @param string $name
* @param array $data
* @param string $dataName
*/
final public function __construct($name = null, array $data = [], $dataname = '') {
parent::__construct($name, $data, $dataname);
$this->setBackupGlobals(false);
$this->setBackupStaticAttributes(false);
$this->setPreserveGlobalState(false);
}
/**
* Runs the bare test sequence.
*/
final public function runBare(): void {
global $CFG, $DB;
if (phpunit_util::$lastdbwrites != $DB->perf_get_writes()) {
// This happens when previous test does not reset, we can not use transactions.
$this->testdbtransaction = null;
} else if ($DB->get_dbfamily() === 'postgres' || $DB->get_dbfamily() === 'mssql') {
// Database must allow rollback of DDL, so no mysql here.
$this->testdbtransaction = $DB->start_delegated_transaction();
}
try {
$this->setCurrentTimeStart();
parent::runBare();
} catch (Exception $ex) {
$e = $ex;
} catch (Throwable $ex) {
// Engine errors in PHP7 throw exceptions of type Throwable (this "catch" will be ignored in PHP5).
$e = $ex;
} finally {
// Reset global state after test and test failure.
$CFG = phpunit_util::get_global_backup('CFG');
$DB = phpunit_util::get_global_backup('DB');
// We need to reset the autoloader.
\core_component::reset();
}
if (isset($e)) {
// Cleanup after failed expectation.
self::resetAllData();
throw $e;
}
// Deal with any debugging messages.
$debugerror = phpunit_util::display_debugging_messages(true);
$this->resetDebugging();
if (!empty($debugerror)) {
trigger_error('Unexpected debugging() call detected.' . "\n" . $debugerror, E_USER_NOTICE);
}
if (!$this->testdbtransaction || $this->testdbtransaction->is_disposed()) {
$this->testdbtransaction = null;
}
if ($this->resetAfterTest === true) {
if ($this->testdbtransaction) {
$DB->force_transaction_rollback();
phpunit_util::reset_all_database_sequences();
phpunit_util::$lastdbwrites = $DB->perf_get_writes(); // No db reset necessary.
}
self::resetAllData(null);
} else if ($this->resetAfterTest === false) {
if ($this->testdbtransaction) {
$this->testdbtransaction->allow_commit();
}
// Keep all data untouched for other tests.
} else {
// Reset but log what changed.
if ($this->testdbtransaction) {
try {
$this->testdbtransaction->allow_commit();
} catch (dml_transaction_exception $e) {
self::resetAllData();
throw new coding_exception('Invalid transaction state detected in test ' . $this->getName());
}
}
self::resetAllData(true);
}
// Reset context cache.
context_helper::reset_caches();
// Make sure test did not forget to close transaction.
if ($DB->is_transaction_started()) {
self::resetAllData();
if (
$this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_PASSED
|| $this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_SKIPPED
|| $this->getStatus() == PHPUnit\Runner\BaseTestRunner::STATUS_INCOMPLETE
) {
throw new coding_exception('Test ' . $this->getName() . ' did not close database transaction');
}
}
}
/**
* @deprecated since Moodle 3.10 - See MDL-67673 and MDL-64600 for more info.
*/
protected function createXMLDataSet() {
throw new coding_exception(__FUNCTION__ . '() is deprecated. Please use dataset_from_files() instead.');
}
/**
* @deprecated since Moodle 3.10 - See MDL-67673 and MDL-64600 for more info.
*/
protected function createCsvDataSet() {
throw new coding_exception(__FUNCTION__ . '() is deprecated. Please use dataset_from_files() instead.');
}
/**
* @deprecated since Moodle 3.10 - See MDL-67673 and MDL-64600 for more info.
*/
protected function createArrayDataSet() {
throw new coding_exception(__FUNCTION__ . '() is deprecated. Please use dataset_from_array() instead.');
}
/**
* @deprecated since Moodle 3.10 - See MDL-67673 and MDL-64600 for more info.
*/
protected function loadDataSet() {
throw new coding_exception(__FUNCTION__ . '() is deprecated. Please use dataset->to_database() instead.');
}
/**
* Creates a new dataset from CVS/XML files.
*
* This method accepts an array of full paths to CSV or XML files to be loaded
* into the dataset. For CSV files, the name of the table which the file belongs
* to needs to be specified. Example:
*
* $fullpaths = [
* '/path/to/users.xml',
* 'course' => '/path/to/courses.csv',
* ];
*
* @since Moodle 3.10
*
* @param array $files full paths to CSV or XML files to load.
* @return phpunit_dataset
*/
protected function dataset_from_files(array $files) {
// We ignore $delimiter, $enclosure and $escape, use the default ones in your fixtures.
$dataset = new phpunit_dataset();
$dataset->from_files($files);
return $dataset;
}
/**
* Creates a new dataset from string (CSV or XML).
*
* @since Moodle 3.10
*
* @param string $content contents (CSV or XML) to load.
* @param string $type format of the content to be loaded (csv or xml).
* @param string $table name of the table which the file belongs to (only for CSV files).
* @return phpunit_dataset
*/
protected function dataset_from_string(string $content, string $type, ?string $table = null) {
$dataset = new phpunit_dataset();
$dataset->from_string($content, $type, $table);
return $dataset;
}
/**
* Creates a new dataset from PHP array.
*
* @since Moodle 3.10
*
* @param array $data array of tables, see {@see phpunit_dataset::from_array()} for supported formats.
* @return phpunit_dataset
*/
protected function dataset_from_array(array $data) {
$dataset = new phpunit_dataset();
$dataset->from_array($data);
return $dataset;
}
/**
* Call this method from test if you want to make sure that
* the resetting of database is done the slow way without transaction
* rollback.
*
* This is useful especially when testing stuff that is not compatible with transactions.
*
* @return void
*/
public function preventResetByRollback() {
if ($this->testdbtransaction && !$this->testdbtransaction->is_disposed()) {
$this->testdbtransaction->allow_commit();
$this->testdbtransaction = null;
}
}
/**
* Reset everything after current test.
* @param bool $reset true means reset state back, false means keep all data for the next test,
* null means reset state and show warnings if anything changed
* @return void
*/
public function resetAfterTest($reset = true) {
$this->resetAfterTest = $reset;
}
/**
* Return debugging messages from the current test.
* @return array with instances having 'message', 'level' and 'stacktrace' property.
*/
public function getDebuggingMessages() {
return phpunit_util::get_debugging_messages();
}
/**
* Clear all previous debugging messages in current test
* and revert to default DEVELOPER_DEBUG level.
*/
public function resetDebugging() {
phpunit_util::reset_debugging();
}
/**
* Assert that exactly debugging was just called once.
*
* Discards the debugging message if successful.
*
* @param null|string $debugmessage null means any
* @param null|string $debuglevel null means any
* @param string $message
*/
public function assertDebuggingCalled($debugmessage = null, $debuglevel = null, $message = '') {
$debugging = $this->getDebuggingMessages();
$debugdisplaymessage = "\n" . phpunit_util::display_debugging_messages(true);
$this->resetDebugging();
$count = count($debugging);
if ($count == 0) {
if ($message === '') {
$message = 'Expectation failed, debugging() not triggered.';
}
$this->fail($message);
}
if ($count > 1) {
if ($message === '') {
$message = 'Expectation failed, debugging() triggered ' . $count . ' times.' . $debugdisplaymessage;
}
$this->fail($message);
}
$this->assertEquals(1, $count);
$message .= $debugdisplaymessage;
$debug = reset($debugging);
if ($debugmessage !== null) {
$this->assertSame($debugmessage, $debug->message, $message);
}
if ($debuglevel !== null) {
$this->assertSame($debuglevel, $debug->level, $message);
}
}
/**
* Asserts how many times debugging has been called.
*
* @param int $expectedcount The expected number of times
* @param array $debugmessages Expected debugging messages, one for each expected message.
* @param array $debuglevels Expected debugging levels, one for each expected message.
* @param string $message
* @return void
*/
public function assertdebuggingcalledcount($expectedcount, $debugmessages = [], $debuglevels = [], $message = '') {
if (!is_int($expectedcount)) {
throw new coding_exception('assertDebuggingCalledCount $expectedcount argument should be an integer.');
}
$debugging = $this->getDebuggingMessages();
$message .= "\n" . phpunit_util::display_debugging_messages(true);
$this->resetDebugging();
$this->assertEquals($expectedcount, count($debugging), $message);
if ($debugmessages) {
if (!is_array($debugmessages) || count($debugmessages) != $expectedcount) {
throw new coding_exception(
'assertDebuggingCalledCount $debugmessages should contain ' . $expectedcount . ' messages',
);
}
foreach ($debugmessages as $key => $debugmessage) {
$this->assertSame($debugmessage, $debugging[$key]->message, $message);
}
}
if ($debuglevels) {
if (!is_array($debuglevels) || count($debuglevels) != $expectedcount) {
throw new coding_exception(
'assertDebuggingCalledCount $debuglevels should contain ' . $expectedcount . ' messages',
);
}
foreach ($debuglevels as $key => $debuglevel) {
$this->assertSame($debuglevel, $debugging[$key]->level, $message);
}
}
}
/**
* Call when no debugging() messages expected.
* @param string $message
*/
public function assertDebuggingNotCalled($message = '') {
$debugging = $this->getDebuggingMessages();
$count = count($debugging);
if ($message === '') {
$message = 'Expectation failed, debugging() was triggered.';
}
$message .= "\n".phpunit_util::display_debugging_messages(true);
$this->resetDebugging();
$this->assertEquals(0, $count, $message);
}
/**
* Assert that an event legacy data is equal to the expected value.
*
* @param mixed $expected expected data.
* @param \core\event\base $event the event object.
* @param string $message
* @return void
*/
public function assertEventLegacyData($expected, \core\event\base $event, $message = '') {
$legacydata = phpunit_event_mock::testable_get_legacy_eventdata($event);
if ($message === '') {
$message = 'Event legacy data does not match expected value.';
}
$this->assertEquals($expected, $legacydata, $message);
}
/**
* Assert that an event legacy log data is equal to the expected value.
*
* @param mixed $expected expected data.
* @param \core\event\base $event the event object.
* @param string $message
* @return void
*/
public function assertEventLegacyLogData($expected, \core\event\base $event, $message = '') {
$legacydata = phpunit_event_mock::testable_get_legacy_logdata($event);
if ($message === '') {
$message = 'Event legacy log data does not match expected value.';
}
$this->assertEquals($expected, $legacydata, $message);
}
/**
* Assert that various event methods are not using event->context
*
* While restoring context might not be valid and it should not be used by event url
* or description methods.
*
* @param \core\event\base $event the event object.
* @param string $message
* @return void
*/
public function assertEventContextNotUsed(\core\event\base $event, $message = '') {
// Save current event->context and set it to false.
$eventcontext = phpunit_event_mock::testable_get_event_context($event);
phpunit_event_mock::testable_set_event_context($event, false);
if ($message === '') {
$message = 'Event should not use context property of event in any method.';
}
// Test event methods should not use event->context.
$event->get_url();
$event->get_description();
// Restore event->context (note that this is unreachable when the event uses context). But ok for correct events.
phpunit_event_mock::testable_set_event_context($event, $eventcontext);
}
/**
* Stores current time as the base for assertTimeCurrent().
*
* Note: this is called automatically before calling individual test methods.
* @return int current time
*/
public function setCurrentTimeStart() {
$this->currenttimestart = time();
return $this->currenttimestart;
}
/**
* Assert that: start < $time < time()
* @param int $time
* @param string $message
* @return void
*/
public function assertTimeCurrent($time, $message = '') {
$msg = ($message === '') ? 'Time is lower that allowed start value' : $message;
$this->assertGreaterThanOrEqual($this->currenttimestart, $time, $msg);
$msg = ($message === '') ? 'Time is in the future' : $message;
$this->assertLessThanOrEqual(time(), $time, $msg);
}
/**
* Starts message redirection.
*
* You can verify if messages were sent or not by inspecting the messages
* array in the returned messaging sink instance. The redirection
* can be stopped by calling $sink->close();
*
* @return phpunit_message_sink
*/
public function redirectMessages() {
return phpunit_util::start_message_redirection();
}
/**
* Starts email redirection.
*
* You can verify if email were sent or not by inspecting the email
* array in the returned phpmailer sink instance. The redirection
* can be stopped by calling $sink->close();
*
* @return phpunit_message_sink
*/
public function redirectEmails() {
return phpunit_util::start_phpmailer_redirection();
}
/**
* Starts event redirection.
*
* You can verify if events were triggered or not by inspecting the events
* array in the returned event sink instance. The redirection
* can be stopped by calling $sink->close();
*
* @return phpunit_event_sink
*/
public function redirectEvents() {
return phpunit_util::start_event_redirection();
}
/**
* Override hook callbacks.
*
* @param string $hookname
* @param callable $callback
* @return void
*/
public function redirectHook(string $hookname, callable $callback): void {
di::get(hook\manager::class)->phpunit_redirect_hook($hookname, $callback);
}
/**
* Remove all hook overrides.
*
* @return void
*/
public function stopHookRedirections(): void {
di::get(hook\manager::class)->phpunit_stop_redirections();
}
/**
* Reset all database tables, restore global state and clear caches and optionally purge dataroot dir.
*
* @param bool $detectchanges
* true - changes in global state and database are reported as errors
* false - no errors reported
* null - only critical problems are reported as errors
* @return void
*/
public static function resetAllData($detectchanges = false) {
phpunit_util::reset_all_data($detectchanges);
}
/**
* Set current $USER, reset access cache.
* @static
* @param null|int|stdClass $user user record, null or 0 means non-logged-in, positive integer means userid
* @return void
*/
public static function setUser($user = null) {
global $CFG, $DB;
if (is_object($user)) {
$user = clone($user);
} else if (!$user) {
$user = new stdClass();
$user->id = 0;
$user->mnethostid = $CFG->mnet_localhost_id;
} else {
$user = $DB->get_record('user', ['id' => $user]);
}
unset($user->description);
unset($user->access);
unset($user->preference);
// Enusre session is empty, as it may contain caches and user specific info.
\core\session\manager::init_empty_session();
\core\session\manager::set_user($user);
}
/**
* Set current $USER to admin account, reset access cache.
* @static
* @return void
*/
public static function setAdminUser() {
self::setUser(2);
}
/**
* Set current $USER to guest account, reset access cache.
* @static
* @return void
*/
public static function setGuestUser() {
self::setUser(1);
}
/**
* Change server and default php timezones.
*
* @param string $servertimezone timezone to set in $CFG->timezone (not validated)
* @param string $defaultphptimezone timezone to fake default php timezone (must be valid)
*/
public static function setTimezone($servertimezone = 'Australia/Perth', $defaultphptimezone = 'Australia/Perth') {
global $CFG;
$CFG->timezone = $servertimezone;
core_date::phpunit_override_default_php_timezone($defaultphptimezone);
core_date::set_default_server_timezone();
}
/**
* Get data generator
* @static
* @return testing_data_generator
*/
public static function getDataGenerator() {
return phpunit_util::get_data_generator();
}
/**
* Returns UTL of the external test file.
*
* The result depends on the value of following constants:
* - TEST_EXTERNAL_FILES_HTTP_URL
* - TEST_EXTERNAL_FILES_HTTPS_URL
*
* They should point to standard external test files repository,
* it defaults to 'http://download.moodle.org/unittest'.
*
* False value means skip tests that require external files.
*
* @param string $path
* @param bool $https true if https required
* @return string url
*/
public function getExternalTestFileUrl($path, $https = false) {
$path = ltrim($path, '/');
if ($path) {
$path = '/' . $path;
}
if ($https) {
if (defined('TEST_EXTERNAL_FILES_HTTPS_URL')) {
if (!TEST_EXTERNAL_FILES_HTTPS_URL) {
$this->markTestSkipped('Tests using external https test files are disabled');
}
return TEST_EXTERNAL_FILES_HTTPS_URL . $path;
}
return 'https://download.moodle.org/unittest' . $path;
}
if (defined('TEST_EXTERNAL_FILES_HTTP_URL')) {
if (!TEST_EXTERNAL_FILES_HTTP_URL) {
$this->markTestSkipped('Tests using external http test files are disabled');
}
return TEST_EXTERNAL_FILES_HTTP_URL . $path;
}
return 'http://download.moodle.org/unittest' . $path;
}
/**
* Recursively visit all the files in the source tree. Calls the callback
* function with the pathname of each file found.
*
* @param string $path the folder to start searching from.
* @param string $callback the method of this class to call with the name of each file found.
* @param string $fileregexp a regexp used to filter the search (optional).
* @param bool $exclude If true, pathnames that match the regexp will be ignored. If false,
* only files that match the regexp will be included. (default false).
* @param array $ignorefolders will not go into any of these folders (optional).
* @return void
*/
public function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
$files = scandir($path);
foreach ($files as $file) {
$filepath = $path . '/' . $file;
if (strpos($file, '.') === 0) {
// Don't check hidden files.
continue;
} else if (is_dir($filepath)) {
if (!in_array($filepath, $ignorefolders)) {
$this->recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
}
} else if ($exclude xor preg_match($fileregexp, $filepath)) {
$this->$callback($filepath);
}
}
}
/**
* Wait for a second to roll over, ensures future calls to time() return a different result.
*
* This is implemented instead of sleep() as we do not need to wait a full second. In some cases
* due to calls we may wait more than sleep() would have, on average it will be less.
*/
public function waitForSecond() {
$starttime = time();
while (time() == $starttime) {
usleep(50000);
}
}
/**
* Run adhoc tasks, optionally matching the specified classname.
*
* @param string $matchclass The name of the class to match on.
* @param int $matchuserid The userid to match.
*/
protected function runAdhocTasks($matchclass = '', $matchuserid = null) {
global $DB;
$params = [];
if (!empty($matchclass)) {
if (strpos($matchclass, '\\') !== 0) {
$matchclass = '\\' . $matchclass;
}
$params['classname'] = $matchclass;
}
if (!empty($matchuserid)) {
$params['userid'] = $matchuserid;
}
$lock = $this->createMock(\core\lock\lock::class);
$cronlock = $this->createMock(\core\lock\lock::class);
$tasks = $DB->get_recordset('task_adhoc', $params);
foreach ($tasks as $record) {
// Note: This is for cron only.
// We do not lock the tasks.
$task = \core\task\manager::adhoc_task_from_record($record);
$user = null;
if ($userid = $task->get_userid()) {
// This task has a userid specified.
$user = \core_user::get_user($userid);
// User found. Check that they are suitable.
\core_user::require_active_user($user, true, true);
}
$task->set_lock($lock);
$cronlock->release();
\core\cron::prepare_core_renderer();
\core\cron::setup_user($user);
$task->execute();
\core\task\manager::adhoc_task_complete($task);
unset($task);
}
$tasks->close();
}
/**
* Run adhoc tasks.
*/
protected function run_all_adhoc_tasks(): void {
// Run the adhoc task.
while ($task = \core\task\manager::get_next_adhoc_task(time())) {
$task->execute();
\core\task\manager::adhoc_task_complete($task);
}
}
/**
* Mock the clock with an incrementing clock.
*
* @param null|int $starttime
* @return \incrementing_clock
*/
public function mock_clock_with_incrementing(
?int $starttime = null,
): \incrementing_clock {
require_once(dirname(__DIR__, 2) . '/testing/classes/incrementing_clock.php');
$clock = new \incrementing_clock($starttime);
\core\di::set(\core\clock::class, $clock);
return $clock;
}
/**
* Mock the clock with a frozen clock.
*
* @param null|int $time
* @return \frozen_clock
*/
public function mock_clock_with_frozen(
?int $time = null,
): \frozen_clock {
require_once(dirname(__DIR__, 2) . '/testing/classes/frozen_clock.php');
$clock = new \frozen_clock($time);
\core\di::set(\core\clock::class, $clock);
return $clock;
}
/**
* Add a mocked plugintype to Moodle.
*
* A new plugintype name must be provided with a path to the plugintype's root.
*
* Please note that tests calling this method must be run in separate isolation mode.
* Please avoid using this if at all possible.
*
* @param string $plugintype The name of the plugintype
* @param string $path The path to the plugintype's root
*/
protected function add_mocked_plugintype(
string $plugintype,
string $path,
): void {
require_phpunit_isolation();
$mockedcomponent = new \ReflectionClass(\core_component::class);
$plugintypes = $mockedcomponent->getStaticPropertyValue('plugintypes');
if (array_key_exists($plugintype, $plugintypes)) {
throw new \coding_exception("The plugintype '{$plugintype}' already exists.");
}
$plugintypes[$plugintype] = $path;
$mockedcomponent->setStaticPropertyValue('plugintypes', $plugintypes);
$this->resetDebugging();
}
/**
* Add a mocked plugin to Moodle.
*
* A new plugin name must be provided with a path to the plugin's root.
* The plugin type must already exist (or have been mocked separately).
*
* Please note that tests calling this method must be run in separate isolation mode.
* Please avoid using this if at all possible.
*
* @param string $plugintype The name of the plugintype
* @param string $pluginname The name of the plugin
* @param string $path The path to the plugin's root
*/
protected function add_mocked_plugin(
string $plugintype,
string $pluginname,
string $path,
): void {
require_phpunit_isolation();
$mockedcomponent = new \ReflectionClass(\core_component::class);
$plugins = $mockedcomponent->getStaticPropertyValue('plugins');
if (!array_key_exists($plugintype, $plugins)) {
$plugins[$plugintype] = [];
}
$plugins[$plugintype][$pluginname] = $path;
$mockedcomponent->setStaticPropertyValue('plugins', $plugins);
$this->resetDebugging();
}
/**
* Convenience method to load a fixture from a component's fixture directory.
*
* @param string $component
* @param string $path
* @throws coding_exception
*/
protected static function load_fixture(
string $component,
string $path,
): void {
$fullpath = sprintf(
"%s/tests/fixtures/%s",
\core_component::get_component_directory($component),
$path,
);
if (!file_exists($fullpath)) {
throw new \coding_exception("Fixture file not found: $fullpath");
}
global $ADMIN;
global $CFG;
global $DB;
global $SITE;
global $USER;
global $OUTPUT;
global $PAGE;
global $SESSION;
global $COURSE;
global $SITE;
require_once($fullpath);
}
}
+598
View File
@@ -0,0 +1,598 @@
<?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/>.
/**
* Base test case class.
*
* @package core
* @category test
* @author Tony Levi <tony.levi@blackboard.com>
* @copyright 2015 Blackboard (http://www.blackboard.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Base class for PHPUnit test cases customised for Moodle
*
* It is intended for functionality common to both basic and advanced_testcase.
*
* @package core
* @category test
* @author Tony Levi <tony.levi@blackboard.com>
* @copyright 2015 Blackboard (http://www.blackboard.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class base_testcase extends PHPUnit\Framework\TestCase {
// phpcs:disable
// Following code is legacy code from phpunit to support assertTag
// and assertNotTag.
/**
* Note: we are overriding this method to remove the deprecated error
* @see https://tracker.moodle.org/browse/MDL-47129
*
* @param array $matcher
* @param string $actual
* @param string $message
* @param boolean $ishtml
*
* @deprecated 3.0
*/
public static function assertTag($matcher, $actual, $message = '', $ishtml = true) {
$dom = (new PHPUnit\Util\Xml\Loader)->load($actual, $ishtml);
$tags = self::findNodes($dom, $matcher, $ishtml);
$matched = (is_array($tags) && count($tags) > 0) && $tags[0] instanceof DOMNode;
self::assertTrue($matched, $message);
}
/**
* Note: we are overriding this method to remove the deprecated error
* @see https://tracker.moodle.org/browse/MDL-47129
*
* @param array $matcher
* @param string $actual
* @param string $message
* @param boolean $ishtml
*
* @deprecated 3.0
*/
public static function assertNotTag($matcher, $actual, $message = '', $ishtml = true) {
$dom = (new PHPUnit\Util\Xml\Loader)->load($actual, $ishtml);
$tags = self::findNodes($dom, $matcher, $ishtml);
$matched = (is_array($tags) && count($tags) > 0) && $tags[0] instanceof DOMNode;
self::assertFalse($matched, $message);
}
/**
* Validate list of keys in the associative array.
*
* @param array $hash
* @param array $validKeys
*
* @return array
*
* @throws PHPUnit\Framework\Exception
*/
public static function assertValidKeys(array $hash, array $validKeys) {
$valids = array();
// Normalize validation keys so that we can use both indexed and
// associative arrays.
foreach ($validKeys as $key => $val) {
is_int($key) ? $valids[$val] = null : $valids[$key] = $val;
}
$validKeys = array_keys($valids);
// Check for invalid keys.
foreach ($hash as $key => $value) {
if (!in_array($key, $validKeys)) {
$unknown[] = $key;
}
}
if (!empty($unknown)) {
throw new PHPUnit\Framework\Exception(
'Unknown key(s): ' . implode(', ', $unknown)
);
}
// Add default values for any valid keys that are empty.
foreach ($valids as $key => $value) {
if (!isset($hash[$key])) {
$hash[$key] = $value;
}
}
return $hash;
}
/**
* Assert that two Date/Time strings are equal.
*
* The strings generated by \DateTime, \strtotime, \date, \time, etc. are generated outside of our control.
* From time-to-time string changes are made.
* One such example is from ICU 72.1 which changed the time format to include a narrow-non-breaking-space (U+202F)
* between the time and AM/PM.
*
* We should not update our tests to match these changes, as it is not our code that is
* generating the strings and they may change again.
* In addition, the changes are not equal amongst all systems as they depend on the version of ICU installed.
*
* @param string $expected
* @param string $actual
* @param string $message
*/
public function assertEqualsIgnoringWhitespace($expected, $actual, string $message = ''): void {
// ICU 72.1 introduced the use of a narrow-non-breaking-space (U+202F) between the time and the AM/PM.
// Normalise all whitespace when performing the comparison.
$expected = preg_replace('/\s+/u', ' ', $expected);
$actual = preg_replace('/\s+/u', ' ', $actual);
$this->assertEquals($expected, $actual, $message);
}
/**
* Parse out the options from the tag using DOM object tree.
*
* @param DOMDocument $dom
* @param array $options
* @param bool $isHtml
*
* @return array
*/
public static function findNodes(DOMDocument $dom, array $options, $isHtml = true) {
$valid = array(
'id', 'class', 'tag', 'content', 'attributes', 'parent',
'child', 'ancestor', 'descendant', 'children', 'adjacent-sibling'
);
$filtered = array();
$options = self::assertValidKeys($options, $valid);
// find the element by id
if ($options['id']) {
$options['attributes']['id'] = $options['id'];
}
if ($options['class']) {
$options['attributes']['class'] = $options['class'];
}
$nodes = array();
// find the element by a tag type
if ($options['tag']) {
if ($isHtml) {
$elements = self::getElementsByCaseInsensitiveTagName(
$dom,
$options['tag']
);
} else {
$elements = $dom->getElementsByTagName($options['tag']);
}
foreach ($elements as $element) {
$nodes[] = $element;
}
if (empty($nodes)) {
return false;
}
} // no tag selected, get them all
else {
$tags = array(
'a', 'abbr', 'acronym', 'address', 'area', 'b', 'base', 'bdo',
'big', 'blockquote', 'body', 'br', 'button', 'caption', 'cite',
'code', 'col', 'colgroup', 'dd', 'del', 'div', 'dfn', 'dl',
'dt', 'em', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2',
'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe',
'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link',
'map', 'meta', 'noframes', 'noscript', 'object', 'ol', 'optgroup',
'option', 'p', 'param', 'pre', 'q', 'samp', 'script', 'select',
'small', 'span', 'strong', 'style', 'sub', 'sup', 'table',
'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title',
'tr', 'tt', 'ul', 'var',
// HTML5
'article', 'aside', 'audio', 'bdi', 'canvas', 'command',
'datalist', 'details', 'dialog', 'embed', 'figure', 'figcaption',
'footer', 'header', 'hgroup', 'keygen', 'mark', 'meter', 'nav',
'output', 'progress', 'ruby', 'rt', 'rp', 'track', 'section',
'source', 'summary', 'time', 'video', 'wbr'
);
foreach ($tags as $tag) {
if ($isHtml) {
$elements = self::getElementsByCaseInsensitiveTagName(
$dom,
$tag
);
} else {
$elements = $dom->getElementsByTagName($tag);
}
foreach ($elements as $element) {
$nodes[] = $element;
}
}
if (empty($nodes)) {
return false;
}
}
// filter by attributes
if ($options['attributes']) {
foreach ($nodes as $node) {
$invalid = false;
foreach ($options['attributes'] as $name => $value) {
// match by regexp if like "regexp:/foo/i"
if (preg_match('/^regexp\s*:\s*(.*)/i', $value, $matches)) {
if (!preg_match($matches[1], $node->getAttribute($name))) {
$invalid = true;
}
} // class can match only a part
elseif ($name == 'class') {
// split to individual classes
$findClasses = explode(
' ',
preg_replace("/\s+/", ' ', $value)
);
$allClasses = explode(
' ',
preg_replace("/\s+/", ' ', $node->getAttribute($name))
);
// make sure each class given is in the actual node
foreach ($findClasses as $findClass) {
if (!in_array($findClass, $allClasses)) {
$invalid = true;
}
}
} // match by exact string
else {
if ($node->getAttribute($name) !== (string) $value) {
$invalid = true;
}
}
}
// if every attribute given matched
if (!$invalid) {
$filtered[] = $node;
}
}
$nodes = $filtered;
$filtered = array();
if (empty($nodes)) {
return false;
}
}
// filter by content
if ($options['content'] !== null) {
foreach ($nodes as $node) {
$invalid = false;
// match by regexp if like "regexp:/foo/i"
if (preg_match('/^regexp\s*:\s*(.*)/i', $options['content'], $matches)) {
if (!preg_match($matches[1], self::getNodeText($node))) {
$invalid = true;
}
} // match empty string
elseif ($options['content'] === '') {
if (self::getNodeText($node) !== '') {
$invalid = true;
}
} // match by exact string
elseif (strstr(self::getNodeText($node), $options['content']) === false) {
$invalid = true;
}
if (!$invalid) {
$filtered[] = $node;
}
}
$nodes = $filtered;
$filtered = array();
if (empty($nodes)) {
return false;
}
}
// filter by parent node
if ($options['parent']) {
$parentNodes = self::findNodes($dom, $options['parent'], $isHtml);
$parentNode = isset($parentNodes[0]) ? $parentNodes[0] : null;
foreach ($nodes as $node) {
if ($parentNode !== $node->parentNode) {
continue;
}
$filtered[] = $node;
}
$nodes = $filtered;
$filtered = array();
if (empty($nodes)) {
return false;
}
}
// filter by child node
if ($options['child']) {
$childNodes = self::findNodes($dom, $options['child'], $isHtml);
$childNodes = !empty($childNodes) ? $childNodes : array();
foreach ($nodes as $node) {
foreach ($node->childNodes as $child) {
foreach ($childNodes as $childNode) {
if ($childNode === $child) {
$filtered[] = $node;
}
}
}
}
$nodes = $filtered;
$filtered = array();
if (empty($nodes)) {
return false;
}
}
// filter by adjacent-sibling
if ($options['adjacent-sibling']) {
$adjacentSiblingNodes = self::findNodes($dom, $options['adjacent-sibling'], $isHtml);
$adjacentSiblingNodes = !empty($adjacentSiblingNodes) ? $adjacentSiblingNodes : array();
foreach ($nodes as $node) {
$sibling = $node;
while ($sibling = $sibling->nextSibling) {
if ($sibling->nodeType !== XML_ELEMENT_NODE) {
continue;
}
foreach ($adjacentSiblingNodes as $adjacentSiblingNode) {
if ($sibling === $adjacentSiblingNode) {
$filtered[] = $node;
break;
}
}
break;
}
}
$nodes = $filtered;
$filtered = array();
if (empty($nodes)) {
return false;
}
}
// filter by ancestor
if ($options['ancestor']) {
$ancestorNodes = self::findNodes($dom, $options['ancestor'], $isHtml);
$ancestorNode = isset($ancestorNodes[0]) ? $ancestorNodes[0] : null;
foreach ($nodes as $node) {
$parent = $node->parentNode;
while ($parent && $parent->nodeType != XML_HTML_DOCUMENT_NODE) {
if ($parent === $ancestorNode) {
$filtered[] = $node;
}
$parent = $parent->parentNode;
}
}
$nodes = $filtered;
$filtered = array();
if (empty($nodes)) {
return false;
}
}
// filter by descendant
if ($options['descendant']) {
$descendantNodes = self::findNodes($dom, $options['descendant'], $isHtml);
$descendantNodes = !empty($descendantNodes) ? $descendantNodes : array();
foreach ($nodes as $node) {
foreach (self::getDescendants($node) as $descendant) {
foreach ($descendantNodes as $descendantNode) {
if ($descendantNode === $descendant) {
$filtered[] = $node;
}
}
}
}
$nodes = $filtered;
$filtered = array();
if (empty($nodes)) {
return false;
}
}
// filter by children
if ($options['children']) {
$validChild = array('count', 'greater_than', 'less_than', 'only');
$childOptions = self::assertValidKeys(
$options['children'],
$validChild
);
foreach ($nodes as $node) {
$childNodes = $node->childNodes;
foreach ($childNodes as $childNode) {
if ($childNode->nodeType !== XML_CDATA_SECTION_NODE &&
$childNode->nodeType !== XML_TEXT_NODE) {
$children[] = $childNode;
}
}
// we must have children to pass this filter
if (!empty($children)) {
// exact count of children
if ($childOptions['count'] !== null) {
if (count($children) !== $childOptions['count']) {
break;
}
} // range count of children
elseif ($childOptions['less_than'] !== null &&
$childOptions['greater_than'] !== null) {
if (count($children) >= $childOptions['less_than'] ||
count($children) <= $childOptions['greater_than']) {
break;
}
} // less than a given count
elseif ($childOptions['less_than'] !== null) {
if (count($children) >= $childOptions['less_than']) {
break;
}
} // more than a given count
elseif ($childOptions['greater_than'] !== null) {
if (count($children) <= $childOptions['greater_than']) {
break;
}
}
// match each child against a specific tag
if ($childOptions['only']) {
$onlyNodes = self::findNodes(
$dom,
$childOptions['only'],
$isHtml
);
// try to match each child to one of the 'only' nodes
foreach ($children as $child) {
$matched = false;
foreach ($onlyNodes as $onlyNode) {
if ($onlyNode === $child) {
$matched = true;
}
}
if (!$matched) {
break 2;
}
}
}
$filtered[] = $node;
}
}
$nodes = $filtered;
if (empty($nodes)) {
return;
}
}
// return the first node that matches all criteria
return !empty($nodes) ? $nodes : array();
}
/**
* Recursively get flat array of all descendants of this node.
*
* @param DOMNode $node
*
* @return array
*/
protected static function getDescendants(DOMNode $node) {
$allChildren = array();
$childNodes = $node->childNodes ? $node->childNodes : array();
foreach ($childNodes as $child) {
if ($child->nodeType === XML_CDATA_SECTION_NODE ||
$child->nodeType === XML_TEXT_NODE) {
continue;
}
$children = self::getDescendants($child);
$allChildren = array_merge($allChildren, $children, array($child));
}
return isset($allChildren) ? $allChildren : array();
}
/**
* Gets elements by case insensitive tagname.
*
* @param DOMDocument $dom
* @param string $tag
*
* @return DOMNodeList
*/
protected static function getElementsByCaseInsensitiveTagName(DOMDocument $dom, $tag) {
$elements = $dom->getElementsByTagName(strtolower($tag));
if ($elements->length == 0) {
$elements = $dom->getElementsByTagName(strtoupper($tag));
}
return $elements;
}
/**
* Get the text value of this node's child text node.
*
* @param DOMNode $node
*
* @return string
*/
protected static function getNodeText(DOMNode $node) {
if (!$node->childNodes instanceof DOMNodeList) {
return '';
}
$result = '';
foreach ($node->childNodes as $childNode) {
if ($childNode->nodeType === XML_TEXT_NODE ||
$childNode->nodeType === XML_CDATA_SECTION_NODE) {
$result .= trim($childNode->data) . ' ';
} else {
$result .= self::getNodeText($childNode);
}
}
return str_replace(' ', ' ', $result);
}
// phpcs:enable
}
+86
View File
@@ -0,0 +1,86 @@
<?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/>.
/**
* Basic test case.
*
* @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
*/
/**
* The simplest PHPUnit test case customised for Moodle
*
* It is intended for isolated tests that do not modify database or any globals.
*
* @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
*/
abstract class basic_testcase extends base_testcase {
/**
* Constructs a test case with the given name.
*
* Note: use setUp() or setUpBeforeClass() in your test cases.
*
* @param string $name
* @param array $data
* @param string $dataName
*/
final public function __construct($name = null, array $data = array(), $dataName = '') {
parent::__construct($name, $data, $dataName);
$this->setBackupGlobals(false);
$this->setBackupStaticAttributes(false);
$this->setRunTestInSeparateProcess(false);
}
/**
* Runs the bare test sequence and log any changes in global state or database.
* @return void
*/
final public function runBare(): void {
global $DB;
try {
parent::runBare();
} catch (Exception $ex) {
$e = $ex;
} catch (Throwable $ex) {
// Engine errors in PHP7 throw exceptions of type Throwable (this "catch" will be ignored in PHP5).
$e = $ex;
}
if (isset($e)) {
// cleanup after failed expectation
phpunit_util::reset_all_data();
throw $e;
}
if ($DB->is_transaction_started()) {
phpunit_util::reset_all_data();
throw new coding_exception('basic_testcase '.$this->getName().' is not supposed to use database transactions!');
}
phpunit_util::reset_all_data(true);
}
}
@@ -0,0 +1,110 @@
<?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/>.
/**
* Constraint that checks a simple object with an isEqual constrain, allowing for exceptions to be made for some fields.
*
* @package core
* @category phpunit
* @copyright 2015 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Constraint that checks a simple object with an isEqual constrain, allowing for exceptions to be made for some fields.
*
* @package core
* @category phpunit
* @copyright 2015 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class phpunit_constraint_object_is_equal_with_exceptions extends PHPUnit\Framework\Constraint\Constraint {
/**
* @var array $keys The list of exceptions.
*/
protected $keys = array();
/**
* @var mixed $value Need to keep it here because it became private for PHPUnit 7.x and up
*/
protected $capturedvalue;
/**
* @var \PHPUnit\Framework\Constraint\IsEqual $isequal original constraint to be used internally.
*/
protected $isequal;
/**
* Override constructor to capture value
*/
public function __construct($value, float $delta = 0.0, int $maxDepth = 10, bool $canonicalize = false,
bool $ignoreCase = false) {
$this->isequal = new \PHPUnit\Framework\Constraint\IsEqual($value, $delta, $maxDepth, $canonicalize, $ignoreCase);
$this->capturedvalue = $value;
}
/**
* Add an exception for the named key to use a different comparison
* method. Any assertion provided by PHPUnit\Framework\Assert is
* acceptable.
*
* @param string $key The key to except.
* @param string $comparator The assertion to use.
*/
public function add_exception($key, $comparator) {
$this->keys[$key] = $comparator;
}
/**
* Evaluates the constraint for parameter $other
*
* If $shouldreturnesult is set to false (the default), an exception is thrown
* in case of a failure. null is returned otherwise.
*
* If $shouldreturnesult is true, the result of the evaluation is returned as
* a boolean value instead: true in case of success, false in case of a
* failure.
*
* @param mixed $other Value or object to evaluate.
* @param string $description Additional information about the test
* @param bool $shouldreturnesult Whether to return a result or throw an exception
* @return mixed
* @throws PHPUnit\Framework\ExpectationFailedException
*/
public function evaluate($other, string $description = '', bool $shouldreturnesult = false): ?bool {
foreach ($this->keys as $key => $comparison) {
if (isset($other->$key) || isset($this->capturedvalue->$key)) {
// One of the keys is present, therefore run the comparison.
PHPUnit\Framework\Assert::$comparison($this->capturedvalue->$key, $other->$key);
// Unset the keys, otherwise the standard evaluation will take place.
unset($other->$key);
unset($this->capturedvalue->$key);
}
}
// Run the IsEqual evaluation.
return $this->isequal->evaluate($other, $description, $shouldreturnesult);
}
// \PHPUnit\Framework\Constraint\IsEqual wrapping.
public function toString(): string {
return $this->isequal->toString();
}
}
+99
View File
@@ -0,0 +1,99 @@
<?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/>.
/**
* Coverage information for PHPUnit.
*
* @package core
* @category phpunit
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class phpunit_coverage_info {
/** @var array The list of folders relative to the plugin root to include in coverage generation. */
protected $includelistfolders = [];
/** @var array The list of files relative to the plugin root to include in coverage generation. */
protected $includelistfiles = [];
/** @var array The list of folders relative to the plugin root to exclude from coverage generation. */
protected $excludelistfolders = [];
/** @var array The list of files relative to the plugin root to exclude from coverage generation. */
protected $excludelistfiles = [];
/**
* Get the formatted XML list of files and folders to include.
*
* @param string $plugindir The root of the plugin, relative to the dataroot.
* @return array
*/
final public function get_includelists(string $plugindir): array {
$coverages = [];
$includelistfolders = array_merge([
'classes',
'tests/generator',
], $this->includelistfolders);;
$includelistfiles = array_merge([
'externallib.php',
'lib.php',
'locallib.php',
'renderer.php',
'rsslib.php',
], $this->includelistfiles);
if (!empty($plugindir)) {
$plugindir .= "/";
}
foreach (array_unique($includelistfolders) as $folder) {
$coverages[] = html_writer::tag('directory', "{$plugindir}{$folder}", ['suffix' => '.php']);
}
foreach (array_unique($includelistfiles) as $file) {
$coverages[] = html_writer::tag('file', "{$plugindir}{$file}");
}
return $coverages;
}
/**
* Get the formatted XML list of files and folders to exclude.
*
* @param string $plugindir The root of the plugin, relative to the dataroot.
* @return array
*/
final public function get_excludelists(string $plugindir): array {
$coverages = [];
if (!empty($plugindir)) {
$plugindir .= "/";
}
foreach ($this->excludelistfolders as $folder) {
$coverages[] = html_writer::tag('directory', "{$plugindir}{$folder}", ['suffix' => '.php']);
}
foreach ($this->excludelistfiles as $file) {
$coverages[] = html_writer::tag('file', "{$plugindir}{$file}");
}
return $coverages;
}
}
@@ -0,0 +1,234 @@
<?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/>.
/**
* Database driver test case.
*
* @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
*/
/**
* Special test case for testing of DML drivers and DDL layer.
*
* Note: Use only 'test_table*' names when creating new tables.
*
* For DML/DDL developers: you can add following settings to config.php if you want to test different driver than the main one,
* the reason is to allow testing of incomplete drivers that do not allow full PHPUnit environment
* initialisation (the database can be empty).
* $CFG->phpunit_extra_drivers = array(
* 1=>array('dbtype'=>'mysqli', 'dbhost'=>'localhost', 'dbname'=>'moodle', 'dbuser'=>'root', 'dbpass'=>'', 'prefix'=>'phpu2_'),
* 2=>array('dbtype'=>'pgsql', 'dbhost'=>'localhost', 'dbname'=>'moodle', 'dbuser'=>'postgres', 'dbpass'=>'', 'prefix'=>'phpu2_'),
* 3=>array('dbtype'=>'sqlsrv', 'dbhost'=>'127.0.0.1', 'dbname'=>'moodle', 'dbuser'=>'sa', 'dbpass'=>'', 'prefix'=>'phpu2_'),
* 4=>array('dbtype'=>'oci', 'dbhost'=>'127.0.0.1', 'dbname'=>'XE', 'dbuser'=>'sa', 'dbpass'=>'', 'prefix'=>'t_'),
* );
* define('PHPUNIT_TEST_DRIVER')=1; //number is index in the previous array
*
* @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
*/
abstract class database_driver_testcase extends base_testcase {
/** @var moodle_database connection to extra database */
private static $extradb = null;
/** @var moodle_database used in these tests*/
protected $tdb;
/**
* Constructs a test case with the given name.
*
* @param string $name
* @param array $data
* @param string $dataName
*/
final public function __construct($name = null, array $data = array(), $dataName = '') {
parent::__construct($name, $data, $dataName);
$this->setBackupGlobals(false);
$this->setBackupStaticAttributes(false);
$this->setRunTestInSeparateProcess(false);
}
public static function setUpBeforeClass(): void {
global $CFG;
parent::setUpBeforeClass();
if (!defined('PHPUNIT_TEST_DRIVER')) {
// use normal $DB
return;
}
if (!isset($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER])) {
throw new exception('Can not find driver configuration options with index: '.PHPUNIT_TEST_DRIVER);
}
$dblibrary = empty($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dblibrary']) ? 'native' : $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dblibrary'];
$dbtype = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbtype'];
$dbhost = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbhost'];
$dbname = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbname'];
$dbuser = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbuser'];
$dbpass = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dbpass'];
$prefix = $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['prefix'];
$dboptions = empty($CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dboptions']) ? array() : $CFG->phpunit_extra_drivers[PHPUNIT_TEST_DRIVER]['dboptions'];
$classname = "{$dbtype}_{$dblibrary}_moodle_database";
require_once("$CFG->libdir/dml/$classname.php");
$d = new $classname();
if (!$d->driver_installed()) {
throw new exception('Database driver for '.$classname.' is not installed');
}
$d->connect($dbhost, $dbuser, $dbpass, $dbname, $prefix, $dboptions);
self::$extradb = $d;
}
protected function setUp(): void {
global $DB;
parent::setUp();
if (self::$extradb) {
$this->tdb = self::$extradb;
} else {
$this->tdb = $DB;
}
}
protected function tearDown(): void {
// delete all test tables
$dbman = $this->tdb->get_manager();
$tables = $this->tdb->get_tables(false);
foreach($tables as $tablename) {
if (strpos($tablename, 'test_table') === 0) {
$table = new xmldb_table($tablename);
$dbman->drop_table($table);
}
}
parent::tearDown();
}
public static function tearDownAfterClass(): void {
if (self::$extradb) {
self::$extradb->dispose();
self::$extradb = null;
}
phpunit_util::reset_all_data(null);
parent::tearDownAfterClass();
}
/**
* Runs the bare test sequence.
* @return void
*/
public function runBare(): void {
try {
parent::runBare();
// Deal with any debugging messages.
$debugerror = phpunit_util::display_debugging_messages(true);
$this->resetDebugging();
if (!empty($debugerror)) {
trigger_error('Unexpected debugging() call detected.' . "\n" . $debugerror, E_USER_NOTICE);
}
} catch (Exception $ex) {
$e = $ex;
} catch (Throwable $ex) {
// Engine errors in PHP7 throw exceptions of type Throwable (this "catch" will be ignored in PHP5).
$e = $ex;
}
if (isset($e)) {
if ($this->tdb->is_transaction_started()) {
$this->tdb->force_transaction_rollback();
}
$this->tearDown();
throw $e;
}
}
/**
* Return debugging messages from the current test.
* @return array with instances having 'message', 'level' and 'stacktrace' property.
*/
public function getDebuggingMessages() {
return phpunit_util::get_debugging_messages();
}
/**
* Clear all previous debugging messages in current test.
*/
public function resetDebugging() {
phpunit_util::reset_debugging();
}
/**
* Assert that exactly debugging was just called once.
*
* Discards the debugging message if successful.
*
* @param null|string $debugmessage null means any
* @param null|string $debuglevel null means any
* @param string $message
*/
public function assertDebuggingCalled($debugmessage = null, $debuglevel = null, $message = '') {
$debugging = $this->getDebuggingMessages();
$count = count($debugging);
if ($count == 0) {
if ($message === '') {
$message = 'Expectation failed, debugging() not triggered.';
}
$this->fail($message);
}
if ($count > 1) {
if ($message === '') {
$message = 'Expectation failed, debugging() triggered '.$count.' times.';
}
$this->fail($message);
}
$this->assertEquals(1, $count);
$debug = reset($debugging);
if ($debugmessage !== null) {
$this->assertSame($debugmessage, $debug->message, $message);
}
if ($debuglevel !== null) {
$this->assertSame($debuglevel, $debug->level, $message);
}
$this->resetDebugging();
}
/**
* Call when no debugging() messages expected.
* @param string $message
*/
public function assertDebuggingNotCalled($message = '') {
$debugging = $this->getDebuggingMessages();
$count = count($debugging);
if ($message === '') {
$message = 'Expectation failed, debugging() was triggered.';
}
$this->assertEquals(0, $count, $message);
}
}
+57
View File
@@ -0,0 +1,57 @@
<?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/>.
/**
* Event mock.
*
* @package core
* @category phpunit
* @copyright 2013 Frédéric Massart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../classes/event/base.php');
/**
* Event mock class.
*
* @package core
* @category phpunit
* @copyright 2013 Frédéric Massart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class phpunit_event_mock extends \core\event\base {
/**
* Returns event context.
*
* @param \core\event\base $event event to get context for.
* @return context event context
*/
public static function testable_get_event_context($event) {
return $event->context;
}
/**
* Sets event context.
*
* @param \core\event\base $event event to set context for.
* @param context $context context to set.
*/
public static function testable_set_event_context($event, $context) {
$event->context = $context;
}
}
+87
View File
@@ -0,0 +1,87 @@
<?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/>.
/**
* Event sink.
*
* @package core
* @category phpunit
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Event redirection sink.
*
* @package core
* @category phpunit
* @copyright 2013 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class phpunit_event_sink {
/** @var \core\event\base[] array of events */
protected $events = array();
/**
* Stop event redirection.
*
* Use if you do not want event redirected any more.
*/
public function close() {
phpunit_util::stop_event_redirection();
}
/**
* To be called from phpunit_util only!
*
* @private
* @param \core\event\base $event record from event_read table
*/
public function add_event(\core\event\base $event) {
/* Number events from 0. */
$this->events[] = $event;
}
/**
* Returns all redirected events.
*
* The instances are records form the event_read table.
* The array indexes are numbered from 0 and the order is matching
* the creation of events.
*
* @return \core\event\base[]
*/
public function get_events() {
return $this->events;
}
/**
* Return number of events redirected to this sink.
*
* @return int
*/
public function count() {
return count($this->events);
}
/**
* Removes all previously stored events.
*/
public function clear() {
$this->events = array();
}
}
+119
View File
@@ -0,0 +1,119 @@
<?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/>.
/**
* Message sink.
*
* @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
*/
/**
* Message sink.
*
* @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
*/
class phpunit_message_sink {
/** @var array of records from messages table */
protected $messages = array();
/**
* Stop message redirection.
*
* Use if you do not want message redirected any more.
*/
public function close() {
phpunit_util::stop_message_redirection();
}
/**
* To be called from phpunit_util only!
*
* @param stdClass $message record from messages table
*/
public function add_message($message) {
/* Number messages from 0. */
$this->messages[] = $message;
}
/**
* Returns all redirected messages.
*
* The instances are records from the messages table.
* The array indexes are numbered from 0 and the order is matching
* the creation of events.
*
* @param callable|null $filter Use to filter the messages.
* @return array
*/
public function get_messages(?callable $filter = null): array {
if ($filter) {
return array_filter($this->messages, $filter);
}
return $this->messages;
}
/**
* Return all redirected messages for a given component.
*
* @param string $component Component name.
* @return array List of messages.
*/
public function get_messages_by_component(string $component): array {
$component = core_component::normalize_componentname($component);
return $this->get_messages(
fn ($message) => core_component::normalize_componentname($message->component) === $component,
);
}
/**
* Return all redirected messages for a given component and type.
*
* @param string $component Component name.
* @param string $type Message type.
* @return array List of messages.
*/
public function get_messages_by_component_and_type(
string $component,
string $type,
): array {
return array_filter($this->get_messages_by_component($component), function($message) use ($type) {
return $message->eventtype == $type;
});
}
/**
* Return number of messages redirected to this sink.
* @return int
*/
public function count() {
return count($this->messages);
}
/**
* Removes all previously stored messages.
*/
public function clear() {
$this->messages = array();
}
}
+87
View File
@@ -0,0 +1,87 @@
<?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/>.
/**
* phpmailer message sink.
*
* @package core
* @category phpunit
* @copyright 2013 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* phpmailer message sink.
*
* @package core
* @category phpunit
* @copyright 2013 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class phpunit_phpmailer_sink {
/**
* @var array of records which would have been sent by phpmailer.
*/
protected $messages = array();
/**
* Stop message redirection.
*
* Use if you do not want message redirected any more.
*/
public function close() {
phpunit_util::stop_phpmailer_redirection();
}
/**
* To be called from phpunit_util only!
*
* @param stdClass $message record from messages table
*/
public function add_message($message) {
/* Number messages from 0. */
$this->messages[] = $message;
}
/**
* Returns all redirected messages.
*
* The instances are records from the messages table.
* The array indexes are numbered from 0 and the order is matching
* the creation of events.
*
* @return array
*/
public function get_messages() {
return $this->messages;
}
/**
* Return number of messages redirected to this sink.
* @return int
*/
public function count() {
return count($this->messages);
}
/**
* Removes all previously stored messages.
*/
public function clear() {
$this->messages = array();
}
}
+371
View File
@@ -0,0 +1,371 @@
<?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/>.
/**
* Handle simple PHP/CSV/XML datasets to be use with ease by unit tests.
*
* This is a very minimal class, able to load data from PHP arrays and
* CSV/XML files, optionally uploading them to database.
*
* This doesn't aim to be a complex or complete solution, but just a
* utility class to replace old phpunit/dbunit uses, because that package
* is not longer maintained. Note that, ideally, generators should provide
* the needed utilities to proceed with this loading of information to
* database and, if there is any future that should be it.
*
* @package core
* @category test
* @copyright 2020 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types=1);
/**
* Lightweight dataset class for phpunit, supports XML, CSV and array datasets.
*
* This is a simple replacement class for the old old phpunit/dbunit, now
* archived. It allows to load CSV, XML and array structures to database.
*/
class phpunit_dataset {
/** @var array tables being handled by the dataset */
protected $tables = [];
/** @var array columns belonging to every table (keys) handled by the dataset */
protected $columns = [];
/** @var array rows belonging to every table (keys) handled by the dataset */
protected $rows = [];
/**
* Load information from multiple files (XML, CSV) to the dataset.
*
* This method accepts an array of full paths to CSV or XML files to be loaded
* into the dataset. For CSV files, the name of the table which the file belongs
* to needs to be specified. Example:
*
* $fullpaths = [
* '/path/to/users.xml',
* 'course' => '/path/to/courses.csv',
* ];
*
* @param array $fullpaths full paths to CSV or XML files to load.
*/
public function from_files(array $fullpaths): void {
foreach ($fullpaths as $table => $fullpath) {
$table = is_int($table) ? null : $table; // Only a table when it's an associative array.
$this->from_file($fullpath, $table);
}
}
/**
* Load information from one file (XML, CSV) to the dataset.
*
* @param string $fullpath full path to CSV or XML file to load.
* @param string|null $table name of the table which the file belongs to (only for CSV files).
*/
public function from_file(string $fullpath, ?string $table = null): void {
if (!file_exists($fullpath)) {
throw new coding_exception('from_file, file not found: ' . $fullpath);
}
if (!is_readable($fullpath)) {
throw new coding_exception('from_file, file not readable: ' . $fullpath);
}
$extension = strtolower(pathinfo($fullpath, PATHINFO_EXTENSION));
if (!in_array($extension, ['csv', 'xml'])) {
throw new coding_exception('from_file, cannot handle files with extension: ' . $extension);
}
$this->from_string(file_get_contents($fullpath), $extension, $table);
}
/**
* Load information from a string (XML, CSV) to the dataset.
*
* @param string $content contents (CSV or XML) to load.
* @param string $type format of the content to be loaded (csv or xml).
* @param string|null $table name of the table which the file belongs to (only for CSV files).
*/
public function from_string(string $content, string $type, ?string $table = null): void {
switch ($type) {
case 'xml':
$this->load_xml($content);
break;
case 'csv':
if (empty($table)) {
throw new coding_exception('from_string, contents of type "cvs" require a $table to be passed, none found');
}
$this->load_csv($content, $table);
break;
default:
throw new coding_exception('from_string, cannot handle contents of type: ' . $type);
}
}
/**
* Load information from a PHP array to the dataset.
*
* The general structure of the PHP array must be
* [table name] => [array of rows, each one being an array of values or column => values.
* The format of the array must be one of the following:
* - non-associative array, with column names in the first row (pretty much like CSV files are):
* $structure = [
* 'table 1' => [
* ['column name 1', 'column name 2'],
* ['row 1 column 1 value', 'row 1 column 2 value'*,
* ['row 2 column 1 value', 'row 2 column 2 value'*,
* ],
* 'table 2' => ...
* ];
* - associative array, with column names being keys in the array.
* $structure = [
* 'table 1' => [
* ['column name 1' => 'row 1 column 1 value', 'column name 2' => 'row 1 column 2 value'],
* ['column name 1' => 'row 2 column 1 value', 'column name 2' => 'row 2 column 2 value'],
* ],
* 'table 2' => ...
* ];
* @param array $structure php array with a valid structure to be loaded to the dataset.
*/
public function from_array(array $structure): void {
foreach ($structure as $tablename => $rows) {
if (in_array($tablename, $this->tables)) {
throw new coding_exception('from_array, table already added to dataset: ' . $tablename);
}
$this->tables[] = $tablename;
$this->columns[$tablename] = [];
$this->rows[$tablename] = [];
$isassociative = false;
$firstrow = reset($rows);
if (array_key_exists(0, $firstrow)) {
// Columns are the first row (csv-like).
$this->columns[$tablename] = $firstrow;
array_shift($rows);
} else {
// Columns are the keys on every record, first one must have all.
$this->columns[$tablename] = array_keys($firstrow);
$isassociative = true;
}
$countcols = count($this->columns[$tablename]);
foreach ($rows as $row) {
$countvalues = count($row);
if ($countcols != $countvalues) {
throw new coding_exception('from_array, number of columns must match number of values, found: ' .
$countcols . ' vs ' . $countvalues);
}
if ($isassociative && $this->columns[$tablename] != array_keys($row)) {
throw new coding_exception('from_array, columns in all elements must match first one, found: ' .
implode(', ', array_keys($row)));
}
$this->rows[$tablename][] = array_combine($this->columns[$tablename], array_values($row));
}
}
}
/**
* Send all the information to the dataset to the database.
*
* This method gets all the information loaded in the dataset, using the from_xxx() methods
* and sends it to the database; table and column names must match.
*
* Note that, if the information to be sent to database contains sequence columns (usually 'id')
* then those values will be preserved (performing an import and adjusting sequences later). Else
* normal inserts will happen and sequence (auto-increment) columns will be fed automatically.
*
* @param string[] $filter Tables to be sent to database. If not specified, all tables are processed.
*/
public function to_database(array $filter = []): void {
global $DB;
// Verify all filter elements are correct.
foreach ($filter as $table) {
if (!in_array($table, $this->tables)) {
throw new coding_exception('dataset_to_database, table is not in the dataset: ' . $table);
}
}
$structure = phpunit_util::get_tablestructure();
foreach ($this->tables as $table) {
// Apply filter.
if (!empty($filter) && !in_array($table, $filter)) {
continue;
}
$doimport = false;
if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
$doimport = in_array('id', $this->columns[$table]);
}
foreach ($this->rows[$table] as $row) {
if ($doimport) {
$DB->import_record($table, $row);
} else {
$DB->insert_record($table, $row);
}
}
if ($doimport) {
$DB->get_manager()->reset_sequence(new xmldb_table($table));
}
}
}
/**
* Returns the rows, for a given table, that the dataset holds.
*
* @param string[] $filter Tables to return rows. If not specified, all tables are processed.
* @return array tables as keys with rows on each as sub array.
*/
public function get_rows(array $filter = []): array {
// Verify all filter elements are correct.
foreach ($filter as $table) {
if (!in_array($table, $this->tables)) {
throw new coding_exception('dataset_get_rows, table is not in the dataset: ' . $table);
}
}
$result = [];
foreach ($this->tables as $table) {
// Apply filter.
if (!empty($filter) && !in_array($table, $filter)) {
continue;
}
$result[$table] = $this->rows[$table];
}
return $result;
}
/**
* Given a CSV content, process and load it as a table into the dataset.
*
* @param string $content CSV content to be loaded (only one table).
* @param string $tablename Name of the table the content belongs to.
*/
protected function load_csv(string $content, string $tablename): void {
if (in_array($tablename, $this->tables)) {
throw new coding_exception('csv_dataset_format, table already added to dataset: ' . $tablename);
}
$this->tables[] = $tablename;
$this->columns[$tablename] = [];
$this->rows[$tablename] = [];
// Normalise newlines.
$content = preg_replace('#\r\n?#', '\n', $content);
// Function str_getcsv() is not good for new lines within the data, so lets use temp file and fgetcsv() instead.
$tempfile = tempnam(make_temp_directory('phpunit'), 'csv');
$fh = fopen($tempfile, 'w+b');
fwrite($fh, $content);
// And let's read it using fgetcsv().
rewind($fh);
// We just accept default, delimiter = comma, enclosure = double quote.
while ( ($row = fgetcsv($fh) ) !== false ) {
if (empty($this->columns[$tablename])) {
$this->columns[$tablename] = $row;
} else {
$this->rows[$tablename][] = array_combine($this->columns[$tablename], $row);
}
}
fclose($fh);
unlink($tempfile);
}
/**
* Given a XML content, process and load it as tables into the dataset.
*
* @param string $content XML content to be loaded (can be multi-table).
*/
protected function load_xml(string $content): void {
$xml = new SimpleXMLElement($content);
// Main element must be dataset.
if ($xml->getName() !== 'dataset') {
throw new coding_exception('xml_dataset_format, main xml element must be "dataset", found: ' . $xml->getName());
}
foreach ($xml->children() as $table) {
// Only table elements allowed.
if ($table->getName() !== 'table') {
throw new coding_exception('xml_dataset_format, only "table" elements allowed, found: ' . $table->getName());
}
// Only allowed attribute of table is "name".
if (!isset($table['name'])) {
throw new coding_exception('xml_dataset_format, "table" element only allows "name" attribute.');
}
$tablename = (string)$table['name'];
if (in_array($tablename, $this->tables)) {
throw new coding_exception('xml_dataset_format, table already added to dataset: ' . $tablename);
}
$this->tables[] = $tablename;
$this->columns[$tablename] = [];
$this->rows[$tablename] = [];
$countcols = 0;
foreach ($table->children() as $colrow) {
// Only column and row allowed.
if ($colrow->getName() !== 'column' && $colrow->getName() !== 'row') {
throw new coding_exception('xml_dataset_format, only "column or "row" elements allowed, found: ' .
$colrow->getName());
}
// Column always before row.
if ($colrow->getName() == 'column' && !empty($this->rows[$tablename])) {
throw new coding_exception('xml_dataset_format, "column" elements always must be before "row" ones');
}
// Row always after column.
if ($colrow->getName() == 'row' && empty($this->columns[$tablename])) {
throw new coding_exception('xml_dataset_format, "row" elements always must be after "column" ones');
}
// Process column.
if ($colrow->getName() == 'column') {
$this->columns[$tablename][] = (string)$colrow;
$countcols++;
}
// Process row.
if ($colrow->getName() == 'row') {
$countvalues = 0;
$row = [];
foreach ($colrow->children() as $value) {
// Only value allowed under row.
if ($value->getName() !== 'value') {
throw new coding_exception('xml_dataset_format, only "value" elements allowed, found: ' .
$value->getName());
}
$row[$this->columns[$tablename][$countvalues]] = (string)$value;
$countvalues++;
}
if ($countcols !== $countvalues) {
throw new coding_exception('xml_dataset_format, number of columns must match number of values, found: ' .
$countcols . ' vs ' . $countvalues);
}
$this->rows[$tablename][] = $row;
}
}
}
}
}
@@ -0,0 +1,172 @@
<?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/>.
/**
* Restore dates test case.
*
* @package core
* @category test
* @copyright 2017 onwards Ankit Agarwal
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
/**
* Advanced PHPUnit test case customised for testing restore dates in Moodle.
*
* @package core
* @category test
* @copyright 2017 onwards Ankit Agarwal
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class restore_date_testcase extends advanced_testcase {
/**
* @var int Course start date.
*/
protected $startdate;
/**
* @var int Course restore date.
*/
protected $restorestartdate;
/**
* Setup.
*/
public function setUp(): void {
global $CFG;
parent::setUp();
$this->resetAfterTest();
$this->setAdminUser();
$this->startdate = strtotime('1 Jan 2017 00:00 GMT');
$this->restorestartdate = strtotime('1 Feb 2017 00:00 GMT');
$CFG->enableavailability = true;
}
/**
* Backs a course up and restores it.
*
* @param stdClass $course Course object to backup
* @param int $newdate If non-zero, specifies custom date for new course
* @return int ID of newly restored course
*/
protected function backup_and_restore($course, $newdate = 0) {
global $USER, $CFG;
// Turn off file logging, otherwise it can't delete the file (Windows).
$CFG->backup_file_logger_level = backup::LOG_NONE;
// Do backup with default settings.
set_config('backup_general_users', 1, 'backup');
$bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_GENERAL,
$USER->id);
$bc->execute_plan();
$results = $bc->get_results();
$file = $results['backup_destination'];
$fp = get_file_packer('application/vnd.moodle.backup');
$filepath = $CFG->dataroot . '/temp/backup/test-restore-course';
$file->extract_to_pathname($fp, $filepath);
$bc->destroy();
// Do restore to new course with default settings.
$newcourseid = restore_dbops::create_new_course(
$course->fullname, $course->shortname . '_2', $course->category);
$rc = new restore_controller('test-restore-course', $newcourseid,
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
backup::TARGET_NEW_COURSE);
if (empty($newdate)) {
$newdate = $this->restorestartdate;
}
$rc->get_plan()->get_setting('course_startdate')->set_value($newdate);
$this->assertTrue($rc->execute_precheck());
$rc->execute_plan();
$rc->destroy();
return $newcourseid;
}
/**
* Helper method to create a course and a module.
*
* @param string $modulename
* @param array|stdClass $record
* @return array
*/
protected function create_course_and_module($modulename, $record = []) {
if ($modulename == 'chat') {
$manager = \core_plugin_manager::resolve_plugininfo_class('mod');
$manager::enable_plugin('chat', 1);
}
if ($modulename == 'survey') {
$manager = \core_plugin_manager::resolve_plugininfo_class('mod');
$manager::enable_plugin('survey', 1);
}
// Create a course with specific start date.
$record = (array)$record;
$generator = $this->getDataGenerator();
$course = $generator->create_course(['startdate' => $this->startdate]);
$record = array_merge(['course' => $course->id], $record);
$module = $this->getDataGenerator()->create_module($modulename, $record);
return [$course, $module];
}
/**
* Verify that the given properties are not rolled.
*
* @param stdClass $oldinstance
* @param stdClass $newinstance
* @param [] $props
*/
protected function assertFieldsNotRolledForward($oldinstance, $newinstance, $props) {
foreach ($props as $prop) {
$this->assertEquals($oldinstance->$prop, $newinstance->$prop, "'$prop' should not roll forward.");
}
}
/**
* Verify that the given properties are rolled.
*
* @param stdClass $oldinstance
* @param stdClass $newinstance
* @param [] $props
*/
protected function assertFieldsRolledForward($oldinstance, $newinstance, $props) {
$diff = $this->get_diff();
foreach ($props as $prop) {
$this->assertEquals(($oldinstance->$prop + $diff), $newinstance->$prop, "'$prop' doesn't roll as expected.");
}
}
/**
* Get time diff between start date and restore date in seconds.
*
* @return mixed
*/
protected function get_diff() {
return ($this->restorestartdate - $this->startdate);
}
}
File diff suppressed because it is too large Load Diff